@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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/client-workflows",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "22.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -29,14 +29,14 @@
|
|
|
29
29
|
"date-fns": "^4.1.0",
|
|
30
30
|
"@shipfox/annotations-dto": "12.3.0",
|
|
31
31
|
"@shipfox/api-definitions-dto": "12.3.0",
|
|
32
|
-
"@shipfox/api-workflows-dto": "
|
|
32
|
+
"@shipfox/api-workflows-dto": "13.0.0",
|
|
33
|
+
"@shipfox/client-logs": "22.0.0",
|
|
33
34
|
"@shipfox/client-api": "6.0.1",
|
|
34
|
-
"@shipfox/client-
|
|
35
|
-
"@shipfox/client-
|
|
36
|
-
"@shipfox/client-
|
|
37
|
-
"@shipfox/
|
|
38
|
-
"@shipfox/client-ui": "
|
|
39
|
-
"@shipfox/react-ui": "1.2.0"
|
|
35
|
+
"@shipfox/client-projects": "22.0.0",
|
|
36
|
+
"@shipfox/client-shell": "22.0.0",
|
|
37
|
+
"@shipfox/client-triggers": "22.0.0",
|
|
38
|
+
"@shipfox/react-ui": "2.1.0",
|
|
39
|
+
"@shipfox/client-ui": "22.0.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@tanstack/react-query": "^5.101.0",
|
|
@@ -3,13 +3,24 @@
|
|
|
3
3
|
|
|
4
4
|
import {ApiError} from '@shipfox/client-api';
|
|
5
5
|
import {QueryLoadError} from '@shipfox/client-ui';
|
|
6
|
+
import {Badge} from '@shipfox/react-ui/badge';
|
|
7
|
+
import {IconButton} from '@shipfox/react-ui/button';
|
|
8
|
+
import {
|
|
9
|
+
DropdownMenu,
|
|
10
|
+
DropdownMenuCheckboxItem,
|
|
11
|
+
DropdownMenuContent,
|
|
12
|
+
DropdownMenuLabel,
|
|
13
|
+
DropdownMenuTrigger,
|
|
14
|
+
} from '@shipfox/react-ui/dropdown-menu';
|
|
6
15
|
import {EmptyState} from '@shipfox/react-ui/empty-state';
|
|
7
16
|
import {Icon} from '@shipfox/react-ui/icon';
|
|
17
|
+
import {Panel, PanelActions, PanelBody, PanelHeader} from '@shipfox/react-ui/panel';
|
|
18
|
+
import {SearchInline} from '@shipfox/react-ui/search';
|
|
8
19
|
import {Skeleton} from '@shipfox/react-ui/skeleton';
|
|
9
20
|
import {TimeTickerProvider} from '@shipfox/react-ui/time-ticker';
|
|
10
21
|
import {Text} from '@shipfox/react-ui/typography';
|
|
11
22
|
import {Link} from '@tanstack/react-router';
|
|
12
|
-
import {type RefObject, useEffect, useRef, useState} from 'react';
|
|
23
|
+
import {type RefObject, useCallback, useEffect, useRef, useState} from 'react';
|
|
13
24
|
import type {RunAnnotationSummary} from '#core/run-annotation.js';
|
|
14
25
|
import {summarizeJobAnnotations} from '#core/run-annotation.js';
|
|
15
26
|
import {isWorkflowRunTerminal, type Job, type JobExecution} from '#core/workflow-run.js';
|
|
@@ -22,6 +33,11 @@ import {
|
|
|
22
33
|
workflowRunSearchParams,
|
|
23
34
|
} from '#routes/inputs.js';
|
|
24
35
|
import {type StepExpandedContext, StepList} from '../step-list/index.js';
|
|
36
|
+
import {
|
|
37
|
+
buildStepListModel,
|
|
38
|
+
getStepStatusVisual,
|
|
39
|
+
type StepListModel,
|
|
40
|
+
} from '../step-list/step-list-model.js';
|
|
25
41
|
import {
|
|
26
42
|
WorkflowRunNotFound,
|
|
27
43
|
WorkflowRunStaleError,
|
|
@@ -71,6 +87,12 @@ export function JobDetailView({
|
|
|
71
87
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
72
88
|
const pageScrollRef = useRef<HTMLDivElement>(null);
|
|
73
89
|
const landingSelectionRef = useRef<FrozenLandingSelection | undefined>(undefined);
|
|
90
|
+
const [logSearch, setLogSearch] = useState('');
|
|
91
|
+
const [wrapLogs, setWrapLogs] = useState(false);
|
|
92
|
+
const [showLineNumbers, setShowLineNumbers] = useState(true);
|
|
93
|
+
const [expandedLogAttemptIds, setExpandedLogAttemptIds] = useState<readonly string[]>([]);
|
|
94
|
+
const [logRefreshTokens, setLogRefreshTokens] = useState<Record<string, number>>({});
|
|
95
|
+
const [logFetchingByAttemptId, setLogFetchingByAttemptId] = useState<Record<string, boolean>>({});
|
|
74
96
|
const hasLoadedData = query.data !== undefined;
|
|
75
97
|
// Reuse the run workspace's bounded annotation read for the job header chip. The separate
|
|
76
98
|
// summary query below stays counts-only and is scoped to the inspector's selected execution.
|
|
@@ -112,6 +134,13 @@ export function JobDetailView({
|
|
|
112
134
|
heading?.focus({preventScroll: true});
|
|
113
135
|
}, [hasLoadedData, jobId]);
|
|
114
136
|
|
|
137
|
+
const handleLogFetchingChange = useCallback((attemptId: string, isFetching: boolean) => {
|
|
138
|
+
setLogFetchingByAttemptId((current) => {
|
|
139
|
+
if (current[attemptId] === isFetching) return current;
|
|
140
|
+
return {...current, [attemptId]: isFetching};
|
|
141
|
+
});
|
|
142
|
+
}, []);
|
|
143
|
+
|
|
115
144
|
if (query.isPending) return <JobDetailSkeleton />;
|
|
116
145
|
|
|
117
146
|
if (query.isError && query.data === undefined) {
|
|
@@ -139,6 +168,7 @@ export function JobDetailView({
|
|
|
139
168
|
const resolvedSelection = resolveWorkflowJobSelection({job, selection: search});
|
|
140
169
|
const selectedJobExecution = resolvedSelection.jobExecution;
|
|
141
170
|
const hasExplicitStep = Boolean(search.stepId && resolvedSelection.step);
|
|
171
|
+
const stepListModel = buildStepListModel({job, jobExecution: selectedJobExecution});
|
|
142
172
|
const currentLandingSelection = workflowJobLandingSelection(selectedJobExecution);
|
|
143
173
|
if (selectedJobExecution) {
|
|
144
174
|
const frozenLanding = landingSelectionRef.current;
|
|
@@ -165,11 +195,22 @@ export function JobDetailView({
|
|
|
165
195
|
}
|
|
166
196
|
const landingSelection = landingSelectionRef.current?.selection;
|
|
167
197
|
const selectedAttemptId = hasExplicitStep ? resolvedSelection.selectedAttemptId : undefined;
|
|
168
|
-
const runningSelection = runningStepSelection(selectedJobExecution);
|
|
198
|
+
const runningSelection = runningStepSelection(selectedJobExecution, stepListModel);
|
|
169
199
|
const selectedStepId = resolvedSelection.step?.id ?? landingSelection?.stepId;
|
|
170
200
|
const selectedAttemptForNotice = hasExplicitStep
|
|
171
201
|
? resolvedSelection.selectedAttemptId
|
|
172
202
|
: landingSelection?.attemptId;
|
|
203
|
+
const expandedLogSelection = findExpandedLogSelection(
|
|
204
|
+
selectedJobExecution,
|
|
205
|
+
expandedLogAttemptIds,
|
|
206
|
+
stepListModel,
|
|
207
|
+
);
|
|
208
|
+
const selectedLogStep = expandedLogSelection?.step;
|
|
209
|
+
const selectedLogAttempt = expandedLogSelection?.attempt;
|
|
210
|
+
const selectedLogStatus = selectedLogAttempt?.status ?? selectedLogStep?.status;
|
|
211
|
+
const logIsFetching = Boolean(
|
|
212
|
+
selectedLogAttempt && logFetchingByAttemptId[selectedLogAttempt.id],
|
|
213
|
+
);
|
|
173
214
|
const showRetargetNotice =
|
|
174
215
|
runningSelection !== undefined &&
|
|
175
216
|
(selectedStepId !== runningSelection.stepId ||
|
|
@@ -224,6 +265,14 @@ export function JobDetailView({
|
|
|
224
265
|
});
|
|
225
266
|
}
|
|
226
267
|
|
|
268
|
+
function refreshLogs() {
|
|
269
|
+
if (!selectedLogAttempt) return;
|
|
270
|
+
setLogRefreshTokens((current) => ({
|
|
271
|
+
...current,
|
|
272
|
+
[selectedLogAttempt.id]: (current[selectedLogAttempt.id] ?? 0) + 1,
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
|
|
227
276
|
return (
|
|
228
277
|
<TimeTickerProvider intervalMs={1000} reducedMotionIntervalMs={10_000}>
|
|
229
278
|
<div ref={rootRef} className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
|
|
@@ -255,49 +304,77 @@ export function JobDetailView({
|
|
|
255
304
|
) : undefined
|
|
256
305
|
}
|
|
257
306
|
/>
|
|
258
|
-
<
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
<
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
307
|
+
<Panel data-job-log-panel className="min-w-0">
|
|
308
|
+
<JobLogPanelHeader
|
|
309
|
+
stepLabel={expandedLogSelection?.stepLabel}
|
|
310
|
+
attempt={selectedLogAttempt?.attempt}
|
|
311
|
+
status={selectedLogStatus}
|
|
312
|
+
search={logSearch}
|
|
313
|
+
onSearchChange={setLogSearch}
|
|
314
|
+
onRefresh={refreshLogs}
|
|
315
|
+
refreshing={logIsFetching}
|
|
316
|
+
showLineNumbers={showLineNumbers}
|
|
317
|
+
onShowLineNumbersChange={setShowLineNumbers}
|
|
318
|
+
wrap={wrapLogs}
|
|
319
|
+
onWrapChange={setWrapLogs}
|
|
320
|
+
disabled={!selectedLogAttempt}
|
|
321
|
+
/>
|
|
322
|
+
<PanelBody className="min-w-0 p-0">
|
|
323
|
+
<Text as="h2" className="sr-only">
|
|
324
|
+
Logs
|
|
325
|
+
</Text>
|
|
326
|
+
{selectedJobExecution ? (
|
|
327
|
+
<>
|
|
328
|
+
<MaterializedOutputFailureNotice jobExecution={selectedJobExecution} />
|
|
329
|
+
<StepList
|
|
330
|
+
job={job}
|
|
331
|
+
jobExecution={selectedJobExecution}
|
|
332
|
+
selectedAttemptId={selectedAttemptId}
|
|
333
|
+
defaultSelectedAttemptId={landingSelection?.attemptId}
|
|
334
|
+
onSelectedAttemptChange={selectAttempt}
|
|
335
|
+
onExpandedAttemptIdsChange={setExpandedLogAttemptIds}
|
|
336
|
+
inspectorOpenAttemptId={inspectorOpenAttemptId}
|
|
337
|
+
onInspectorOpenChange={onInspectorOpenChange}
|
|
338
|
+
autoSelectActiveAttempt
|
|
339
|
+
emptyState={emptyStateForJob(job, selectedJobExecution)}
|
|
340
|
+
showHeader={false}
|
|
341
|
+
className="rounded-none border-0 bg-transparent shadow-none"
|
|
342
|
+
renderExpandedStep={(context) => (
|
|
343
|
+
<ExpandedStep
|
|
344
|
+
context={context}
|
|
345
|
+
pageScrollRef={pageScrollRef}
|
|
346
|
+
search={logSearch}
|
|
347
|
+
wrap={wrapLogs}
|
|
348
|
+
showLineNumbers={showLineNumbers}
|
|
349
|
+
attemptId={context.attemptId}
|
|
350
|
+
refreshToken={logRefreshTokens[context.attemptId] ?? 0}
|
|
351
|
+
onFetchingChange={handleLogFetchingChange}
|
|
352
|
+
/>
|
|
353
|
+
)}
|
|
354
|
+
renderInspector={(entry) => (
|
|
355
|
+
<StepInspectorSheet
|
|
356
|
+
entry={entry}
|
|
357
|
+
open
|
|
358
|
+
onOpenChange={(open) => onInspectorOpenChange(open ? entry.id : null)}
|
|
359
|
+
workspaceSlug={workspaceSlug}
|
|
360
|
+
projectSlug={projectSlug}
|
|
361
|
+
workflowRunId={run.id}
|
|
362
|
+
runAttempt={run.runAttempt.attempt}
|
|
363
|
+
jobId={job.id}
|
|
364
|
+
annotationCount={annotationCountForStep(
|
|
365
|
+
annotationSummaryQuery.data,
|
|
366
|
+
entry.step.id,
|
|
367
|
+
entry.attempt,
|
|
368
|
+
)}
|
|
369
|
+
/>
|
|
293
370
|
)}
|
|
294
371
|
/>
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
372
|
+
</>
|
|
373
|
+
) : (
|
|
374
|
+
<EmptyStateForMissingExecution job={job} />
|
|
375
|
+
)}
|
|
376
|
+
</PanelBody>
|
|
377
|
+
</Panel>
|
|
301
378
|
</section>
|
|
302
379
|
{showRetargetNotice && runningSelection ? (
|
|
303
380
|
<div
|
|
@@ -336,9 +413,21 @@ export function JobDetailView({
|
|
|
336
413
|
function ExpandedStep({
|
|
337
414
|
context,
|
|
338
415
|
pageScrollRef,
|
|
416
|
+
search,
|
|
417
|
+
wrap,
|
|
418
|
+
showLineNumbers,
|
|
419
|
+
attemptId,
|
|
420
|
+
refreshToken,
|
|
421
|
+
onFetchingChange,
|
|
339
422
|
}: {
|
|
340
423
|
context: StepExpandedContext;
|
|
341
424
|
pageScrollRef: RefObject<HTMLDivElement | null>;
|
|
425
|
+
search: string;
|
|
426
|
+
wrap: boolean;
|
|
427
|
+
showLineNumbers: boolean;
|
|
428
|
+
attemptId: string;
|
|
429
|
+
refreshToken: number;
|
|
430
|
+
onFetchingChange: (attemptId: string, isFetching: boolean) => void;
|
|
342
431
|
}) {
|
|
343
432
|
if (context.carriedOver) return <CarriedOverStepPanel />;
|
|
344
433
|
|
|
@@ -347,7 +436,7 @@ function ExpandedStep({
|
|
|
347
436
|
role="region"
|
|
348
437
|
tabIndex={0}
|
|
349
438
|
aria-label={`${context.stepLabel} output, attempt ${context.attempt}`}
|
|
350
|
-
className="flex min-w-0 flex-col border-t border-border-neutral-base bg-background-
|
|
439
|
+
className="flex min-w-0 flex-col border-t border-border-neutral-base bg-background-contrast-base outline-none focus-visible:shadow-border-interactive-with-active"
|
|
351
440
|
>
|
|
352
441
|
<StepAttemptLogPanel
|
|
353
442
|
stepId={context.stepId}
|
|
@@ -355,21 +444,124 @@ function ExpandedStep({
|
|
|
355
444
|
attemptStatus={context.attemptStatus}
|
|
356
445
|
attemptStartedAt={context.attemptStartedAt}
|
|
357
446
|
pageScrollRef={pageScrollRef}
|
|
358
|
-
|
|
447
|
+
search={search}
|
|
448
|
+
wrap={wrap}
|
|
449
|
+
showLineNumbers={showLineNumbers}
|
|
450
|
+
attemptId={attemptId}
|
|
451
|
+
refreshToken={refreshToken}
|
|
452
|
+
onFetchingChange={onFetchingChange}
|
|
359
453
|
/>
|
|
360
454
|
</section>
|
|
361
455
|
);
|
|
362
456
|
}
|
|
363
457
|
|
|
458
|
+
function JobLogPanelHeader({
|
|
459
|
+
stepLabel,
|
|
460
|
+
attempt,
|
|
461
|
+
status,
|
|
462
|
+
search,
|
|
463
|
+
onSearchChange,
|
|
464
|
+
onRefresh,
|
|
465
|
+
refreshing,
|
|
466
|
+
showLineNumbers,
|
|
467
|
+
onShowLineNumbersChange,
|
|
468
|
+
wrap,
|
|
469
|
+
onWrapChange,
|
|
470
|
+
disabled,
|
|
471
|
+
}: {
|
|
472
|
+
stepLabel: string | undefined;
|
|
473
|
+
attempt: number | undefined;
|
|
474
|
+
status: string | undefined;
|
|
475
|
+
search: string;
|
|
476
|
+
onSearchChange: (value: string) => void;
|
|
477
|
+
onRefresh: () => void;
|
|
478
|
+
refreshing: boolean;
|
|
479
|
+
showLineNumbers: boolean;
|
|
480
|
+
onShowLineNumbersChange: (value: boolean) => void;
|
|
481
|
+
wrap: boolean;
|
|
482
|
+
onWrapChange: (value: boolean) => void;
|
|
483
|
+
disabled: boolean;
|
|
484
|
+
}) {
|
|
485
|
+
const statusVisual = status ? getStepStatusVisual(status) : undefined;
|
|
486
|
+
|
|
487
|
+
return (
|
|
488
|
+
<PanelHeader className="flex flex-wrap items-center gap-group">
|
|
489
|
+
<div className="min-w-0 flex-1">
|
|
490
|
+
<Text size="sm" bold className="truncate text-foreground-neutral-base">
|
|
491
|
+
{stepLabel ?? 'Logs'}
|
|
492
|
+
</Text>
|
|
493
|
+
{statusVisual ? (
|
|
494
|
+
<div className="flex min-w-0 items-center gap-inline text-foreground-neutral-muted">
|
|
495
|
+
<Badge variant={statusVisual.badge} size="2xs" radius="rounded">
|
|
496
|
+
{statusVisual.label}
|
|
497
|
+
</Badge>
|
|
498
|
+
{attempt ? (
|
|
499
|
+
<span className="font-code text-xs leading-20 tabular-nums">attempt {attempt}</span>
|
|
500
|
+
) : null}
|
|
501
|
+
</div>
|
|
502
|
+
) : (
|
|
503
|
+
<Text size="xs" className="text-foreground-neutral-muted">
|
|
504
|
+
Select a step to view its logs.
|
|
505
|
+
</Text>
|
|
506
|
+
)}
|
|
507
|
+
</div>
|
|
508
|
+
<PanelActions className="min-w-0 flex-wrap justify-end">
|
|
509
|
+
<SearchInline
|
|
510
|
+
value={search}
|
|
511
|
+
onChange={(event) => onSearchChange(event.target.value)}
|
|
512
|
+
aria-label="Search logs"
|
|
513
|
+
placeholder="Search logs"
|
|
514
|
+
size="small"
|
|
515
|
+
className="w-180 max-w-full"
|
|
516
|
+
disabled={disabled}
|
|
517
|
+
/>
|
|
518
|
+
<IconButton
|
|
519
|
+
type="button"
|
|
520
|
+
variant="transparent"
|
|
521
|
+
size="sm"
|
|
522
|
+
icon="refreshLine"
|
|
523
|
+
aria-label="Refresh logs"
|
|
524
|
+
onClick={onRefresh}
|
|
525
|
+
disabled={disabled || refreshing}
|
|
526
|
+
isLoading={refreshing}
|
|
527
|
+
/>
|
|
528
|
+
<DropdownMenu>
|
|
529
|
+
<DropdownMenuTrigger asChild>
|
|
530
|
+
<IconButton
|
|
531
|
+
type="button"
|
|
532
|
+
variant="transparent"
|
|
533
|
+
size="sm"
|
|
534
|
+
icon="settings3Line"
|
|
535
|
+
aria-label="Log settings"
|
|
536
|
+
disabled={disabled}
|
|
537
|
+
/>
|
|
538
|
+
</DropdownMenuTrigger>
|
|
539
|
+
<DropdownMenuContent align="end" size="sm">
|
|
540
|
+
<DropdownMenuLabel>Log display</DropdownMenuLabel>
|
|
541
|
+
<DropdownMenuCheckboxItem
|
|
542
|
+
checked={showLineNumbers}
|
|
543
|
+
onCheckedChange={onShowLineNumbersChange}
|
|
544
|
+
>
|
|
545
|
+
Line numbers
|
|
546
|
+
</DropdownMenuCheckboxItem>
|
|
547
|
+
<DropdownMenuCheckboxItem checked={wrap} onCheckedChange={onWrapChange}>
|
|
548
|
+
Wrap long lines
|
|
549
|
+
</DropdownMenuCheckboxItem>
|
|
550
|
+
</DropdownMenuContent>
|
|
551
|
+
</DropdownMenu>
|
|
552
|
+
</PanelActions>
|
|
553
|
+
</PanelHeader>
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
364
557
|
function EmptyStateForMissingExecution({job}: {job: Job}) {
|
|
365
558
|
const emptyState = emptyStateForMissingExecution(job);
|
|
366
559
|
return (
|
|
367
560
|
<EmptyState
|
|
368
|
-
className="min-h-120 p-panel"
|
|
369
561
|
icon="componentLine"
|
|
370
562
|
title={emptyState.title}
|
|
371
563
|
description={emptyState.description}
|
|
372
|
-
variant="
|
|
564
|
+
variant="panel"
|
|
373
565
|
/>
|
|
374
566
|
);
|
|
375
567
|
}
|
|
@@ -442,7 +634,7 @@ function NewerAttemptNotice({
|
|
|
442
634
|
);
|
|
443
635
|
}
|
|
444
636
|
|
|
445
|
-
function runningStepSelection(jobExecution: JobExecution | undefined) {
|
|
637
|
+
function runningStepSelection(jobExecution: JobExecution | undefined, model: StepListModel) {
|
|
446
638
|
if (!jobExecution) return undefined;
|
|
447
639
|
const steps = [...jobExecution.steps].sort(
|
|
448
640
|
(left, right) => left.position - right.position || left.id.localeCompare(right.id),
|
|
@@ -455,7 +647,11 @@ function runningStepSelection(jobExecution: JobExecution | undefined) {
|
|
|
455
647
|
.reverse()
|
|
456
648
|
.find((candidate) => candidate.status === 'running');
|
|
457
649
|
if (attempt)
|
|
458
|
-
return {
|
|
650
|
+
return {
|
|
651
|
+
stepId: step.id,
|
|
652
|
+
attemptId: attempt.id,
|
|
653
|
+
stepLabel: model.entries.find((entry) => entry.id === attempt.id)?.step.label ?? 'Step',
|
|
654
|
+
};
|
|
459
655
|
}
|
|
460
656
|
return undefined;
|
|
461
657
|
}
|
|
@@ -468,6 +664,25 @@ function findAttempt(jobExecution: JobExecution, attemptId: string) {
|
|
|
468
664
|
return undefined;
|
|
469
665
|
}
|
|
470
666
|
|
|
667
|
+
function findExpandedLogSelection(
|
|
668
|
+
jobExecution: JobExecution | undefined,
|
|
669
|
+
attemptIds: readonly string[],
|
|
670
|
+
model: StepListModel,
|
|
671
|
+
) {
|
|
672
|
+
if (!jobExecution) return undefined;
|
|
673
|
+
|
|
674
|
+
for (let index = attemptIds.length - 1; index >= 0; index -= 1) {
|
|
675
|
+
const attemptId = attemptIds[index];
|
|
676
|
+
if (!attemptId) continue;
|
|
677
|
+
const match = findAttempt(jobExecution, attemptId);
|
|
678
|
+
const attempt = match?.step.attempts.find((candidate) => candidate.id === attemptId);
|
|
679
|
+
const entry = model.entries.find((candidate) => candidate.id === attemptId);
|
|
680
|
+
if (match && attempt && entry) return {step: match.step, attempt, stepLabel: entry.step.label};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return undefined;
|
|
684
|
+
}
|
|
685
|
+
|
|
471
686
|
function annotationCountForStep(
|
|
472
687
|
summary: RunAnnotationSummary | undefined,
|
|
473
688
|
stepId: string,
|
|
@@ -256,11 +256,11 @@ function humanizeFailureReason(reason: string): string {
|
|
|
256
256
|
export function CarriedOverStepPanel() {
|
|
257
257
|
return (
|
|
258
258
|
<EmptyState
|
|
259
|
-
className="
|
|
259
|
+
className="rounded-8 border border-border-neutral-base bg-background-components-base"
|
|
260
260
|
icon="componentLine"
|
|
261
261
|
title="Carried over from a previous attempt"
|
|
262
262
|
description="Not executed in this run."
|
|
263
|
-
variant="
|
|
263
|
+
variant="panel"
|
|
264
264
|
/>
|
|
265
265
|
);
|
|
266
266
|
}
|
|
@@ -2,7 +2,7 @@ import {configureApiClient} from '@shipfox/client-api';
|
|
|
2
2
|
import {type StepLogSnapshot, stepLogsQueryKeys} from '@shipfox/client-logs';
|
|
3
3
|
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
|
|
4
4
|
import {act, cleanup, fireEvent, render, screen, waitFor} from '@testing-library/react';
|
|
5
|
-
import {createRef
|
|
5
|
+
import {createRef} from 'react';
|
|
6
6
|
import {inlineLogBody, outputLine} from '#test/fixtures/logs.js';
|
|
7
7
|
import {StepAttemptLogPanel} from './step-attempt-log-panel.js';
|
|
8
8
|
|
|
@@ -49,25 +49,28 @@ function renderPanel(
|
|
|
49
49
|
new QueryClient({
|
|
50
50
|
defaultOptions: {queries: {retry: false}},
|
|
51
51
|
});
|
|
52
|
-
const wrapper = ({children}: {children: ReactNode}) => (
|
|
53
|
-
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
54
|
-
);
|
|
55
52
|
const pageScrollRef = createRef<HTMLDivElement>();
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
queryClient,
|
|
59
|
-
...render(
|
|
53
|
+
const renderPanelUi = (panelProps: Partial<Parameters<typeof StepAttemptLogPanel>[0]>) => (
|
|
54
|
+
<QueryClientProvider client={queryClient}>
|
|
60
55
|
<div ref={pageScrollRef}>
|
|
61
56
|
<StepAttemptLogPanel
|
|
62
57
|
stepId={STEP_ID}
|
|
63
58
|
attempt={1}
|
|
64
59
|
attemptStatus="running"
|
|
65
60
|
pageScrollRef={pageScrollRef}
|
|
66
|
-
{...
|
|
61
|
+
{...panelProps}
|
|
67
62
|
/>
|
|
68
|
-
</div
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
</div>
|
|
64
|
+
</QueryClientProvider>
|
|
65
|
+
);
|
|
66
|
+
const rendered = render(renderPanelUi(props));
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
queryClient,
|
|
70
|
+
...rendered,
|
|
71
|
+
rerenderPanel: (nextProps: Partial<Parameters<typeof StepAttemptLogPanel>[0]> = {}) => {
|
|
72
|
+
rendered.rerender(renderPanelUi({...props, ...nextProps}));
|
|
73
|
+
},
|
|
71
74
|
};
|
|
72
75
|
}
|
|
73
76
|
|
|
@@ -240,6 +243,21 @@ describe('StepAttemptLogPanel', () => {
|
|
|
240
243
|
expect(screen.getByRole('button', {name: 'Retry'})).toBeInTheDocument();
|
|
241
244
|
});
|
|
242
245
|
|
|
246
|
+
test('refetches the expanded attempt when its refresh token changes', async () => {
|
|
247
|
+
const fetchImpl = vi
|
|
248
|
+
.fn()
|
|
249
|
+
.mockResolvedValueOnce(jsonResponse(inlineLogBody(outputLine('first\n'), 1)))
|
|
250
|
+
.mockResolvedValueOnce(jsonResponse(inlineLogBody(outputLine('second\n'), 2)));
|
|
251
|
+
configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
|
|
252
|
+
const {rerenderPanel} = renderPanel({attemptStatus: 'succeeded', refreshToken: 0});
|
|
253
|
+
|
|
254
|
+
expect(await screen.findByText('first')).toBeInTheDocument();
|
|
255
|
+
rerenderPanel({refreshToken: 1});
|
|
256
|
+
|
|
257
|
+
expect(await screen.findByText('second')).toBeInTheDocument();
|
|
258
|
+
expect(fetchImpl).toHaveBeenCalledTimes(2);
|
|
259
|
+
});
|
|
260
|
+
|
|
243
261
|
test('does not follow new log output after the user scrolls away from the tail', async () => {
|
|
244
262
|
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => {
|
|
245
263
|
callback(0);
|