@shipfox/client-workflows 20.0.0 → 22.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +51 -0
- package/dist/components/job-detail/job-detail-view.d.ts.map +1 -1
- package/dist/components/job-detail/job-detail-view.js +232 -44
- package/dist/components/job-detail/job-detail-view.js.map +1 -1
- package/dist/components/job-detail/job-empty-states.js +2 -2
- package/dist/components/job-detail/job-empty-states.js.map +1 -1
- package/dist/components/job-detail/step-attempt-log-panel.d.ts +7 -1
- package/dist/components/job-detail/step-attempt-log-panel.d.ts.map +1 -1
- package/dist/components/job-detail/step-attempt-log-panel.js +37 -10
- package/dist/components/job-detail/step-attempt-log-panel.js.map +1 -1
- package/dist/components/job-graph/job-graph-content.js +2 -2
- package/dist/components/job-graph/job-graph-content.js.map +1 -1
- package/dist/components/step-list/step-list-model.d.ts +1 -0
- package/dist/components/step-list/step-list-model.d.ts.map +1 -1
- package/dist/components/step-list/step-list-model.js +1 -1
- package/dist/components/step-list/step-list-model.js.map +1 -1
- package/dist/components/step-list/step-list.d.ts +2 -1
- package/dist/components/step-list/step-list.d.ts.map +1 -1
- package/dist/components/step-list/step-list.js +25 -9
- package/dist/components/step-list/step-list.js.map +1 -1
- package/dist/components/workflow-run-list/job-status-strip.d.ts +9 -1
- package/dist/components/workflow-run-list/job-status-strip.d.ts.map +1 -1
- package/dist/components/workflow-run-list/job-status-strip.js +22 -11
- package/dist/components/workflow-run-list/job-status-strip.js.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-filters.js +1 -1
- package/dist/components/workflow-run-list/workflow-run-filters.js.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-list-content.d.ts.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-list-content.js +13 -7
- package/dist/components/workflow-run-list/workflow-run-list-content.js.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-list-states.d.ts.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-list-states.js +43 -38
- package/dist/components/workflow-run-list/workflow-run-list-states.js.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-list-view.d.ts.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-list-view.js +34 -22
- package/dist/components/workflow-run-list/workflow-run-list-view.js.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-row.d.ts +3 -3
- package/dist/components/workflow-run-list/workflow-run-row.d.ts.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-row.js +25 -18
- package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
- package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
- package/dist/components/workflow-run-summary/workflow-run-summary.js +4 -4
- package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
- package/dist/components/workflow-run-view/run-workspace-nav.d.ts.map +1 -1
- package/dist/components/workflow-run-view/run-workspace-nav.js +20 -7
- package/dist/components/workflow-run-view/run-workspace-nav.js.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-states.d.ts.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-states.js +1 -1
- package/dist/components/workflow-run-view/workflow-run-states.js.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-view.js +103 -78
- package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
- package/dist/components/workflow-source-panel/workflow-source-content.js +1 -1
- package/dist/components/workflow-source-panel/workflow-source-content.js.map +1 -1
- package/dist/pages/project-workflows-page.d.ts.map +1 -1
- package/dist/pages/project-workflows-page.js +171 -158
- package/dist/pages/project-workflows-page.js.map +1 -1
- package/dist/pages/workflow-job-detail-page.d.ts.map +1 -1
- package/dist/pages/workflow-job-detail-page.js +3 -2
- package/dist/pages/workflow-job-detail-page.js.map +1 -1
- package/dist/pages/workflow-run-detail-page.js +2 -1
- package/dist/pages/workflow-run-detail-page.js.map +1 -1
- package/dist/pages/workflow-run-list-page.d.ts.map +1 -1
- package/dist/pages/workflow-run-list-page.js +9 -11
- package/dist/pages/workflow-run-list-page.js.map +1 -1
- package/dist/routes/job-detail.d.ts +1 -1
- package/dist/routes/job-detail.js +1 -1
- package/dist/routes/job-detail.js.map +1 -1
- package/dist/routes/run-detail.d.ts +1 -1
- package/dist/routes/run-detail.js +1 -1
- package/dist/routes/run-detail.js.map +1 -1
- package/dist/routes/runs.d.ts +1 -1
- package/dist/routes/runs.js +1 -1
- package/dist/routes/runs.js.map +1 -1
- package/dist/routes/workflows.d.ts +3 -0
- package/dist/routes/workflows.js +3 -0
- package/dist/routes/workflows.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/src/components/job-detail/job-detail-view.tsx +264 -49
- package/src/components/job-detail/job-empty-states.tsx +2 -2
- package/src/components/job-detail/step-attempt-log-panel.test.tsx +30 -12
- package/src/components/job-detail/step-attempt-log-panel.tsx +68 -10
- package/src/components/job-graph/job-graph-content.tsx +2 -2
- package/src/components/job-graph/job-graph.stories.tsx +1 -1
- package/src/components/job-graph/job-node.stories.tsx +1 -1
- package/src/components/step-list/step-list-model.ts +1 -1
- package/src/components/step-list/step-list.test.tsx +29 -1
- package/src/components/step-list/step-list.tsx +29 -11
- package/src/components/workflow-run-list/job-status-strip.tsx +25 -12
- package/src/components/workflow-run-list/workflow-run-filters.tsx +1 -1
- package/src/components/workflow-run-list/workflow-run-list-content.tsx +7 -2
- package/src/components/workflow-run-list/workflow-run-list-states.tsx +42 -41
- package/src/components/workflow-run-list/workflow-run-list-view.test.tsx +53 -4
- package/src/components/workflow-run-list/workflow-run-list-view.tsx +28 -22
- package/src/components/workflow-run-list/workflow-run-list.stories.tsx +9 -20
- package/src/components/workflow-run-list/workflow-run-row.tsx +26 -19
- package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +3 -3
- package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +11 -2
- package/src/components/workflow-run-summary/workflow-run-summary.tsx +4 -7
- package/src/components/workflow-run-view/run-workspace-nav.test.tsx +40 -2
- package/src/components/workflow-run-view/run-workspace-nav.tsx +28 -9
- package/src/components/workflow-run-view/workflow-run-states.tsx +1 -4
- package/src/components/workflow-run-view/workflow-run-view.stories.tsx +142 -0
- package/src/components/workflow-run-view/workflow-run-view.test.tsx +42 -8
- package/src/components/workflow-run-view/workflow-run-view.tsx +112 -96
- package/src/components/workflow-source-panel/workflow-source-content.tsx +2 -2
- package/src/pages/project-workflows-page.tsx +135 -125
- package/src/pages/workflow-job-detail-page.test.tsx +32 -0
- package/src/pages/workflow-job-detail-page.tsx +5 -1
- package/src/pages/workflow-run-detail-page.tsx +1 -1
- package/src/pages/workflow-run-list-page.tsx +9 -11
- package/src/pages/workflow-run-pages.test.tsx +26 -0
- package/src/routes/job-detail.tsx +1 -1
- package/src/routes/run-detail.tsx +1 -1
- package/src/routes/runs.tsx +1 -1
- package/src/routes/workflows.tsx +1 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -13,7 +13,7 @@ import {JobExecutionTimeText} from './job-execution-time-text.js';
|
|
|
13
13
|
const TAIL_FOLLOW_THRESHOLD_PX = 24;
|
|
14
14
|
const INITIAL_LOG_ERROR_RETRY_COUNT = 5;
|
|
15
15
|
const INITIAL_LOG_ERROR_RETRY_DELAY_MS = 1_500;
|
|
16
|
-
const defaultLogSurfaceClasses = 'rounded-
|
|
16
|
+
const defaultLogSurfaceClasses = 'rounded-none border-0 bg-background-contrast-base shadow-none';
|
|
17
17
|
|
|
18
18
|
export interface StepAttemptLogPanelProps {
|
|
19
19
|
stepId: string;
|
|
@@ -24,6 +24,12 @@ export interface StepAttemptLogPanelProps {
|
|
|
24
24
|
/** The page scroll column used by the uncapped job-detail log surface. */
|
|
25
25
|
pageScrollRef: RefObject<HTMLElement | null>;
|
|
26
26
|
surfaceClassName?: string | undefined;
|
|
27
|
+
search?: string | undefined;
|
|
28
|
+
wrap?: boolean | undefined;
|
|
29
|
+
showLineNumbers?: boolean | undefined;
|
|
30
|
+
attemptId?: string | undefined;
|
|
31
|
+
refreshToken?: number | undefined;
|
|
32
|
+
onFetchingChange?: ((attemptId: string, isFetching: boolean) => void) | undefined;
|
|
27
33
|
initialErrorRetryCount?: number | undefined;
|
|
28
34
|
initialErrorRetryDelayMs?: number | undefined;
|
|
29
35
|
}
|
|
@@ -35,6 +41,12 @@ export function StepAttemptLogPanel({
|
|
|
35
41
|
attemptStartedAt,
|
|
36
42
|
pageScrollRef,
|
|
37
43
|
surfaceClassName = defaultLogSurfaceClasses,
|
|
44
|
+
search = '',
|
|
45
|
+
wrap = false,
|
|
46
|
+
showLineNumbers = true,
|
|
47
|
+
attemptId,
|
|
48
|
+
refreshToken = 0,
|
|
49
|
+
onFetchingChange,
|
|
38
50
|
initialErrorRetryCount = INITIAL_LOG_ERROR_RETRY_COUNT,
|
|
39
51
|
initialErrorRetryDelayMs = INITIAL_LOG_ERROR_RETRY_DELAY_MS,
|
|
40
52
|
}: StepAttemptLogPanelProps) {
|
|
@@ -55,6 +67,18 @@ export function StepAttemptLogPanel({
|
|
|
55
67
|
retryMissingStream && query.data === undefined && isMissingStepLogStreamError(query.error);
|
|
56
68
|
const initialError = query.isError && query.data === undefined && !missingActiveStream;
|
|
57
69
|
const staleError = query.isError && query.data !== undefined;
|
|
70
|
+
const lastRefreshTokenRef = useRef(refreshToken);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (refreshToken === lastRefreshTokenRef.current) return;
|
|
74
|
+
lastRefreshTokenRef.current = refreshToken;
|
|
75
|
+
void query.refetchLogs();
|
|
76
|
+
}, [query.refetchLogs, refreshToken]);
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!attemptId) return;
|
|
80
|
+
onFetchingChange?.(attemptId, query.isFetching);
|
|
81
|
+
}, [attemptId, onFetchingChange, query.isFetching]);
|
|
58
82
|
|
|
59
83
|
useEffect(() => {
|
|
60
84
|
const scrollElement = pageScrollRef.current;
|
|
@@ -85,18 +109,32 @@ export function StepAttemptLogPanel({
|
|
|
85
109
|
}, [anchorToFailure, pageScrollRef, recordCount]);
|
|
86
110
|
|
|
87
111
|
if (query.isPending) {
|
|
88
|
-
return
|
|
112
|
+
return (
|
|
113
|
+
<StepLogsLoadingSurface
|
|
114
|
+
label="Loading logs"
|
|
115
|
+
className={surfaceClassName}
|
|
116
|
+
wrap={wrap}
|
|
117
|
+
showLineNumbers={showLineNumbers}
|
|
118
|
+
/>
|
|
119
|
+
);
|
|
89
120
|
}
|
|
90
121
|
|
|
91
122
|
if (missingActiveStream) {
|
|
92
123
|
if (attemptStatus === 'running' && attemptStartedAt) {
|
|
93
124
|
return <StepLogsWaitingSurface startedAt={attemptStartedAt} className={surfaceClassName} />;
|
|
94
125
|
}
|
|
95
|
-
return
|
|
126
|
+
return (
|
|
127
|
+
<StepLogsLoadingSurface
|
|
128
|
+
label="Waiting for logs"
|
|
129
|
+
className={surfaceClassName}
|
|
130
|
+
wrap={wrap}
|
|
131
|
+
showLineNumbers={showLineNumbers}
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
96
134
|
}
|
|
97
135
|
|
|
98
136
|
if (initialError) {
|
|
99
|
-
return <StepLogsError retrying={query.isFetching} onRetry={() => void query.
|
|
137
|
+
return <StepLogsError retrying={query.isFetching} onRetry={() => void query.refetchLogs()} />;
|
|
100
138
|
}
|
|
101
139
|
|
|
102
140
|
return (
|
|
@@ -115,7 +153,7 @@ export function StepAttemptLogPanel({
|
|
|
115
153
|
size="2xs"
|
|
116
154
|
variant="secondary"
|
|
117
155
|
isLoading={query.isFetching}
|
|
118
|
-
onClick={() => void query.
|
|
156
|
+
onClick={() => void query.refetchLogs()}
|
|
119
157
|
>
|
|
120
158
|
Retry
|
|
121
159
|
</Button>
|
|
@@ -124,19 +162,32 @@ export function StepAttemptLogPanel({
|
|
|
124
162
|
) : null}
|
|
125
163
|
<LogView
|
|
126
164
|
records={records}
|
|
165
|
+
search={search}
|
|
166
|
+
wrap={wrap}
|
|
167
|
+
showLineNumbers={showLineNumbers}
|
|
127
168
|
emptyState={query.data?.complete ? 'complete' : 'pending'}
|
|
128
169
|
anchorToFailure={anchorToFailure}
|
|
129
|
-
ariaLive={attemptStatus === 'running' ? 'polite' : 'off'}
|
|
170
|
+
ariaLive={search.trim() ? 'off' : attemptStatus === 'running' ? 'polite' : 'off'}
|
|
130
171
|
className={surfaceClassName}
|
|
131
172
|
/>
|
|
132
173
|
</div>
|
|
133
174
|
);
|
|
134
175
|
}
|
|
135
176
|
|
|
136
|
-
function StepLogsLoadingSurface({
|
|
177
|
+
function StepLogsLoadingSurface({
|
|
178
|
+
label,
|
|
179
|
+
className,
|
|
180
|
+
wrap,
|
|
181
|
+
showLineNumbers,
|
|
182
|
+
}: {
|
|
183
|
+
label: string;
|
|
184
|
+
className: string;
|
|
185
|
+
wrap: boolean;
|
|
186
|
+
showLineNumbers: boolean;
|
|
187
|
+
}) {
|
|
137
188
|
return (
|
|
138
189
|
<div role="status" aria-label={label}>
|
|
139
|
-
<LogViewSkeleton className={className} />
|
|
190
|
+
<LogViewSkeleton className={className} wrap={wrap} showLineNumbers={showLineNumbers} />
|
|
140
191
|
</div>
|
|
141
192
|
);
|
|
142
193
|
}
|
|
@@ -144,7 +195,7 @@ function StepLogsLoadingSurface({label, className}: {label: string; className: s
|
|
|
144
195
|
function StepLogsWaitingSurface({startedAt, className}: {startedAt: string; className: string}) {
|
|
145
196
|
return (
|
|
146
197
|
<div role="status" aria-label="Waiting for logs" className={className}>
|
|
147
|
-
<Text size="xs" className="px-tight py-row text-foreground-
|
|
198
|
+
<Text size="xs" className="px-tight py-row text-foreground-contrast-secondary">
|
|
148
199
|
Waiting for output ·{' '}
|
|
149
200
|
<span className="font-code tabular-nums" aria-hidden="true">
|
|
150
201
|
<JobExecutionTimeText time={{state: 'live', fromIso: startedAt}} />
|
|
@@ -188,7 +239,14 @@ function StepLogsError({retrying, onRetry}: {retrying: boolean; onRetry: () => v
|
|
|
188
239
|
>
|
|
189
240
|
<div className="flex min-w-0 flex-1 items-center justify-between gap-inline">
|
|
190
241
|
<Text size="xs">Could not load logs.</Text>
|
|
191
|
-
<Button
|
|
242
|
+
<Button
|
|
243
|
+
type="button"
|
|
244
|
+
size="2xs"
|
|
245
|
+
variant="secondary"
|
|
246
|
+
isLoading={retrying}
|
|
247
|
+
disabled={retrying}
|
|
248
|
+
onClick={onRetry}
|
|
249
|
+
>
|
|
192
250
|
Retry
|
|
193
251
|
</Button>
|
|
194
252
|
</div>
|
|
@@ -37,11 +37,11 @@ export function JobGraphContent({
|
|
|
37
37
|
if (model.nodes.length === 0) {
|
|
38
38
|
return (
|
|
39
39
|
<EmptyState
|
|
40
|
-
className="min-h-160
|
|
40
|
+
className="min-h-160"
|
|
41
41
|
icon="componentLine"
|
|
42
42
|
title="No jobs yet"
|
|
43
43
|
description="This run has not materialized jobs."
|
|
44
|
-
variant="
|
|
44
|
+
variant="panel"
|
|
45
45
|
/>
|
|
46
46
|
);
|
|
47
47
|
}
|
|
@@ -11,7 +11,7 @@ const meta = {
|
|
|
11
11
|
parameters: {layout: 'centered'},
|
|
12
12
|
decorators: [
|
|
13
13
|
(Story) => (
|
|
14
|
-
<div className="h-520 w-900 overflow-auto bg-background-
|
|
14
|
+
<div className="h-520 w-900 overflow-auto bg-background-subtle-base p-16">
|
|
15
15
|
<Story />
|
|
16
16
|
</div>
|
|
17
17
|
),
|
|
@@ -132,7 +132,7 @@ function toStepModel(step: Step, index: number): StepModel {
|
|
|
132
132
|
};
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
function stepLabel(step: Step, index: number): string {
|
|
135
|
+
export function stepLabel(step: Step, index: number): string {
|
|
136
136
|
const name = step.name.trim();
|
|
137
137
|
if (name) return name;
|
|
138
138
|
|
|
@@ -145,6 +145,34 @@ describe('StepList', () => {
|
|
|
145
145
|
expect(screen.getByText(`logs for ${deployAttempt.id}`)).toBeInTheDocument();
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
+
test('reports the expanded attempt ids as rows change', async () => {
|
|
149
|
+
const user = userEvent.setup();
|
|
150
|
+
const onExpandedAttemptIdsChange = vi.fn();
|
|
151
|
+
const buildAttempt = makeAttempt({status: 'succeeded'});
|
|
152
|
+
const deployAttempt = makeAttempt({status: 'running'});
|
|
153
|
+
const build = makeStep({name: 'build', attempts: [buildAttempt]});
|
|
154
|
+
const deploy = makeStep({name: 'deploy', position: 1, attempts: [deployAttempt]});
|
|
155
|
+
render(
|
|
156
|
+
<StepList
|
|
157
|
+
job={makeJob({steps: [build, deploy]})}
|
|
158
|
+
onExpandedAttemptIdsChange={onExpandedAttemptIdsChange}
|
|
159
|
+
renderExpandedStep={({attemptId}) => <Text size="sm">logs for {attemptId}</Text>}
|
|
160
|
+
/>,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
await user.click(screen.getByRole('button', {name: 'build, Succeeded, attempt 1'}));
|
|
164
|
+
expect(onExpandedAttemptIdsChange).toHaveBeenLastCalledWith([buildAttempt.id]);
|
|
165
|
+
|
|
166
|
+
await user.click(screen.getByRole('button', {name: 'deploy, Running, attempt 1'}));
|
|
167
|
+
expect(onExpandedAttemptIdsChange).toHaveBeenLastCalledWith([
|
|
168
|
+
buildAttempt.id,
|
|
169
|
+
deployAttempt.id,
|
|
170
|
+
]);
|
|
171
|
+
|
|
172
|
+
await user.click(screen.getByRole('button', {name: 'build, Succeeded, attempt 1'}));
|
|
173
|
+
expect(onExpandedAttemptIdsChange).toHaveBeenLastCalledWith([deployAttempt.id]);
|
|
174
|
+
});
|
|
175
|
+
|
|
148
176
|
test('keeps multiple expanded rows open when external attempt selection is singular', async () => {
|
|
149
177
|
const user = userEvent.setup();
|
|
150
178
|
const buildAttempt = makeAttempt({status: 'succeeded'});
|
|
@@ -210,7 +238,7 @@ describe('StepList', () => {
|
|
|
210
238
|
await user.click(deployRow);
|
|
211
239
|
|
|
212
240
|
expect(buildRow.parentElement?.parentElement).not.toHaveClass('bg-background-components-hover');
|
|
213
|
-
expect(deployRow.parentElement?.parentElement).toHaveClass('bg-background-
|
|
241
|
+
expect(deployRow.parentElement?.parentElement).toHaveClass('bg-background-neutral-hover');
|
|
214
242
|
});
|
|
215
243
|
|
|
216
244
|
test('opens a default selected attempt', () => {
|
|
@@ -58,6 +58,7 @@ export interface StepListProps {
|
|
|
58
58
|
selectedAttemptId?: string | null | undefined;
|
|
59
59
|
defaultSelectedAttemptId?: string | undefined;
|
|
60
60
|
onSelectedAttemptChange?: ((attemptId: string | undefined) => void) | undefined;
|
|
61
|
+
onExpandedAttemptIdsChange?: ((attemptIds: readonly string[]) => void) | undefined;
|
|
61
62
|
inspectorOpenAttemptId?: string | null | undefined;
|
|
62
63
|
onInspectorOpenChange?: ((attemptId: string | null) => void) | undefined;
|
|
63
64
|
autoSelectActiveAttempt?: boolean | undefined;
|
|
@@ -74,6 +75,7 @@ export function StepList({
|
|
|
74
75
|
selectedAttemptId,
|
|
75
76
|
defaultSelectedAttemptId,
|
|
76
77
|
onSelectedAttemptChange,
|
|
78
|
+
onExpandedAttemptIdsChange,
|
|
77
79
|
inspectorOpenAttemptId = null,
|
|
78
80
|
onInspectorOpenChange,
|
|
79
81
|
autoSelectActiveAttempt = false,
|
|
@@ -96,6 +98,7 @@ export function StepList({
|
|
|
96
98
|
selectedAttemptId={selectedAttemptId}
|
|
97
99
|
defaultSelectedAttemptId={defaultSelectedAttemptId}
|
|
98
100
|
onSelectedAttemptChange={onSelectedAttemptChange}
|
|
101
|
+
onExpandedAttemptIdsChange={onExpandedAttemptIdsChange}
|
|
99
102
|
inspectorOpenAttemptId={inspectorOpenAttemptId}
|
|
100
103
|
onInspectorOpenChange={onInspectorOpenChange}
|
|
101
104
|
autoSelectActiveAttempt={autoSelectActiveAttempt}
|
|
@@ -113,6 +116,7 @@ function StepListContent({
|
|
|
113
116
|
selectedAttemptId,
|
|
114
117
|
defaultSelectedAttemptId,
|
|
115
118
|
onSelectedAttemptChange,
|
|
119
|
+
onExpandedAttemptIdsChange,
|
|
116
120
|
inspectorOpenAttemptId = null,
|
|
117
121
|
onInspectorOpenChange,
|
|
118
122
|
autoSelectActiveAttempt,
|
|
@@ -134,20 +138,35 @@ function StepListContent({
|
|
|
134
138
|
const lastNotifiedSelectedAttemptId = useRef<string | null>(null);
|
|
135
139
|
const autoSelectedAttemptIdRef = useRef<string | undefined>(undefined);
|
|
136
140
|
const previousSelectedAttemptIdRef = useRef<string | null | undefined>(selectedAttemptId);
|
|
141
|
+
const lastReportedExpandedAttemptIdsKeyRef = useRef<string | null>(null);
|
|
137
142
|
const shouldUseControlledCollapsedState =
|
|
138
143
|
selectedAttemptId === null && lastNotifiedSelectedAttemptId.current === null;
|
|
139
144
|
const autoSelectedAttemptId =
|
|
140
145
|
selectedAttemptId === undefined && autoSelectActiveAttempt && !userSelectedAttempt
|
|
141
146
|
? (autoSelectedAttemptIdRef.current ?? model.activeEntryId)
|
|
142
147
|
: undefined;
|
|
143
|
-
const autoSelectedAttemptIds =
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
148
|
+
const autoSelectedAttemptIds = useMemo(
|
|
149
|
+
() => (autoSelectedAttemptId ? [autoSelectedAttemptId] : []),
|
|
150
|
+
[autoSelectedAttemptId],
|
|
151
|
+
);
|
|
152
|
+
const selectedAttemptIds = useMemo(
|
|
153
|
+
() =>
|
|
154
|
+
shouldUseControlledCollapsedState
|
|
155
|
+
? []
|
|
156
|
+
: localSelectedAttemptIds.length > 0
|
|
157
|
+
? localSelectedAttemptIds
|
|
158
|
+
: autoSelectedAttemptIds,
|
|
159
|
+
[autoSelectedAttemptIds, localSelectedAttemptIds, shouldUseControlledCollapsedState],
|
|
160
|
+
);
|
|
149
161
|
const hasExpandedContent = renderExpandedStep !== undefined;
|
|
150
162
|
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
const key = selectedAttemptIds.join('|');
|
|
165
|
+
if (lastReportedExpandedAttemptIdsKeyRef.current === key) return;
|
|
166
|
+
lastReportedExpandedAttemptIdsKeyRef.current = key;
|
|
167
|
+
onExpandedAttemptIdsChange?.(selectedAttemptIds);
|
|
168
|
+
}, [onExpandedAttemptIdsChange, selectedAttemptIds]);
|
|
169
|
+
|
|
151
170
|
useEffect(() => {
|
|
152
171
|
if (selectedAttemptId !== undefined) {
|
|
153
172
|
previousSelectedAttemptIdRef.current = selectedAttemptId;
|
|
@@ -223,7 +242,7 @@ function StepListContent({
|
|
|
223
242
|
<section
|
|
224
243
|
aria-labelledby={showHeader ? titleId : undefined}
|
|
225
244
|
className={cn(
|
|
226
|
-
'flex min-h-0 flex-col rounded-8 border border-border-neutral-base bg-background-
|
|
245
|
+
'flex min-h-0 flex-col rounded-8 border border-border-neutral-base bg-background-neutral-base',
|
|
227
246
|
className,
|
|
228
247
|
)}
|
|
229
248
|
>
|
|
@@ -307,11 +326,10 @@ function StepListEmptyStateView({
|
|
|
307
326
|
if (!emptyState.status) {
|
|
308
327
|
return (
|
|
309
328
|
<EmptyState
|
|
310
|
-
className="min-h-120 p-panel"
|
|
311
329
|
icon="componentLine"
|
|
312
330
|
title={emptyState.title}
|
|
313
331
|
description={emptyState.description}
|
|
314
|
-
variant="
|
|
332
|
+
variant="panel"
|
|
315
333
|
/>
|
|
316
334
|
);
|
|
317
335
|
}
|
|
@@ -425,8 +443,8 @@ function StepRow({
|
|
|
425
443
|
<>
|
|
426
444
|
<div
|
|
427
445
|
className={cn(
|
|
428
|
-
'group flex min-w-0 items-center gap-inline pr-[8px] transition-colors hover:bg-background-
|
|
429
|
-
selected && 'bg-background-
|
|
446
|
+
'group flex min-w-0 items-center gap-inline bg-background-neutral-base pr-[8px] transition-colors hover:bg-background-neutral-hover active:bg-background-neutral-pressed',
|
|
447
|
+
selected && 'bg-background-neutral-hover',
|
|
430
448
|
!hasExpandedContent && ['border-b border-border-neutral-base', isLast && 'border-b-0'],
|
|
431
449
|
)}
|
|
432
450
|
>
|
|
@@ -1,21 +1,17 @@
|
|
|
1
|
+
import {WORKFLOW_RUN_JOB_PREVIEW_LIMIT} from '@shipfox/api-workflows-dto';
|
|
1
2
|
import {Tooltip, TooltipContent, TooltipTrigger} from '@shipfox/react-ui/tooltip';
|
|
2
3
|
import {Code, Text} from '@shipfox/react-ui/typography';
|
|
3
4
|
import {cn} from '@shipfox/react-ui/utils';
|
|
5
|
+
import {memo} from 'react';
|
|
4
6
|
import {getWorkflowStatusVisual} from '#components/workflow-status/status-visuals.js';
|
|
5
7
|
import {WorkflowStatusIcon} from '#components/workflow-status/workflow-status-icon.js';
|
|
6
8
|
import {deriveJobDisplayStatus, type JobDisplayStatus} from '#core/entities/job.js';
|
|
7
9
|
import type {WorkflowRunJobSummary, WorkflowRunJobs} from '#core/workflow-run.js';
|
|
8
10
|
|
|
9
|
-
/**
|
|
10
|
-
* How many glyphs fit the strip's column before it starts costing the run name width.
|
|
11
|
-
*
|
|
12
|
-
* Sized so the widest case still fits the row's 160px strip column: seven 12px glyphs with
|
|
13
|
-
* 3px gaps is 102px, and the overflow indicator adds a glyph plus a count of at most five
|
|
14
|
-
* monospace characters, about 56px. Overshooting does not wrap, it paints over the duration
|
|
15
|
-
* column. This sits under the API's preview bound, so resizing the strip is a change here.
|
|
16
|
-
*/
|
|
17
|
-
const MAX_VISIBLE_JOBS = 7;
|
|
18
11
|
const GLYPH_SIZE = 12;
|
|
12
|
+
const GLYPH_GAP = 3;
|
|
13
|
+
const OVERFLOW_GAP = 2;
|
|
14
|
+
const OVERFLOW_LABEL_WIDTH = 40;
|
|
19
15
|
const MAX_TOOLTIP_JOB_NAMES = 6;
|
|
20
16
|
|
|
21
17
|
/**
|
|
@@ -32,6 +28,20 @@ const COMPACT_COUNT_FORMAT = new Intl.NumberFormat('en', {
|
|
|
32
28
|
maximumFractionDigits: 1,
|
|
33
29
|
});
|
|
34
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Reserves one full API preview and its overflow marker in a run row.
|
|
33
|
+
*
|
|
34
|
+
* Keep this derived from the API preview bound so the row reservation grows with the payload
|
|
35
|
+
* contract instead of silently drifting into the duration and timestamp columns.
|
|
36
|
+
*/
|
|
37
|
+
export function jobStatusStripWidthForPreview(previewLimit: number): number {
|
|
38
|
+
const previewWidth = previewLimit * GLYPH_SIZE + Math.max(previewLimit - 1, 0) * GLYPH_GAP;
|
|
39
|
+
const overflowWidth = GLYPH_GAP + GLYPH_SIZE + OVERFLOW_GAP + OVERFLOW_LABEL_WIDTH;
|
|
40
|
+
return previewWidth + overflowWidth;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const JOB_STATUS_STRIP_WIDTH = jobStatusStripWidthForPreview(WORKFLOW_RUN_JOB_PREVIEW_LIMIT);
|
|
44
|
+
|
|
35
45
|
// Worst-first, so the overflow glyph reports the most alarming thing it is standing in for.
|
|
36
46
|
// A strip that hides a failure behind eight green discs would be worse than no strip at all.
|
|
37
47
|
const STATUS_SEVERITY: Record<JobDisplayStatus, number> = {
|
|
@@ -62,10 +72,13 @@ export interface JobStatusStripProps {
|
|
|
62
72
|
* job including those the server never sent. That split is what lets the overflow glyph
|
|
63
73
|
* report a failure sitting past the preview instead of quietly dropping it.
|
|
64
74
|
*/
|
|
65
|
-
export function JobStatusStrip({jobs, className}: JobStatusStripProps) {
|
|
75
|
+
export const JobStatusStrip = memo(function JobStatusStrip({jobs, className}: JobStatusStripProps) {
|
|
66
76
|
if (jobs.total === 0) return null;
|
|
67
77
|
|
|
68
|
-
|
|
78
|
+
// The data frame gives the row enough room for the whole API preview. Only jobs beyond that
|
|
79
|
+
// server-side preview need an overflow marker; the old seven-glyph cap belonged to the
|
|
80
|
+
// previous 1120px page width.
|
|
81
|
+
const visible = jobs.preview;
|
|
69
82
|
const hiddenCount = jobs.total - visible.length;
|
|
70
83
|
const overflowStatus = worstHiddenStatus(jobs, visible);
|
|
71
84
|
const summary = jobStatusSummary(jobs);
|
|
@@ -115,7 +128,7 @@ export function JobStatusStrip({jobs, className}: JobStatusStripProps) {
|
|
|
115
128
|
</TooltipContent>
|
|
116
129
|
</Tooltip>
|
|
117
130
|
);
|
|
118
|
-
}
|
|
131
|
+
});
|
|
119
132
|
|
|
120
133
|
function JobStatusStripTooltip({jobs, summary}: {jobs: WorkflowRunJobs; summary: string}) {
|
|
121
134
|
// Naming the jobs that need attention is the whole point of hovering; succeeded jobs are
|
|
@@ -53,7 +53,7 @@ export function WorkflowRunFilters({
|
|
|
53
53
|
const [sheetOpen, setSheetOpen] = useState(false);
|
|
54
54
|
|
|
55
55
|
return (
|
|
56
|
-
<div className="flex flex-wrap items-center gap-inline">
|
|
56
|
+
<div className="flex w-full flex-wrap items-center gap-inline">
|
|
57
57
|
<Input
|
|
58
58
|
value={search.search ?? ''}
|
|
59
59
|
onChange={(event) => onChange({search: event.target.value})}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {QueryLoadError} from '@shipfox/client-ui';
|
|
2
|
+
import {Panel} from '@shipfox/react-ui/panel';
|
|
2
3
|
import type {WorkflowRunListItem} from '#core/workflow-run.js';
|
|
3
4
|
import type {WorkflowRunListQuery} from './types.js';
|
|
4
5
|
import {
|
|
@@ -55,11 +56,15 @@ export function WorkflowRunListContent({
|
|
|
55
56
|
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
56
57
|
{isPending ? <WorkflowRunListSkeleton /> : null}
|
|
57
58
|
{!isPending ? (
|
|
58
|
-
<
|
|
59
|
+
<Panel>
|
|
60
|
+
<QueryLoadError query={query} subject="workflow runs" icon="pulseLine" variant="panel" />
|
|
61
|
+
</Panel>
|
|
59
62
|
) : null}
|
|
60
63
|
{!isPending && refreshFailed ? <WorkflowRunListStaleError query={query} /> : null}
|
|
61
64
|
{showEmptyState ? (
|
|
62
|
-
<
|
|
65
|
+
<Panel>
|
|
66
|
+
<WorkflowRunListEmpty workspaceSlug={workspaceSlug} projectSlug={projectSlug} />
|
|
67
|
+
</Panel>
|
|
63
68
|
) : null}
|
|
64
69
|
{showNoMatches ? (
|
|
65
70
|
<WorkflowRunListNoMatches
|
|
@@ -2,6 +2,7 @@ import type {QueryLoadErrorQuery} from '@shipfox/client-ui';
|
|
|
2
2
|
import {Button} from '@shipfox/react-ui/button';
|
|
3
3
|
import {Callout} from '@shipfox/react-ui/callout';
|
|
4
4
|
import {EmptyState} from '@shipfox/react-ui/empty-state';
|
|
5
|
+
import {Panel} from '@shipfox/react-ui/panel';
|
|
5
6
|
import {Skeleton} from '@shipfox/react-ui/skeleton';
|
|
6
7
|
import {Text} from '@shipfox/react-ui/typography';
|
|
7
8
|
import {Link} from '@tanstack/react-router';
|
|
@@ -15,25 +16,21 @@ const SKELETON_ROW_COUNT = 8;
|
|
|
15
16
|
*/
|
|
16
17
|
export function WorkflowRunListSkeleton() {
|
|
17
18
|
return (
|
|
18
|
-
<
|
|
19
|
-
className="@container divide-y divide-border-neutral-base border-t border-border-neutral-base"
|
|
20
|
-
role="status"
|
|
21
|
-
aria-label="Loading runs"
|
|
22
|
-
>
|
|
19
|
+
<Panel role="status" aria-label="Loading runs" className="@container divide-y">
|
|
23
20
|
{Array.from({length: SKELETON_ROW_COUNT}).map((_, index) => (
|
|
24
21
|
<div
|
|
25
22
|
// biome-ignore lint/suspicious/noArrayIndexKey: skeleton row, stable position
|
|
26
23
|
key={index}
|
|
27
|
-
className="flex items-center gap-inline px-row py-row @min-[976px]:h-44 @min-[976px]:py-0"
|
|
24
|
+
className="flex min-h-44 items-center gap-inline px-row py-row @min-[976px]:h-44 @min-[976px]:py-0"
|
|
28
25
|
>
|
|
29
26
|
<Skeleton className="size-14 shrink-0 rounded-full" />
|
|
30
27
|
<Skeleton className="h-12 w-[220px] max-w-[40%]" />
|
|
31
|
-
<Skeleton className="ml-auto hidden h-12 w-96 @min-[
|
|
28
|
+
<Skeleton className="ml-auto hidden h-12 w-96 @min-[1200px]:block" />
|
|
32
29
|
<Skeleton className="h-12 w-48" />
|
|
33
30
|
<Skeleton className="h-12 w-48" />
|
|
34
31
|
</div>
|
|
35
32
|
))}
|
|
36
|
-
</
|
|
33
|
+
</Panel>
|
|
37
34
|
);
|
|
38
35
|
}
|
|
39
36
|
|
|
@@ -89,6 +86,7 @@ export function WorkflowRunListEmpty({
|
|
|
89
86
|
</Button>
|
|
90
87
|
) : null
|
|
91
88
|
}
|
|
89
|
+
variant="panel"
|
|
92
90
|
/>
|
|
93
91
|
);
|
|
94
92
|
}
|
|
@@ -116,40 +114,43 @@ export function WorkflowRunListNoMatches({
|
|
|
116
114
|
onLoadMore?: () => void;
|
|
117
115
|
}) {
|
|
118
116
|
return (
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
117
|
+
<Panel>
|
|
118
|
+
<div className="flex flex-col gap-inline">
|
|
119
|
+
{isFetchNextPageError ? (
|
|
120
|
+
<Callout role="alert" type="error">
|
|
121
|
+
Could not load more workflow runs. Try again to continue searching older runs.
|
|
122
|
+
</Callout>
|
|
123
|
+
) : null}
|
|
124
|
+
<EmptyState
|
|
125
|
+
icon="filterOffLine"
|
|
126
|
+
title={hasNextPage ? 'No matches in loaded history' : 'No matching runs'}
|
|
127
|
+
description={
|
|
128
|
+
hasNextPage
|
|
129
|
+
? 'No loaded run matches these filters. Load more to search further back.'
|
|
130
|
+
: 'No run matches these filters.'
|
|
131
|
+
}
|
|
132
|
+
action={
|
|
133
|
+
<div className="flex flex-wrap justify-center gap-inline">
|
|
134
|
+
{hasNextPage && onLoadMore ? (
|
|
135
|
+
<Button
|
|
136
|
+
type="button"
|
|
137
|
+
size="sm"
|
|
138
|
+
variant="primary"
|
|
139
|
+
isLoading={isFetchingNextPage}
|
|
140
|
+
onClick={onLoadMore}
|
|
141
|
+
>
|
|
142
|
+
Load more runs
|
|
143
|
+
</Button>
|
|
144
|
+
) : null}
|
|
145
|
+
<Button type="button" size="sm" variant="secondary" onClick={onClear}>
|
|
146
|
+
Show all runs
|
|
144
147
|
</Button>
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
/>
|
|
152
|
-
</div>
|
|
148
|
+
</div>
|
|
149
|
+
}
|
|
150
|
+
variant="panel"
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
</Panel>
|
|
153
154
|
);
|
|
154
155
|
}
|
|
155
156
|
|