Skip to content

Commit d253ffa

Browse files
committed
process updates
1 parent 595ca0a commit d253ffa

File tree

14 files changed

+818
-31
lines changed

14 files changed

+818
-31
lines changed

common/schemas/entities.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,10 @@
465465
"type": "string",
466466
"description": "Optional JSON with extra context (exit code, error message, etc.)"
467467
},
468+
"processUid": {
469+
"type": "string",
470+
"description": "UUID of the associated process, if this event produced log output"
471+
},
468472
"createdAt": {
469473
"type": "string"
470474
}

common/schemas/services-api.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,10 @@
773773
"type": "string",
774774
"description": "Optional JSON with extra context (exit code, error message, etc.)"
775775
},
776+
"processUid": {
777+
"type": "string",
778+
"description": "UUID of the associated process, if this event produced log output"
779+
},
776780
"createdAt": {
777781
"type": "string"
778782
}

common/src/models/service-state-history.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,8 @@ export class ServiceStateHistory {
6363
/** Optional JSON with extra context (exit code, error message, etc.) */
6464
metadata?: string
6565

66+
/** UUID of the associated process, if this event produced log output */
67+
processUid?: string
68+
6669
createdAt!: string
6770
}

frontend/src/components/body.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ const appRoutes = {
2626
'/services/wizard/:stackName': {
2727
component: ({ match }) => <CreateServiceWizard stackName={match.params.stackName} />,
2828
},
29+
'/services/:id/logs/:processUid': {
30+
component: ({ match }) => <ServiceLogs serviceId={match.params.id} processUid={match.params.processUid} />,
31+
},
2932
'/services/:id/logs': {
3033
component: ({ match }) => <ServiceLogs serviceId={match.params.id} />,
3134
},
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { createComponent, Shade } from '@furystack/shades'
2+
3+
import type { AnsiStyle } from '../utils/parse-ansi.js'
4+
import { parseAnsi } from '../utils/parse-ansi.js'
5+
6+
const URL_REGEX = /(https?:\/\/[^\s)]+)/g
7+
8+
const renderTextWithLinks = (text: string, style: AnsiStyle) => {
9+
const parts: JSX.Element[] = []
10+
let lastIndex = 0
11+
12+
URL_REGEX.lastIndex = 0
13+
let match = URL_REGEX.exec(text)
14+
15+
while (match !== null) {
16+
const before = text.slice(lastIndex, match.index)
17+
if (before) {
18+
parts.push(<span style={style}>{before}</span>)
19+
}
20+
21+
const url = match[1]
22+
parts.push(
23+
<a
24+
href={url}
25+
target="_blank"
26+
rel="noopener noreferrer"
27+
style={{
28+
...style,
29+
color: style.color ?? '#58a6ff',
30+
textDecoration: 'underline',
31+
cursor: 'pointer',
32+
}}
33+
>
34+
{url}
35+
</a>,
36+
)
37+
38+
lastIndex = match.index + match[0].length
39+
match = URL_REGEX.exec(text)
40+
}
41+
42+
const remaining = text.slice(lastIndex)
43+
if (remaining) {
44+
parts.push(<span style={style}>{remaining}</span>)
45+
}
46+
47+
return parts
48+
}
49+
50+
type LogLineProps = {
51+
line: string
52+
}
53+
54+
export const LogLine = Shade<LogLineProps>({
55+
shadowDomName: 'shade-log-line',
56+
render: ({ props }) => {
57+
const segments = parseAnsi(props.line)
58+
59+
return (
60+
<span>
61+
{segments.flatMap((segment) => renderTextWithLinks(segment.text, segment.style))}
62+
</span>
63+
)
64+
},
65+
})

frontend/src/components/log-viewer.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ import { createComponent, Shade } from '@furystack/shades'
33
import { Input, Loader } from '@furystack/shades-common-components'
44
import { ServiceLogEntry } from 'common'
55

6+
import { LogLine } from './log-line.js'
7+
68
type LogViewerProps = {
79
serviceId: string
10+
processUid?: string
811
}
912

1013
export const LogViewer = Shade<LogViewerProps>({
@@ -15,7 +18,10 @@ export const LogViewer = Shade<LogViewerProps>({
1518
const containerRef = useRef<HTMLDivElement>('container')
1619

1720
const logsState = useCollectionSync(options, ServiceLogEntry, {
18-
filter: { serviceId: { $eq: props.serviceId } },
21+
filter: {
22+
serviceId: { $eq: props.serviceId },
23+
...(props.processUid ? { processUid: { $eq: props.processUid } } : {}),
24+
},
1925
order: { id: 'DESC' },
2026
top: 300,
2127
})
@@ -65,7 +71,9 @@ export const LogViewer = Shade<LogViewerProps>({
6571
<div style={{ opacity: '0.5', textAlign: 'center', padding: '32px' }}>No log output yet.</div>
6672
) : null}
6773
{filteredEntries.map((entry) => (
68-
<div style={{ color: entry.stream === 'stderr' ? '#f85149' : '#c9d1d9' }}>{entry.line}</div>
74+
<div style={{ color: entry.stream === 'stderr' ? '#f85149' : '#c9d1d9' }}>
75+
<LogLine line={entry.line} />
76+
</div>
6977
))}
7078
</div>
7179
</div>

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

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,16 @@ import {
1515
Paper,
1616
} from '@furystack/shades-common-components'
1717
import { ObservableValue } from '@furystack/utils'
18+
import type { ServiceView, StackView } from 'common'
1819
import {
20+
getServiceCwd,
1921
GitHubRepository,
2022
ServiceConfig,
2123
ServiceDefinition,
2224
ServiceStateHistory,
2325
ServiceStatus,
2426
StackDefinition,
2527
} from 'common'
26-
import type { ServiceView, StackView } from 'common'
27-
import { getServiceCwd } from 'common'
2828

2929
import { navigate } from '../../utils/navigate.js'
3030

@@ -440,12 +440,12 @@ type ServiceHistoryProps = {
440440
serviceId: string
441441
}
442442

443-
type HistoryColumn = 'createdAt' | 'event' | 'triggeredBy' | 'triggerSource' | 'metadata'
443+
type HistoryColumn = 'createdAt' | 'event' | 'triggeredBy' | 'triggerSource' | 'metadata' | 'processUid'
444444

445445
const ServiceHistory = Shade<ServiceHistoryProps>({
446446
shadowDomName: 'shade-service-history',
447447
render: (options) => {
448-
const { props, useDisposable } = options
448+
const { props, injector, useDisposable } = options
449449

450450
const historyState = useCollectionSync(options, ServiceStateHistory, {
451451
filter: { serviceId: { $eq: props.serviceId } },
@@ -479,7 +479,7 @@ const ServiceHistory = Shade<ServiceHistoryProps>({
479479
<div style={{ opacity: '0.6', padding: '12px 0' }}>No history entries yet.</div>
480480
) : (
481481
<DataGrid<ServiceStateHistory, HistoryColumn>
482-
columns={['createdAt', 'event', 'triggeredBy', 'triggerSource', 'metadata']}
482+
columns={['createdAt', 'event', 'triggeredBy', 'triggerSource', 'metadata', 'processUid']}
483483
findOptions={findOptions}
484484
styles={undefined}
485485
collectionService={collectionService}
@@ -489,6 +489,7 @@ const ServiceHistory = Shade<ServiceHistoryProps>({
489489
triggeredBy: () => <span>Triggered by</span>,
490490
triggerSource: () => <span>Source</span>,
491491
metadata: () => <span>Details</span>,
492+
processUid: () => <span>Logs</span>,
492493
}}
493494
rowComponents={{
494495
createdAt: (entry) => <span>{new Date(entry.createdAt).toLocaleString()}</span>,
@@ -500,6 +501,17 @@ const ServiceHistory = Shade<ServiceHistoryProps>({
500501
{entry.metadata ?? ''}
501502
</span>
502503
),
504+
processUid: (entry) =>
505+
entry.processUid ? (
506+
<Button
507+
size="small"
508+
onclick={() => navigate(injector, `/services/${props.serviceId}/logs/${entry.processUid}`)}
509+
>
510+
Show Logs
511+
</Button>
512+
) : (
513+
<span />
514+
),
503515
}}
504516
/>
505517
)}

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

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ServicesApiClient } from '../../services/api-clients/services-api-clien
66

77
type ServiceLogsProps = {
88
serviceId: string
9+
processUid?: string
910
}
1011

1112
export const ServiceLogs = Shade<ServiceLogsProps>({
@@ -39,17 +40,19 @@ export const ServiceLogs = Shade<ServiceLogsProps>({
3940
<PageContainer>
4041
<PageHeader
4142
icon="📋"
42-
title="Service Logs"
43+
title={props.processUid ? 'Process Logs' : 'Service Logs'}
4344
actions={
4445
<div style={{ display: 'flex', gap: '8px' }}>
45-
<Button
46-
variant="outlined"
47-
color="error"
48-
onclick={() => void handleClearLogs()}
49-
startIcon={<Icon icon={icons.trash} size="small" />}
50-
>
51-
Clear Logs
52-
</Button>
46+
{!props.processUid && (
47+
<Button
48+
variant="outlined"
49+
color="error"
50+
onclick={() => void handleClearLogs()}
51+
startIcon={<Icon icon={icons.trash} size="small" />}
52+
>
53+
Clear Logs
54+
</Button>
55+
)}
5356
<Button
5457
variant="outlined"
5558
onclick={() => history.back()}
@@ -61,7 +64,7 @@ export const ServiceLogs = Shade<ServiceLogsProps>({
6164
}
6265
/>
6366
<Paper style={{ flex: '1', overflow: 'hidden' }}>
64-
<LogViewer serviceId={props.serviceId} />
67+
<LogViewer serviceId={props.serviceId} processUid={props.processUid} />
6568
</Paper>
6669
</PageContainer>
6770
)

0 commit comments

Comments
 (0)