@shipfox/client-workflows 13.0.0 → 14.0.1
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/.storybook/main.ts +16 -1
- package/.storybook/workspace-source.ts +31 -0
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +37 -0
- package/dist/components/job-graph/job-duration-label.js +1 -1
- package/dist/components/job-graph/job-duration-label.js.map +1 -1
- package/dist/components/job-graph/job-graph-content.d.ts +2 -1
- package/dist/components/job-graph/job-graph-content.d.ts.map +1 -1
- package/dist/components/job-graph/job-graph-content.js +3 -3
- package/dist/components/job-graph/job-graph-content.js.map +1 -1
- package/dist/components/job-graph/job-graph-view.d.ts +2 -1
- package/dist/components/job-graph/job-graph-view.d.ts.map +1 -1
- package/dist/components/job-graph/job-graph-view.js +2 -2
- package/dist/components/job-graph/job-graph-view.js.map +1 -1
- package/dist/components/job-graph/job-node.js +5 -5
- package/dist/components/job-graph/job-node.js.map +1 -1
- package/dist/components/job-graph/types.d.ts +2 -1
- package/dist/components/job-graph/types.d.ts.map +1 -1
- package/dist/components/job-graph/types.js.map +1 -1
- package/dist/components/workflow-run-duration-label.js +1 -1
- package/dist/components/workflow-run-duration-label.js.map +1 -1
- package/dist/components/workflow-run-number-label.js +1 -1
- package/dist/components/workflow-run-number-label.js.map +1 -1
- package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js +2 -2
- package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js.map +1 -1
- package/dist/components/workflow-run-summary/workflow-run-summary.d.ts +2 -7
- package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
- package/dist/components/workflow-run-summary/workflow-run-summary.js +92 -68
- package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
- package/dist/components/workflow-run-tabs/index.d.ts +6 -0
- package/dist/components/workflow-run-tabs/index.d.ts.map +1 -0
- package/dist/components/workflow-run-tabs/index.js +7 -0
- package/dist/components/workflow-run-tabs/index.js.map +1 -0
- package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts +11 -0
- package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts.map +1 -0
- package/dist/components/workflow-run-tabs/run-annotation-summary-line.js +71 -0
- package/dist/components/workflow-run-tabs/run-annotation-summary-line.js.map +1 -0
- package/dist/components/workflow-run-tabs/run-annotations-empty.d.ts +2 -0
- package/dist/components/workflow-run-tabs/run-annotations-empty.d.ts.map +1 -0
- package/dist/components/workflow-run-tabs/run-annotations-empty.js +11 -0
- package/dist/components/workflow-run-tabs/run-annotations-empty.js.map +1 -0
- package/dist/components/workflow-run-tabs/run-jobs-list.d.ts +18 -0
- package/dist/components/workflow-run-tabs/run-jobs-list.d.ts.map +1 -0
- package/dist/components/workflow-run-tabs/run-jobs-list.js +107 -0
- package/dist/components/workflow-run-tabs/run-jobs-list.js.map +1 -0
- package/dist/components/workflow-run-tabs/run-tab-count.d.ts +6 -0
- package/dist/components/workflow-run-tabs/run-tab-count.d.ts.map +1 -0
- package/dist/components/workflow-run-tabs/run-tab-count.js +27 -0
- package/dist/components/workflow-run-tabs/run-tab-count.js.map +1 -0
- package/dist/components/workflow-run-tabs/run-tab-strip.d.ts +8 -0
- package/dist/components/workflow-run-tabs/run-tab-strip.d.ts.map +1 -0
- package/dist/components/workflow-run-tabs/run-tab-strip.js +82 -0
- package/dist/components/workflow-run-tabs/run-tab-strip.js.map +1 -0
- package/dist/components/workflow-run-view/workflow-run-selection.d.ts.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-selection.js +40 -1
- package/dist/components/workflow-run-view/workflow-run-selection.js.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-states.d.ts +1 -0
- package/dist/components/workflow-run-view/workflow-run-states.d.ts.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-states.js +10 -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 +8 -4
- package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-view.js +125 -78
- package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
- package/dist/components/workflow-source-panel/index.d.ts +1 -0
- package/dist/components/workflow-source-panel/index.d.ts.map +1 -1
- package/dist/components/workflow-source-panel/index.js +1 -0
- package/dist/components/workflow-source-panel/index.js.map +1 -1
- package/dist/components/workflow-source-panel/workflow-source-content.d.ts +12 -0
- package/dist/components/workflow-source-panel/workflow-source-content.d.ts.map +1 -0
- package/dist/components/workflow-source-panel/workflow-source-content.js +53 -0
- package/dist/components/workflow-source-panel/workflow-source-content.js.map +1 -0
- package/dist/components/workflow-source-panel/workflow-source-panel.d.ts +1 -1
- package/dist/components/workflow-source-panel/workflow-source-panel.d.ts.map +1 -1
- package/dist/components/workflow-source-panel/workflow-source-panel.js +7 -54
- package/dist/components/workflow-source-panel/workflow-source-panel.js.map +1 -1
- package/dist/core/entities/step.d.ts +1 -1
- package/dist/core/entities/step.d.ts.map +1 -1
- package/dist/core/entities/step.js +2 -0
- package/dist/core/entities/step.js.map +1 -1
- package/dist/core/workflow-run-tabs.d.ts +9 -0
- package/dist/core/workflow-run-tabs.d.ts.map +1 -0
- package/dist/core/workflow-run-tabs.js +3 -0
- package/dist/core/workflow-run-tabs.js.map +1 -0
- package/dist/pages/workflow-run-detail-page.d.ts.map +1 -1
- package/dist/pages/workflow-run-detail-page.js +28 -6
- package/dist/pages/workflow-run-detail-page.js.map +1 -1
- package/dist/routes/inputs.d.ts +33 -0
- package/dist/routes/inputs.d.ts.map +1 -1
- package/dist/routes/inputs.js +49 -0
- package/dist/routes/inputs.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/src/components/job-graph/job-duration-label.tsx +5 -1
- package/src/components/job-graph/job-graph-content.tsx +7 -4
- package/src/components/job-graph/job-graph-view.tsx +6 -3
- 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/job-graph/job-node.test.tsx +27 -2
- package/src/components/job-graph/job-node.tsx +6 -6
- package/src/components/job-graph/types.ts +5 -1
- package/src/components/workflow-run-duration-label.tsx +1 -1
- package/src/components/workflow-run-number-label.tsx +1 -1
- package/src/components/workflow-run-summary/workflow-run-attempt-switcher.tsx +2 -2
- package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +23 -30
- package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +3 -22
- package/src/components/workflow-run-summary/workflow-run-summary.tsx +84 -69
- package/src/components/workflow-run-tabs/index.ts +5 -0
- package/src/components/workflow-run-tabs/run-annotation-summary-line.test.tsx +69 -0
- package/src/components/workflow-run-tabs/run-annotation-summary-line.tsx +97 -0
- package/src/components/workflow-run-tabs/run-annotations-empty.tsx +11 -0
- package/src/components/workflow-run-tabs/run-jobs-list.tsx +151 -0
- package/src/components/workflow-run-tabs/run-tab-count.tsx +28 -0
- package/src/components/workflow-run-tabs/run-tab-strip.stories.tsx +52 -0
- package/src/components/workflow-run-tabs/run-tab-strip.test.tsx +117 -0
- package/src/components/workflow-run-tabs/run-tab-strip.tsx +97 -0
- package/src/components/workflow-run-view/workflow-run-selection.test.ts +48 -0
- package/src/components/workflow-run-view/workflow-run-selection.ts +48 -1
- package/src/components/workflow-run-view/workflow-run-states.tsx +12 -1
- package/src/components/workflow-run-view/workflow-run-view.test.tsx +64 -64
- package/src/components/workflow-run-view/workflow-run-view.tsx +199 -85
- package/src/components/workflow-source-panel/index.ts +1 -0
- package/src/components/workflow-source-panel/workflow-source-content.tsx +84 -0
- package/src/components/workflow-source-panel/workflow-source-panel.tsx +14 -81
- package/src/core/entities/step.ts +4 -0
- package/src/core/workflow-run-tabs.ts +8 -0
- package/src/pages/workflow-run-detail-page.tsx +31 -4
- package/src/pages/workflow-run-pages.test.tsx +185 -45
- package/src/routes/inputs.test.ts +47 -0
- package/src/routes/inputs.ts +45 -0
- package/src/storybook-config.test.ts +27 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/tsconfig.test.json +7 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {ApiError} from '@shipfox/client-api';
|
|
2
2
|
import {QueryLoadError} from '@shipfox/client-ui';
|
|
3
|
+
import {EmptyState} from '@shipfox/react-ui/empty-state';
|
|
3
4
|
import {RelativeTimeProvider} from '@shipfox/react-ui/relative-time';
|
|
5
|
+
import {Tabs, TabsContent, TabsContents} from '@shipfox/react-ui/tabs';
|
|
4
6
|
import {toast} from '@shipfox/react-ui/toast';
|
|
5
7
|
import {useNavigate} from '@tanstack/react-router';
|
|
6
8
|
import {useEffect, useId, useRef, useState} from 'react';
|
|
@@ -10,6 +12,7 @@ import {
|
|
|
10
12
|
type StepSourceLocation,
|
|
11
13
|
type WorkflowRunRerunMode,
|
|
12
14
|
} from '#core/workflow-run.js';
|
|
15
|
+
import type {RunAnnotationSummary} from '#core/workflow-run-tabs.js';
|
|
13
16
|
import {
|
|
14
17
|
type WorkflowRunSelectionInput,
|
|
15
18
|
withoutWorkflowRunSelectionSearch,
|
|
@@ -19,12 +22,20 @@ import {
|
|
|
19
22
|
useRerunWorkflowRunMutation,
|
|
20
23
|
useWorkflowRunAttemptQuery,
|
|
21
24
|
} from '#hooks/api/workflow-runs.js';
|
|
25
|
+
import {WORKFLOW_RUN_TABS, type WorkflowRunTab} from '#routes/inputs.js';
|
|
22
26
|
import {JobGraph} from '../job-graph/index.js';
|
|
27
|
+
import type {JobGraphSelectionSource} from '../job-graph/types.js';
|
|
23
28
|
import {WorkflowRunSummary} from '../workflow-run-summary/index.js';
|
|
24
|
-
import {
|
|
25
|
-
|
|
29
|
+
import {
|
|
30
|
+
RunAnnotationSummaryLine,
|
|
31
|
+
RunAnnotationsEmpty,
|
|
32
|
+
RunJobsList,
|
|
33
|
+
RunTabStrip,
|
|
34
|
+
} from '../workflow-run-tabs/index.js';
|
|
35
|
+
import {WorkflowSourceContent, WorkflowSourcePanel} from '../workflow-source-panel/index.js';
|
|
26
36
|
import {resolveWorkflowRunSelection} from './workflow-run-selection.js';
|
|
27
37
|
import {
|
|
38
|
+
WorkflowRunContentSkeleton,
|
|
28
39
|
WorkflowRunNotFound,
|
|
29
40
|
WorkflowRunSkeleton,
|
|
30
41
|
WorkflowRunStaleError,
|
|
@@ -42,12 +53,14 @@ export interface WorkflowRunViewProps {
|
|
|
42
53
|
workflowRunId?: string | undefined;
|
|
43
54
|
selection?: WorkflowRunSelectionInput | undefined;
|
|
44
55
|
onSelectionChange?: ((selection: WorkflowRunSelectionInput) => void) | undefined;
|
|
56
|
+
tab?: WorkflowRunTab | undefined;
|
|
57
|
+
onTabChange?: ((tab: WorkflowRunTab) => void) | undefined;
|
|
58
|
+
annotationSummary?: RunAnnotationSummary | undefined;
|
|
45
59
|
}
|
|
46
60
|
|
|
47
61
|
/**
|
|
48
|
-
* Renders the run for `workflowRunId`,
|
|
49
|
-
*
|
|
50
|
-
* loading state itself.
|
|
62
|
+
* Renders the run for `workflowRunId`, keeping the local tab strip mounted while the run
|
|
63
|
+
* request is pending so polling never inserts navigation under the user's cursor.
|
|
51
64
|
*/
|
|
52
65
|
export function WorkflowRunView({
|
|
53
66
|
projectId,
|
|
@@ -56,6 +69,9 @@ export function WorkflowRunView({
|
|
|
56
69
|
workflowRunId,
|
|
57
70
|
selection,
|
|
58
71
|
onSelectionChange,
|
|
72
|
+
tab,
|
|
73
|
+
onTabChange,
|
|
74
|
+
annotationSummary,
|
|
59
75
|
}: WorkflowRunViewProps) {
|
|
60
76
|
const runQuery = useWorkflowRunAttemptQuery({workflowRunId, runAttempt: selection?.runAttempt});
|
|
61
77
|
const rerunMutation = useRerunWorkflowRunMutation(projectId);
|
|
@@ -70,6 +86,9 @@ export function WorkflowRunView({
|
|
|
70
86
|
rerunMutation={rerunMutation}
|
|
71
87
|
selection={selection}
|
|
72
88
|
onSelectionChange={onSelectionChange}
|
|
89
|
+
tab={tab}
|
|
90
|
+
onTabChange={onTabChange}
|
|
91
|
+
annotationSummary={annotationSummary}
|
|
73
92
|
/>
|
|
74
93
|
</div>
|
|
75
94
|
</RelativeTimeProvider>
|
|
@@ -83,6 +102,9 @@ function RunViewContent({
|
|
|
83
102
|
rerunMutation,
|
|
84
103
|
selection,
|
|
85
104
|
onSelectionChange,
|
|
105
|
+
tab,
|
|
106
|
+
onTabChange,
|
|
107
|
+
annotationSummary,
|
|
86
108
|
}: {
|
|
87
109
|
workspaceSlug: string | undefined;
|
|
88
110
|
projectSlug: string | undefined;
|
|
@@ -90,18 +112,22 @@ function RunViewContent({
|
|
|
90
112
|
rerunMutation: ReturnType<typeof useRerunWorkflowRunMutation>;
|
|
91
113
|
selection: WorkflowRunSelectionInput | undefined;
|
|
92
114
|
onSelectionChange: ((selection: WorkflowRunSelectionInput) => void) | undefined;
|
|
115
|
+
tab: WorkflowRunTab | undefined;
|
|
116
|
+
onTabChange: ((tab: WorkflowRunTab) => void) | undefined;
|
|
117
|
+
annotationSummary: RunAnnotationSummary | undefined;
|
|
93
118
|
}) {
|
|
94
119
|
const navigate = useNavigate();
|
|
95
120
|
const [selectedJobId, setSelectedJobId] = useState<string | undefined>();
|
|
96
121
|
const [selectedJobExecutionId, setSelectedJobExecutionId] = useState<string | undefined>();
|
|
122
|
+
const [localTab, setLocalTab] = useState<WorkflowRunTab>(tab ?? 'summary');
|
|
97
123
|
const [sourcePanelOpen, setSourcePanelOpen] = useState(false);
|
|
98
124
|
const [sourceFocus, setSourceFocus] = useState<WorkflowSourceFocus | null>(null);
|
|
99
125
|
const sourcePanelId = useId();
|
|
100
|
-
|
|
101
|
-
// The button that last opened the panel (summary or a step detail), so Escape /
|
|
102
|
-
// Close returns focus to whoever opened it.
|
|
126
|
+
// The step Source button that last opened the panel, so Escape / Close returns focus to it.
|
|
103
127
|
const lastSourceTriggerRef = useRef<HTMLButtonElement | null>(null);
|
|
104
128
|
const selectionControlled = selection !== undefined;
|
|
129
|
+
const tabControlled = onTabChange !== undefined;
|
|
130
|
+
const activeTab = tabControlled ? (tab ?? 'summary') : localTab;
|
|
105
131
|
const sourceAvailable =
|
|
106
132
|
query.data?.sourceSnapshot !== null && query.data?.sourceSnapshot !== undefined;
|
|
107
133
|
const cancelMutation = useCancelWorkflowRunMutation(query.data);
|
|
@@ -113,8 +139,8 @@ function RunViewContent({
|
|
|
113
139
|
}
|
|
114
140
|
}, [sourceAvailable]);
|
|
115
141
|
|
|
116
|
-
// If a refetch drops the focused step or its location,
|
|
117
|
-
//
|
|
142
|
+
// If a refetch drops the focused step or its location, close the panel so it never points at
|
|
143
|
+
// an unmounted Source button.
|
|
118
144
|
useEffect(() => {
|
|
119
145
|
if (!sourceFocus) return;
|
|
120
146
|
const stillLocated = query.data?.jobs.some((job) =>
|
|
@@ -124,31 +150,26 @@ function RunViewContent({
|
|
|
124
150
|
);
|
|
125
151
|
if (!stillLocated) {
|
|
126
152
|
setSourceFocus(null);
|
|
127
|
-
|
|
153
|
+
setSourcePanelOpen(false);
|
|
128
154
|
}
|
|
129
155
|
}, [sourceFocus, query.data]);
|
|
130
156
|
|
|
131
|
-
if (query.isPending) return <WorkflowRunSkeleton />;
|
|
132
|
-
|
|
133
|
-
// Only show the full error placeholder when nothing ever loaded. A refetch that fails after
|
|
134
|
-
// a prior success keeps the loaded run on screen (see below) instead of wiping the pane,
|
|
135
|
-
// since active-run polling can hit a transient API error.
|
|
136
|
-
if (query.isError && query.data === undefined) {
|
|
137
|
-
if (query.error instanceof ApiError && query.error.status === 404) {
|
|
138
|
-
return <WorkflowRunNotFound />;
|
|
139
|
-
}
|
|
140
|
-
return <QueryLoadError query={query} subject="workflow run" icon="pulseLine" />;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (query.data === undefined) return <WorkflowRunSkeleton />;
|
|
144
|
-
|
|
145
157
|
const runData = query.data;
|
|
146
|
-
const resolvedSelection =
|
|
147
|
-
|
|
148
|
-
|
|
158
|
+
const resolvedSelection =
|
|
159
|
+
selectionControlled && runData
|
|
160
|
+
? resolveWorkflowRunSelection({
|
|
161
|
+
run: runData,
|
|
162
|
+
selection: selection as WorkflowRunSelectionInput,
|
|
163
|
+
})
|
|
164
|
+
: undefined;
|
|
165
|
+
const hasExplicitJobSelection = Boolean(
|
|
166
|
+
selection?.jobId || selection?.jobExecutionId || selection?.stepId || selection?.stepAttemptId,
|
|
167
|
+
);
|
|
149
168
|
const selectedJob = selectionControlled
|
|
150
|
-
?
|
|
151
|
-
|
|
169
|
+
? hasExplicitJobSelection
|
|
170
|
+
? resolvedSelection?.job
|
|
171
|
+
: undefined
|
|
172
|
+
: (runData?.jobs.find((job) => job.id === selectedJobId) ?? runData?.jobs.at(0));
|
|
152
173
|
const selectedJobExecution = selectionControlled
|
|
153
174
|
? resolvedSelection?.jobExecution
|
|
154
175
|
: selectedJob
|
|
@@ -157,13 +178,32 @@ function RunViewContent({
|
|
|
157
178
|
const selectedAttemptId = selectionControlled
|
|
158
179
|
? (resolvedSelection?.selectedAttemptId ?? null)
|
|
159
180
|
: undefined;
|
|
160
|
-
// Explicit per-step focus wins; fall back to the URL-selected step so deep links
|
|
161
|
-
//
|
|
181
|
+
// Explicit per-step focus wins; fall back to the URL-selected step so deep links still
|
|
182
|
+
// pre-highlight source in the Source tab.
|
|
162
183
|
const highlightedLineRange =
|
|
163
184
|
sourceFocus?.location ?? resolvedSelection?.step?.sourceLocation ?? null;
|
|
164
|
-
const sourceSnapshot = runData
|
|
185
|
+
const sourceSnapshot = runData?.sourceSnapshot ?? null;
|
|
186
|
+
|
|
187
|
+
function changeTab(nextTab: WorkflowRunTab) {
|
|
188
|
+
if (!tabControlled) {
|
|
189
|
+
setLocalTab(nextTab);
|
|
190
|
+
} else if (activeTab !== nextTab) {
|
|
191
|
+
onTabChange?.(nextTab);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (nextTab !== 'jobs') {
|
|
195
|
+
setSourcePanelOpen(false);
|
|
196
|
+
setSourceFocus(null);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function handleTabChange(nextTab: string) {
|
|
201
|
+
if (!WORKFLOW_RUN_TABS.some((value) => value === nextTab)) return;
|
|
202
|
+
changeTab(nextTab as WorkflowRunTab);
|
|
203
|
+
}
|
|
204
|
+
|
|
165
205
|
async function rerun(mode: WorkflowRunRerunMode) {
|
|
166
|
-
if (!workspaceSlug || !projectSlug) {
|
|
206
|
+
if (!runData || !workspaceSlug || !projectSlug) {
|
|
167
207
|
toast.error('Could not start re-run from this route.');
|
|
168
208
|
return;
|
|
169
209
|
}
|
|
@@ -185,12 +225,16 @@ function RunViewContent({
|
|
|
185
225
|
}
|
|
186
226
|
}
|
|
187
227
|
|
|
188
|
-
function selectJob(jobId: string | undefined) {
|
|
228
|
+
function selectJob(jobId: string | undefined, source: JobGraphSelectionSource = 'pointer') {
|
|
229
|
+
if (source === 'pointer') changeTab('jobs');
|
|
189
230
|
if (!selectionControlled) {
|
|
190
231
|
setSelectedJobId(jobId);
|
|
191
232
|
setSelectedJobExecutionId(undefined);
|
|
192
233
|
return;
|
|
193
234
|
}
|
|
235
|
+
// Keyboard roving focus is an interaction within the Summary graph. Keep that panel mounted
|
|
236
|
+
// and let JobGraphView own the visual selection so focus is not moved to a different tab.
|
|
237
|
+
if (source === 'keyboard') return;
|
|
194
238
|
|
|
195
239
|
onSelectionChange?.(
|
|
196
240
|
jobId ? {jobId, runAttempt: selection?.runAttempt} : {runAttempt: selection?.runAttempt},
|
|
@@ -235,20 +279,6 @@ function RunViewContent({
|
|
|
235
279
|
});
|
|
236
280
|
}
|
|
237
281
|
|
|
238
|
-
function openWholeWorkflowSource() {
|
|
239
|
-
setSourceFocus(null);
|
|
240
|
-
lastSourceTriggerRef.current = sourceButtonRef.current;
|
|
241
|
-
setSourcePanelOpen(true);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function toggleWholeWorkflowSource() {
|
|
245
|
-
if (sourcePanelOpen && sourceFocus === null) {
|
|
246
|
-
closeSourcePanel();
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
openWholeWorkflowSource();
|
|
250
|
-
}
|
|
251
|
-
|
|
252
282
|
function openStepSource(
|
|
253
283
|
stepId: string,
|
|
254
284
|
location: StepSourceLocation,
|
|
@@ -261,13 +291,11 @@ function RunViewContent({
|
|
|
261
291
|
|
|
262
292
|
function closeSourcePanel() {
|
|
263
293
|
const trigger = lastSourceTriggerRef.current;
|
|
264
|
-
const fallbackTrigger = sourceButtonRef.current;
|
|
265
294
|
setSourcePanelOpen(false);
|
|
266
|
-
// Defer so focus lands after the panel unmounts; clear the focus only after
|
|
267
|
-
//
|
|
295
|
+
// Defer so focus lands after the panel unmounts; clear the focus only after focusing so the
|
|
296
|
+
// opener button is still expanded (force-visible) on return.
|
|
268
297
|
window.setTimeout(() => {
|
|
269
|
-
|
|
270
|
-
focusTarget?.focus();
|
|
298
|
+
if (trigger?.isConnected) trigger.focus();
|
|
271
299
|
setSourceFocus(null);
|
|
272
300
|
}, 0);
|
|
273
301
|
}
|
|
@@ -280,38 +308,73 @@ function RunViewContent({
|
|
|
280
308
|
});
|
|
281
309
|
}
|
|
282
310
|
|
|
311
|
+
const fatalError = query.isError && runData === undefined;
|
|
312
|
+
const tabState = fatalError ? (
|
|
313
|
+
query.error instanceof ApiError && query.error.status === 404 ? (
|
|
314
|
+
<WorkflowRunNotFound />
|
|
315
|
+
) : (
|
|
316
|
+
<QueryLoadError query={query} subject="workflow run" icon="pulseLine" />
|
|
317
|
+
)
|
|
318
|
+
) : query.isPending || runData === undefined ? (
|
|
319
|
+
<WorkflowRunContentSkeleton />
|
|
320
|
+
) : null;
|
|
321
|
+
|
|
283
322
|
return (
|
|
284
323
|
<>
|
|
285
|
-
<
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
324
|
+
<Tabs
|
|
325
|
+
value={activeTab}
|
|
326
|
+
onValueChange={handleTabChange}
|
|
327
|
+
className="flex min-w-0 flex-1 flex-col gap-0"
|
|
328
|
+
>
|
|
329
|
+
{runData ? (
|
|
330
|
+
<WorkflowRunSummary
|
|
331
|
+
workspaceSlug={workspaceSlug}
|
|
332
|
+
projectSlug={projectSlug}
|
|
333
|
+
run={runData}
|
|
334
|
+
cancelling={cancelMutation.isPending}
|
|
335
|
+
onCancel={cancelRun}
|
|
336
|
+
rerunPending={rerunMutation.isPending}
|
|
337
|
+
onRerun={(mode) => void rerun(mode)}
|
|
338
|
+
latestAttempt={runData.latestAttempt}
|
|
339
|
+
/>
|
|
340
|
+
) : (
|
|
341
|
+
<WorkflowRunSkeleton />
|
|
342
|
+
)}
|
|
343
|
+
{runData && query.isError ? <WorkflowRunStaleError query={query} /> : null}
|
|
344
|
+
<RunTabStrip
|
|
345
|
+
jobCount={runData?.jobs.length}
|
|
346
|
+
jobsFailed={runData?.jobs.filter((job) => job.status === 'failed').length}
|
|
347
|
+
annotationSummary={annotationSummary}
|
|
300
348
|
/>
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
349
|
+
<TabsContents className="min-h-0 flex-1 overflow-auto bg-background-neutral-background pb-24 pt-16">
|
|
350
|
+
<TabsContent
|
|
351
|
+
value="summary"
|
|
352
|
+
tabIndex={-1}
|
|
353
|
+
className="mx-auto flex w-full max-w-[1120px] flex-col gap-16 px-24 outline-none"
|
|
354
|
+
>
|
|
355
|
+
{runData ? (
|
|
356
|
+
<JobGraph
|
|
357
|
+
run={runData}
|
|
358
|
+
selectedJobId={selectedJob?.id}
|
|
359
|
+
onSelectedJobChange={selectJob}
|
|
360
|
+
/>
|
|
361
|
+
) : (
|
|
362
|
+
tabState
|
|
363
|
+
)}
|
|
364
|
+
</TabsContent>
|
|
365
|
+
<TabsContent
|
|
366
|
+
value="jobs"
|
|
367
|
+
tabIndex={-1}
|
|
368
|
+
className="mx-auto w-full max-w-[1120px] px-24 outline-none"
|
|
369
|
+
>
|
|
370
|
+
{runData ? (
|
|
371
|
+
<RunJobsList
|
|
372
|
+
jobs={runData.jobs}
|
|
373
|
+
selectedJobId={selectedJob?.id}
|
|
374
|
+
onSelectedJobChange={selectJob}
|
|
311
375
|
workspaceSlug={workspaceSlug}
|
|
312
|
-
job={selectedJob}
|
|
313
376
|
selectedJobExecution={selectedJobExecution}
|
|
314
|
-
selectedAttemptId={
|
|
377
|
+
selectedAttemptId={selectedAttemptId}
|
|
315
378
|
onSelectedJobExecutionChange={selectJobExecution}
|
|
316
379
|
onSelectedAttemptChange={selectionControlled ? selectAttempt : undefined}
|
|
317
380
|
sourcePanelId={sourcePanelId}
|
|
@@ -319,10 +382,61 @@ function RunViewContent({
|
|
|
319
382
|
focusedSourceStepId={sourceFocus?.stepId ?? null}
|
|
320
383
|
onOpenStepSource={openStepSource}
|
|
321
384
|
/>
|
|
322
|
-
) :
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
385
|
+
) : (
|
|
386
|
+
tabState
|
|
387
|
+
)}
|
|
388
|
+
</TabsContent>
|
|
389
|
+
<TabsContent
|
|
390
|
+
value="annotations"
|
|
391
|
+
tabIndex={-1}
|
|
392
|
+
className="mx-auto flex w-full max-w-[1120px] flex-col gap-16 px-24 outline-none"
|
|
393
|
+
>
|
|
394
|
+
{runData ? (
|
|
395
|
+
<>
|
|
396
|
+
<RunAnnotationSummaryLine
|
|
397
|
+
summary={annotationSummary}
|
|
398
|
+
workspaceSlug={workspaceSlug}
|
|
399
|
+
projectSlug={projectSlug}
|
|
400
|
+
workflowRunId={runData.id}
|
|
401
|
+
search={selection}
|
|
402
|
+
/>
|
|
403
|
+
<RunAnnotationsEmpty />
|
|
404
|
+
</>
|
|
405
|
+
) : (
|
|
406
|
+
tabState
|
|
407
|
+
)}
|
|
408
|
+
</TabsContent>
|
|
409
|
+
<TabsContent
|
|
410
|
+
value="source"
|
|
411
|
+
keepMounted={sourceSnapshot !== null}
|
|
412
|
+
tabIndex={-1}
|
|
413
|
+
className="mx-auto flex min-h-full w-full max-w-[1120px] px-24 outline-none"
|
|
414
|
+
>
|
|
415
|
+
{runData ? (
|
|
416
|
+
sourceSnapshot ? (
|
|
417
|
+
<WorkflowSourceContent
|
|
418
|
+
source={sourceSnapshot}
|
|
419
|
+
highlightedLineRange={highlightedLineRange}
|
|
420
|
+
scrollHighlightedIntoView
|
|
421
|
+
/>
|
|
422
|
+
) : (
|
|
423
|
+
<EmptyState
|
|
424
|
+
className="min-h-160 w-full"
|
|
425
|
+
icon="fileDamageLine"
|
|
426
|
+
title="Source snapshot unavailable"
|
|
427
|
+
description={
|
|
428
|
+
runData.isTemporary
|
|
429
|
+
? 'Temporary runs do not capture workflow source.'
|
|
430
|
+
: 'This run was created before workflow source snapshots were captured.'
|
|
431
|
+
}
|
|
432
|
+
/>
|
|
433
|
+
)
|
|
434
|
+
) : (
|
|
435
|
+
tabState
|
|
436
|
+
)}
|
|
437
|
+
</TabsContent>
|
|
438
|
+
</TabsContents>
|
|
439
|
+
</Tabs>
|
|
326
440
|
<WorkflowSourcePanel
|
|
327
441
|
id={sourcePanelId}
|
|
328
442
|
source={sourceSnapshot}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CodeBlock,
|
|
3
|
+
CodeBlockBody,
|
|
4
|
+
CodeBlockContent,
|
|
5
|
+
CodeBlockCopyButton,
|
|
6
|
+
CodeBlockFilename,
|
|
7
|
+
CodeBlockFiles,
|
|
8
|
+
CodeBlockHeader,
|
|
9
|
+
type CodeBlockHighlightedLineRange,
|
|
10
|
+
CodeBlockItem,
|
|
11
|
+
} from '@shipfox/react-ui/code-block';
|
|
12
|
+
import {cn} from '@shipfox/react-ui/utils';
|
|
13
|
+
import type {ReactNode} from 'react';
|
|
14
|
+
import type {WorkflowSourceSnapshot} from '#core/workflow-run.js';
|
|
15
|
+
|
|
16
|
+
const WORKFLOW_SOURCE_FILENAME = 'workflow.yaml';
|
|
17
|
+
const WORKFLOW_SOURCE_CODE_THEMES = {
|
|
18
|
+
light: 'vitesse-dark',
|
|
19
|
+
dark: 'vitesse-dark',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export interface WorkflowSourceContentProps {
|
|
23
|
+
source: WorkflowSourceSnapshot;
|
|
24
|
+
highlightedLineRange?: CodeBlockHighlightedLineRange | null | undefined;
|
|
25
|
+
scrollHighlightedIntoView?: boolean | undefined;
|
|
26
|
+
headerAction?: ReactNode;
|
|
27
|
+
className?: string | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function WorkflowSourceContent({
|
|
31
|
+
source,
|
|
32
|
+
highlightedLineRange,
|
|
33
|
+
scrollHighlightedIntoView,
|
|
34
|
+
headerAction,
|
|
35
|
+
className,
|
|
36
|
+
}: WorkflowSourceContentProps) {
|
|
37
|
+
const data = [
|
|
38
|
+
{
|
|
39
|
+
language: 'yaml',
|
|
40
|
+
filename: WORKFLOW_SOURCE_FILENAME,
|
|
41
|
+
code: source.content,
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<CodeBlock
|
|
47
|
+
data={data}
|
|
48
|
+
className={cn(
|
|
49
|
+
'flex size-full flex-col rounded-none bg-background-contrast-base shadow-none',
|
|
50
|
+
className,
|
|
51
|
+
)}
|
|
52
|
+
>
|
|
53
|
+
<CodeBlockHeader className="shrink-0 border-b border-border-contrast-base bg-background-contrast-base">
|
|
54
|
+
<CodeBlockFiles>
|
|
55
|
+
{(item) => <CodeBlockFilename value={item.filename}>{item.filename}</CodeBlockFilename>}
|
|
56
|
+
</CodeBlockFiles>
|
|
57
|
+
<CodeBlockCopyButton />
|
|
58
|
+
{headerAction}
|
|
59
|
+
</CodeBlockHeader>
|
|
60
|
+
<CodeBlockBody className="flex min-h-0 flex-1 overflow-auto scrollbar">
|
|
61
|
+
{(item) => (
|
|
62
|
+
<CodeBlockItem
|
|
63
|
+
value={item.filename}
|
|
64
|
+
className={cn(
|
|
65
|
+
'min-h-full px-0 pb-0',
|
|
66
|
+
'[&>div]:rounded-none [&>div]:border-0 [&>div]:bg-background-contrast-base [&>div]:dark:bg-background-contrast-base',
|
|
67
|
+
'[&_code]:!text-sm [&_code]:!text-foreground-neutral-on-inverted [&_.line]:!text-sm [&_.line]:before:!text-sm [&_.line]:before:!text-foreground-neutral-muted',
|
|
68
|
+
)}
|
|
69
|
+
>
|
|
70
|
+
<CodeBlockContent
|
|
71
|
+
language="yaml"
|
|
72
|
+
themes={WORKFLOW_SOURCE_CODE_THEMES}
|
|
73
|
+
syntaxHighlighting
|
|
74
|
+
highlightedLineRange={highlightedLineRange}
|
|
75
|
+
scrollHighlightedIntoView={scrollHighlightedIntoView}
|
|
76
|
+
>
|
|
77
|
+
{item.code}
|
|
78
|
+
</CodeBlockContent>
|
|
79
|
+
</CodeBlockItem>
|
|
80
|
+
)}
|
|
81
|
+
</CodeBlockBody>
|
|
82
|
+
</CodeBlock>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -1,24 +1,9 @@
|
|
|
1
1
|
import {Button} from '@shipfox/react-ui/button';
|
|
2
|
-
import {
|
|
3
|
-
CodeBlock,
|
|
4
|
-
CodeBlockBody,
|
|
5
|
-
CodeBlockContent,
|
|
6
|
-
CodeBlockCopyButton,
|
|
7
|
-
CodeBlockFilename,
|
|
8
|
-
CodeBlockFiles,
|
|
9
|
-
CodeBlockHeader,
|
|
10
|
-
type CodeBlockHighlightedLineRange,
|
|
11
|
-
CodeBlockItem,
|
|
12
|
-
} from '@shipfox/react-ui/code-block';
|
|
2
|
+
import type {CodeBlockHighlightedLineRange} from '@shipfox/react-ui/code-block';
|
|
13
3
|
import {Sheet, SheetClose, SheetContent, SheetTitle} from '@shipfox/react-ui/sheet';
|
|
14
4
|
import {cn} from '@shipfox/react-ui/utils';
|
|
15
5
|
import type {WorkflowSourceSnapshot} from '#core/workflow-run.js';
|
|
16
|
-
|
|
17
|
-
const WORKFLOW_SOURCE_FILENAME = 'workflow.yaml';
|
|
18
|
-
const WORKFLOW_SOURCE_CODE_THEMES = {
|
|
19
|
-
light: 'vitesse-dark',
|
|
20
|
-
dark: 'vitesse-dark',
|
|
21
|
-
};
|
|
6
|
+
import {WorkflowSourceContent} from './workflow-source-content.js';
|
|
22
7
|
|
|
23
8
|
export interface WorkflowSourcePanelProps {
|
|
24
9
|
id: string;
|
|
@@ -61,76 +46,24 @@ export function WorkflowSourcePanel({
|
|
|
61
46
|
)}
|
|
62
47
|
>
|
|
63
48
|
<SheetTitle className="sr-only">Workflow source</SheetTitle>
|
|
64
|
-
<
|
|
49
|
+
<WorkflowSourceContent
|
|
65
50
|
source={source}
|
|
66
51
|
highlightedLineRange={highlightedLineRange}
|
|
67
52
|
scrollHighlightedIntoView={scrollHighlightedIntoView}
|
|
53
|
+
headerAction={
|
|
54
|
+
<SheetClose asChild>
|
|
55
|
+
<Button
|
|
56
|
+
type="button"
|
|
57
|
+
variant="transparentMuted"
|
|
58
|
+
size="sm"
|
|
59
|
+
iconLeft="close"
|
|
60
|
+
aria-label="Close source"
|
|
61
|
+
/>
|
|
62
|
+
</SheetClose>
|
|
63
|
+
}
|
|
68
64
|
/>
|
|
69
65
|
</SheetContent>
|
|
70
66
|
) : null}
|
|
71
67
|
</Sheet>
|
|
72
68
|
);
|
|
73
69
|
}
|
|
74
|
-
|
|
75
|
-
function WorkflowSourcePanelContent({
|
|
76
|
-
source,
|
|
77
|
-
highlightedLineRange,
|
|
78
|
-
scrollHighlightedIntoView,
|
|
79
|
-
}: {
|
|
80
|
-
source: WorkflowSourceSnapshot;
|
|
81
|
-
highlightedLineRange: CodeBlockHighlightedLineRange | null | undefined;
|
|
82
|
-
scrollHighlightedIntoView: boolean | undefined;
|
|
83
|
-
}) {
|
|
84
|
-
const data = [
|
|
85
|
-
{
|
|
86
|
-
language: 'yaml',
|
|
87
|
-
filename: WORKFLOW_SOURCE_FILENAME,
|
|
88
|
-
code: source.content,
|
|
89
|
-
},
|
|
90
|
-
];
|
|
91
|
-
|
|
92
|
-
return (
|
|
93
|
-
<CodeBlock
|
|
94
|
-
data={data}
|
|
95
|
-
className="flex size-full flex-col rounded-none bg-background-contrast-base shadow-none"
|
|
96
|
-
>
|
|
97
|
-
<CodeBlockHeader className="shrink-0 border-b border-border-contrast-base bg-background-contrast-base">
|
|
98
|
-
<CodeBlockFiles>
|
|
99
|
-
{(item) => <CodeBlockFilename value={item.filename}>{item.filename}</CodeBlockFilename>}
|
|
100
|
-
</CodeBlockFiles>
|
|
101
|
-
<CodeBlockCopyButton />
|
|
102
|
-
<SheetClose asChild>
|
|
103
|
-
<Button
|
|
104
|
-
type="button"
|
|
105
|
-
variant="transparentMuted"
|
|
106
|
-
size="sm"
|
|
107
|
-
iconLeft="close"
|
|
108
|
-
aria-label="Close source"
|
|
109
|
-
/>
|
|
110
|
-
</SheetClose>
|
|
111
|
-
</CodeBlockHeader>
|
|
112
|
-
<CodeBlockBody className="flex min-h-0 flex-1 overflow-auto scrollbar">
|
|
113
|
-
{(item) => (
|
|
114
|
-
<CodeBlockItem
|
|
115
|
-
value={item.filename}
|
|
116
|
-
className={cn(
|
|
117
|
-
'min-h-full px-0 pb-0',
|
|
118
|
-
'[&>div]:rounded-none [&>div]:border-0 [&>div]:bg-background-contrast-base [&>div]:dark:bg-background-contrast-base',
|
|
119
|
-
'[&_code]:!text-sm [&_code]:!text-foreground-neutral-on-inverted [&_.line]:!text-sm [&_.line]:before:!text-sm [&_.line]:before:!text-foreground-neutral-muted',
|
|
120
|
-
)}
|
|
121
|
-
>
|
|
122
|
-
<CodeBlockContent
|
|
123
|
-
language="yaml"
|
|
124
|
-
themes={WORKFLOW_SOURCE_CODE_THEMES}
|
|
125
|
-
syntaxHighlighting
|
|
126
|
-
highlightedLineRange={highlightedLineRange}
|
|
127
|
-
scrollHighlightedIntoView={scrollHighlightedIntoView}
|
|
128
|
-
>
|
|
129
|
-
{item.code}
|
|
130
|
-
</CodeBlockContent>
|
|
131
|
-
</CodeBlockItem>
|
|
132
|
-
)}
|
|
133
|
-
</CodeBlockBody>
|
|
134
|
-
</CodeBlock>
|
|
135
|
-
);
|
|
136
|
-
}
|
|
@@ -4,6 +4,8 @@ export type StepErrorReason =
|
|
|
4
4
|
| 'checkout_failed'
|
|
5
5
|
| 'checkout_auth_failed'
|
|
6
6
|
| 'checkout_unavailable'
|
|
7
|
+
| 'checkout_path_invalid'
|
|
8
|
+
| 'checkout_destination_occupied'
|
|
7
9
|
| 'git_unavailable'
|
|
8
10
|
| 'workspace_prep_failed'
|
|
9
11
|
| 'setup_aborted'
|
|
@@ -23,6 +25,8 @@ export const STEP_ERROR_REASONS = new Set<StepErrorReason>([
|
|
|
23
25
|
'checkout_failed',
|
|
24
26
|
'checkout_auth_failed',
|
|
25
27
|
'checkout_unavailable',
|
|
28
|
+
'checkout_path_invalid',
|
|
29
|
+
'checkout_destination_occupied',
|
|
26
30
|
'git_unavailable',
|
|
27
31
|
'workspace_prep_failed',
|
|
28
32
|
'setup_aborted',
|