Skip to content

Commit 9275125

Browse files
committed
service details page refactor
1 parent 1b15900 commit 9275125

4 files changed

Lines changed: 176 additions & 169 deletions

File tree

frontend/src/components/service-table.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { ServiceView } from 'common'
88

99
import { ServicesApiClient } from '../services/api-clients/services-api-client.js'
1010
import { applyClientFindOptions } from '../utils/apply-client-find-options.js'
11-
import { ServiceStatusIndicator } from './service-status-indicator.js'
11+
import { RunStatusChip } from './status-chips.js'
1212

1313
type ServiceTableProps = {
1414
services: ServiceView[]
@@ -86,7 +86,7 @@ export const ServiceTable = Shade<ServiceTableProps>({
8686
) : null}
8787
</span>
8888
),
89-
runStatus: (entry) => <ServiceStatusIndicator service={entry} />,
89+
runStatus: (entry) => <RunStatusChip status={entry.runStatus} />,
9090
actions: (entry) => {
9191
const needsSetup =
9292
(entry.repositoryId && entry.cloneStatus !== 'cloned') ||
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { createComponent, Shade } from '@furystack/shades'
2+
import { Chip } from '@furystack/shades-common-components'
3+
import type { Palette } from '@furystack/shades-common-components'
4+
import type { CloneStatus, InstallStatus, BuildStatus, RunStatus } from 'common'
5+
6+
type StatusMapping<T extends string> = Record<T, { label: string; color: keyof Palette; icon: string }>
7+
8+
const cloneStatusMap: StatusMapping<CloneStatus> = {
9+
'not-cloned': { label: 'Not Cloned', color: 'secondary', icon: '·' },
10+
cloning: { label: 'Cloning', color: 'warning', icon: '⏳' },
11+
cloned: { label: 'Cloned', color: 'success', icon: '✓' },
12+
failed: { label: 'Failed', color: 'error', icon: '✗' },
13+
}
14+
15+
const installStatusMap: StatusMapping<InstallStatus> = {
16+
'not-installed': { label: 'Not Installed', color: 'secondary', icon: '·' },
17+
installing: { label: 'Installing', color: 'warning', icon: '⏳' },
18+
installed: { label: 'Installed', color: 'success', icon: '✓' },
19+
failed: { label: 'Failed', color: 'error', icon: '✗' },
20+
}
21+
22+
const buildStatusMap: StatusMapping<BuildStatus> = {
23+
'not-built': { label: 'Not Built', color: 'secondary', icon: '·' },
24+
building: { label: 'Building', color: 'warning', icon: '⏳' },
25+
built: { label: 'Built', color: 'success', icon: '✓' },
26+
failed: { label: 'Failed', color: 'error', icon: '✗' },
27+
}
28+
29+
const runStatusMap: StatusMapping<RunStatus> = {
30+
stopped: { label: 'Stopped', color: 'secondary', icon: '·' },
31+
starting: { label: 'Starting', color: 'warning', icon: '⏳' },
32+
running: { label: 'Running', color: 'success', icon: '✓' },
33+
stopping: { label: 'Stopping', color: 'warning', icon: '⏳' },
34+
error: { label: 'Error', color: 'error', icon: '✗' },
35+
}
36+
37+
export const CloneStatusChip = Shade<{ status: CloneStatus }>({
38+
shadowDomName: 'shade-clone-status-chip',
39+
render: ({ props }) => {
40+
const { label, color, icon } = cloneStatusMap[props.status]
41+
return (
42+
<Chip variant="outlined" color={color} size="small">
43+
{icon} {label}
44+
</Chip>
45+
)
46+
},
47+
})
48+
49+
export const InstallStatusChip = Shade<{ status: InstallStatus }>({
50+
shadowDomName: 'shade-install-status-chip',
51+
render: ({ props }) => {
52+
const { label, color, icon } = installStatusMap[props.status]
53+
return (
54+
<Chip variant="outlined" color={color} size="small">
55+
{icon} {label}
56+
</Chip>
57+
)
58+
},
59+
})
60+
61+
export const BuildStatusChip = Shade<{ status: BuildStatus }>({
62+
shadowDomName: 'shade-build-status-chip',
63+
render: ({ props }) => {
64+
const { label, color, icon } = buildStatusMap[props.status]
65+
return (
66+
<Chip variant="outlined" color={color} size="small">
67+
{icon} {label}
68+
</Chip>
69+
)
70+
},
71+
})
72+
73+
export const RunStatusChip = Shade<{ status: RunStatus }>({
74+
shadowDomName: 'shade-run-status-chip',
75+
render: ({ props }) => {
76+
const { label, color, icon } = runStatusMap[props.status]
77+
return (
78+
<Chip variant="outlined" color={color} size="small">
79+
{icon} {label}
80+
</Chip>
81+
)
82+
},
83+
})

frontend/src/pages/services/service-detail.tsx

Lines changed: 81 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { createComponent, LocationService, NestedRouteLink, Shade } from '@furys
44
import type { ColumnFilterConfig } from '@furystack/shades-common-components'
55
import {
66
Button,
7-
ButtonGroup,
87
CollectionService,
98
DataGrid,
109
Icon,
@@ -32,6 +31,12 @@ import { navigate } from '../../utils/navigate.js'
3231
import { ConfirmDialog } from '../../components/confirm-dialog.js'
3332
import { ServiceForm } from '../../components/entity-forms/service-form.js'
3433
import { ServiceStatusIndicator } from '../../components/service-status-indicator.js'
34+
import {
35+
BuildStatusChip,
36+
CloneStatusChip,
37+
InstallStatusChip,
38+
RunStatusChip,
39+
} from '../../components/status-chips.js'
3540
import { ServicesApiClient } from '../../services/api-clients/services-api-client.js'
3641

3742
const eventLabels: Record<string, string> = {
@@ -270,50 +275,6 @@ export const ServiceDetail = Shade<ServiceDetailProps>({
270275
actions={
271276
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}>
272277
<ServiceStatusIndicator service={service} />
273-
<ButtonGroup variant="outlined">
274-
{service.runStatus !== 'running' ? (
275-
<Button
276-
size="small"
277-
color="success"
278-
loading={actionInProgress === 'start'}
279-
disabled={!!actionInProgress}
280-
onclick={() => void runAction('start', '/services/:id/start')}
281-
startIcon={<Icon icon={icons.play} size="small" />}
282-
>
283-
Start
284-
</Button>
285-
) : (
286-
<Button
287-
size="small"
288-
loading={actionInProgress === 'stop'}
289-
disabled={!!actionInProgress}
290-
onclick={() => void runAction('stop', '/services/:id/stop')}
291-
startIcon={<Icon icon={icons.stopCircle} size="small" />}
292-
>
293-
Stop
294-
</Button>
295-
)}
296-
<Button
297-
size="small"
298-
loading={actionInProgress === 'restart'}
299-
disabled={!!actionInProgress}
300-
onclick={() => void runAction('restart', '/services/:id/restart')}
301-
startIcon={<Icon icon={icons.refresh} size="small" />}
302-
>
303-
Restart
304-
</Button>
305-
{service.repositoryId ? (
306-
<Button
307-
size="small"
308-
loading={actionInProgress === 'update'}
309-
disabled={!!actionInProgress}
310-
onclick={() => void runAction('update', '/services/:id/update')}
311-
startIcon={<Icon icon={icons.download} size="small" />}
312-
>
313-
Update
314-
</Button>
315-
) : null}
316-
</ButtonGroup>
317278
<NestedRouteLink href={`/services/${service.id}/logs`}>
318279
<Button variant="outlined" size="small" startIcon={<Icon icon={icons.file} size="small" />}>
319280
Logs
@@ -340,7 +301,15 @@ export const ServiceDetail = Shade<ServiceDetailProps>({
340301
}
341302
/>
342303
<Paper>
343-
<div style={{ display: 'grid', gridTemplateColumns: '200px 1fr', gap: '8px 16px', fontSize: '14px' }}>
304+
<div
305+
style={{
306+
display: 'grid',
307+
gridTemplateColumns: '200px 1fr auto',
308+
gap: '8px 16px',
309+
fontSize: '14px',
310+
alignItems: 'center',
311+
}}
312+
>
344313
{linkedRepo ? (
345314
<div style={{ display: 'contents' }}>
346315
<strong>Repository</strong>
@@ -349,46 +318,43 @@ export const ServiceDetail = Shade<ServiceDetailProps>({
349318
{linkedRepo.displayName}
350319
</NestedRouteLink>
351320
</span>
321+
<span>
322+
{linkedRepo.url ? (
323+
<a href={linkedRepo.url} target="_blank" rel="noopener noreferrer" style={{ color: 'inherit' }}>
324+
<Button variant="outlined" size="small" startIcon={<Icon icon={icons.externalLink} size="small" />}>
325+
Open
326+
</Button>
327+
</a>
328+
) : null}
329+
</span>
352330
</div>
353331
) : null}
354332
<strong>Working Directory</strong>
355333
<span style={{ fontFamily: 'monospace' }}>{fullCwd ?? '(loading…)'}</span>
356-
<strong>Run Command</strong>
357-
<span style={{ fontFamily: 'monospace' }}>{service.runCommand}</span>
358-
{service.installCommand ? (
359-
<div style={{ display: 'contents' }}>
360-
<strong>Install Command</strong>
361-
<span style={{ fontFamily: 'monospace' }}>{service.installCommand}</span>
362-
</div>
363-
) : null}
364-
{service.buildCommand ? (
334+
<span />
335+
{service.repositoryId ? (
365336
<div style={{ display: 'contents' }}>
366-
<strong>Build Command</strong>
367-
<span style={{ fontFamily: 'monospace' }}>{service.buildCommand}</span>
337+
<strong>Clone</strong>
338+
<CloneStatusChip status={service.cloneStatus} />
339+
<Button
340+
variant="outlined"
341+
size="small"
342+
loading={actionInProgress === 'pull'}
343+
disabled={!!actionInProgress}
344+
onclick={() => void runAction('pull', '/services/:id/pull')}
345+
startIcon={<Icon icon={icons.download} size="small" />}
346+
>
347+
{service.cloneStatus === 'not-cloned' ? 'Clone' : 'Pull'}
348+
</Button>
368349
</div>
369350
) : null}
370-
{service.repositoryId ? (
351+
{service.installCommand ? (
371352
<div style={{ display: 'contents' }}>
372-
<strong>Clone Status</strong>
353+
<strong>Install</strong>
373354
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
374-
<span>{service.cloneStatus}</span>
375-
<Button
376-
variant="outlined"
377-
size="small"
378-
loading={actionInProgress === 'pull'}
379-
disabled={!!actionInProgress}
380-
onclick={() => void runAction('pull', '/services/:id/pull')}
381-
startIcon={<Icon icon={icons.download} size="small" />}
382-
>
383-
{service.cloneStatus === 'not-cloned' ? 'Clone' : 'Pull'}
384-
</Button>
355+
<span style={{ fontFamily: 'monospace' }}>{service.installCommand}</span>
356+
<InstallStatusChip status={service.installStatus} />
385357
</div>
386-
</div>
387-
) : null}
388-
<strong>Install Status</strong>
389-
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
390-
<span>{service.installStatus}</span>
391-
{service.installCommand ? (
392358
<Button
393359
variant="outlined"
394360
size="small"
@@ -399,12 +365,15 @@ export const ServiceDetail = Shade<ServiceDetailProps>({
399365
>
400366
Install
401367
</Button>
402-
) : null}
403-
</div>
404-
<strong>Build Status</strong>
405-
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
406-
<span>{service.buildStatus}</span>
407-
{service.buildCommand ? (
368+
</div>
369+
) : null}
370+
{service.buildCommand ? (
371+
<div style={{ display: 'contents' }}>
372+
<strong>Build</strong>
373+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
374+
<span style={{ fontFamily: 'monospace' }}>{service.buildCommand}</span>
375+
<BuildStatusChip status={service.buildStatus} />
376+
</div>
408377
<Button
409378
variant="outlined"
410379
size="small"
@@ -415,10 +384,37 @@ export const ServiceDetail = Shade<ServiceDetailProps>({
415384
>
416385
Build
417386
</Button>
418-
) : null}
387+
</div>
388+
) : null}
389+
<strong>Run</strong>
390+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
391+
<span style={{ fontFamily: 'monospace' }}>{service.runCommand}</span>
392+
<RunStatusChip status={service.runStatus} />
419393
</div>
420-
<strong>Run Status</strong>
421-
<span>{service.runStatus}</span>
394+
{service.runStatus !== 'running' ? (
395+
<Button
396+
variant="outlined"
397+
size="small"
398+
color="success"
399+
loading={actionInProgress === 'start'}
400+
disabled={!!actionInProgress}
401+
onclick={() => void runAction('start', '/services/:id/start')}
402+
startIcon={<Icon icon={icons.play} size="small" />}
403+
>
404+
Start
405+
</Button>
406+
) : (
407+
<Button
408+
variant="outlined"
409+
size="small"
410+
loading={actionInProgress === 'stop'}
411+
disabled={!!actionInProgress}
412+
onclick={() => void runAction('stop', '/services/:id/stop')}
413+
startIcon={<Icon icon={icons.stopCircle} size="small" />}
414+
>
415+
Stop
416+
</Button>
417+
)}
422418
</div>
423419
</Paper>
424420
<ServiceHistory serviceId={service.id} />

0 commit comments

Comments
 (0)