@shipfox/client-workflows 14.0.1 → 16.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/.storybook/preview.css +4 -0
- package/.storybook/preview.tsx +7 -2
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +42 -0
- package/dist/components/job-detail/agent-config-failure-callout.d.ts.map +1 -0
- package/dist/components/{workflow-run-view → job-detail}/agent-config-failure-callout.js +2 -2
- package/dist/components/job-detail/agent-config-failure-callout.js.map +1 -0
- package/dist/components/job-detail/job-detail-header.d.ts +15 -0
- package/dist/components/job-detail/job-detail-header.d.ts.map +1 -0
- package/dist/components/job-detail/job-detail-header.js +124 -0
- package/dist/components/job-detail/job-detail-header.js.map +1 -0
- package/dist/components/job-detail/job-detail-view.d.ts +16 -0
- package/dist/components/job-detail/job-detail-view.d.ts.map +1 -0
- package/dist/components/job-detail/job-detail-view.js +420 -0
- package/dist/components/job-detail/job-detail-view.js.map +1 -0
- package/dist/components/job-detail/job-empty-states.d.ts +10 -0
- package/dist/components/job-detail/job-empty-states.d.ts.map +1 -0
- package/dist/components/job-detail/job-empty-states.js +195 -0
- package/dist/components/job-detail/job-empty-states.js.map +1 -0
- package/dist/components/job-detail/job-execution-switcher.d.ts.map +1 -0
- package/dist/components/job-detail/job-execution-switcher.js.map +1 -0
- package/dist/components/job-detail/job-execution-time-text.d.ts.map +1 -0
- package/dist/components/job-detail/job-execution-time-text.js.map +1 -0
- package/dist/components/job-detail/job-selection.d.ts +24 -0
- package/dist/components/job-detail/job-selection.d.ts.map +1 -0
- package/dist/components/job-detail/job-selection.js +66 -0
- package/dist/components/job-detail/job-selection.js.map +1 -0
- package/dist/components/job-detail/step-attempt-log-panel.d.ts +15 -0
- package/dist/components/job-detail/step-attempt-log-panel.d.ts.map +1 -0
- package/dist/components/{workflow-run-view → job-detail}/step-attempt-log-panel.js +61 -16
- package/dist/components/job-detail/step-attempt-log-panel.js.map +1 -0
- package/dist/components/job-graph/job-graph-content.js +1 -1
- package/dist/components/job-graph/job-graph-content.js.map +1 -1
- package/dist/components/step-list/step-list-model.js +2 -2
- package/dist/components/step-list/step-list-model.js.map +1 -1
- package/dist/components/step-list/step-list.d.ts +1 -0
- package/dist/components/step-list/step-list.d.ts.map +1 -1
- package/dist/components/step-list/step-list.js +29 -4
- package/dist/components/step-list/step-list.js.map +1 -1
- package/dist/components/workflow-run-duration-label.d.ts +9 -4
- package/dist/components/workflow-run-duration-label.d.ts.map +1 -1
- package/dist/components/workflow-run-duration-label.js +27 -7
- package/dist/components/workflow-run-duration-label.js.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-row.d.ts.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-row.js +6 -5
- 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 +69 -64
- package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
- package/dist/components/workflow-run-tabs/index.d.ts +3 -3
- package/dist/components/workflow-run-tabs/index.d.ts.map +1 -1
- package/dist/components/workflow-run-tabs/index.js +3 -3
- package/dist/components/workflow-run-tabs/index.js.map +1 -1
- package/dist/components/workflow-run-tabs/run-annotation-count-chip.d.ts +16 -0
- package/dist/components/workflow-run-tabs/run-annotation-count-chip.d.ts.map +1 -0
- package/dist/components/workflow-run-tabs/run-annotation-count-chip.js +50 -0
- package/dist/components/workflow-run-tabs/run-annotation-count-chip.js.map +1 -0
- package/dist/components/workflow-run-tabs/run-annotation-item.d.ts +20 -0
- package/dist/components/workflow-run-tabs/run-annotation-item.d.ts.map +1 -0
- package/dist/components/workflow-run-tabs/run-annotation-item.js +102 -0
- package/dist/components/workflow-run-tabs/run-annotation-item.js.map +1 -0
- package/dist/components/workflow-run-tabs/run-annotation-list.d.ts +32 -0
- package/dist/components/workflow-run-tabs/run-annotation-list.d.ts.map +1 -0
- package/dist/components/workflow-run-tabs/run-annotation-list.js +202 -0
- package/dist/components/workflow-run-tabs/run-annotation-list.js.map +1 -0
- package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts +1 -1
- package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts.map +1 -1
- package/dist/components/workflow-run-tabs/run-annotation-summary-line.js +87 -22
- package/dist/components/workflow-run-tabs/run-annotation-summary-line.js.map +1 -1
- package/dist/components/workflow-run-tabs/run-annotations-empty.d.ts +3 -1
- package/dist/components/workflow-run-tabs/run-annotations-empty.d.ts.map +1 -1
- package/dist/components/workflow-run-tabs/run-annotations-empty.js +2 -2
- package/dist/components/workflow-run-tabs/run-annotations-empty.js.map +1 -1
- package/dist/components/workflow-run-tabs/severity-visuals.d.ts +20 -0
- package/dist/components/workflow-run-tabs/severity-visuals.d.ts.map +1 -0
- package/dist/components/workflow-run-tabs/severity-visuals.js +31 -0
- package/dist/components/workflow-run-tabs/severity-visuals.js.map +1 -0
- package/dist/components/workflow-run-view/index.d.ts +0 -1
- package/dist/components/workflow-run-view/index.d.ts.map +1 -1
- package/dist/components/workflow-run-view/index.js +0 -1
- package/dist/components/workflow-run-view/index.js.map +1 -1
- package/dist/components/workflow-run-view/run-workspace-nav.d.ts +20 -0
- package/dist/components/workflow-run-view/run-workspace-nav.d.ts.map +1 -0
- package/dist/components/workflow-run-view/run-workspace-nav.js +277 -0
- package/dist/components/workflow-run-view/run-workspace-nav.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 +1 -14
- package/dist/components/workflow-run-view/workflow-run-selection.js.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-view.d.ts +10 -10
- package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-view.js +351 -239
- package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
- package/dist/components/workflow-status/status-visuals.d.ts.map +1 -1
- package/dist/components/workflow-status/status-visuals.js +10 -2
- package/dist/components/workflow-status/status-visuals.js.map +1 -1
- package/dist/components/workflow-status/workflow-status-icon.d.ts +3 -1
- package/dist/components/workflow-status/workflow-status-icon.d.ts.map +1 -1
- package/dist/components/workflow-status/workflow-status-icon.js +13 -2
- package/dist/components/workflow-status/workflow-status-icon.js.map +1 -1
- package/dist/core/entities/job-execution.d.ts +9 -0
- package/dist/core/entities/job-execution.d.ts.map +1 -1
- package/dist/core/entities/job-execution.js +13 -3
- package/dist/core/entities/job-execution.js.map +1 -1
- package/dist/core/entities/job.d.ts.map +1 -1
- package/dist/core/entities/step-attempt.d.ts.map +1 -1
- package/dist/core/entities/step.d.ts +2 -0
- package/dist/core/entities/step.d.ts.map +1 -1
- package/dist/core/entities/step.js +13 -0
- package/dist/core/entities/step.js.map +1 -1
- package/dist/core/entities/workflow-run-attempt.d.ts.map +1 -1
- package/dist/core/entities/workflow-run.d.ts +56 -1
- package/dist/core/entities/workflow-run.d.ts.map +1 -1
- package/dist/core/entities/workflow-run.js +98 -0
- package/dist/core/entities/workflow-run.js.map +1 -1
- package/dist/core/run-annotation.d.ts +73 -0
- package/dist/core/run-annotation.d.ts.map +1 -0
- package/dist/core/run-annotation.js +125 -0
- package/dist/core/run-annotation.js.map +1 -0
- package/dist/core/workflow-run.d.ts +4 -4
- package/dist/core/workflow-run.d.ts.map +1 -1
- package/dist/core/workflow-run.js +3 -3
- package/dist/core/workflow-run.js.map +1 -1
- package/dist/feature.d.ts +20 -16
- package/dist/feature.d.ts.map +1 -1
- package/dist/feature.js +5 -0
- package/dist/feature.js.map +1 -1
- package/dist/hooks/api/run-annotation-mapper.d.ts +10 -0
- package/dist/hooks/api/run-annotation-mapper.d.ts.map +1 -0
- package/dist/hooks/api/run-annotation-mapper.js +22 -0
- package/dist/hooks/api/run-annotation-mapper.js.map +1 -0
- package/dist/hooks/api/run-annotations.d.ts +25 -0
- package/dist/hooks/api/run-annotations.d.ts.map +1 -0
- package/dist/hooks/api/run-annotations.js +78 -0
- package/dist/hooks/api/run-annotations.js.map +1 -0
- package/dist/hooks/api/workflow-runs.d.ts +5 -3
- package/dist/hooks/api/workflow-runs.d.ts.map +1 -1
- package/dist/hooks/api/workflow-runs.js +5 -4
- package/dist/hooks/api/workflow-runs.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/pages/workflow-job-detail-page.d.ts +11 -0
- package/dist/pages/workflow-job-detail-page.d.ts.map +1 -0
- package/dist/pages/workflow-job-detail-page.js +66 -0
- package/dist/pages/workflow-job-detail-page.js.map +1 -0
- package/dist/pages/workflow-run-detail-page.d.ts.map +1 -1
- package/dist/pages/workflow-run-detail-page.js +4 -37
- package/dist/pages/workflow-run-detail-page.js.map +1 -1
- package/dist/routes/inputs.d.ts +52 -34
- package/dist/routes/inputs.d.ts.map +1 -1
- package/dist/routes/inputs.js +74 -43
- package/dist/routes/inputs.js.map +1 -1
- package/dist/routes/job-detail.d.ts +10 -0
- package/dist/routes/job-detail.d.ts.map +1 -0
- package/dist/routes/job-detail.js +27 -0
- package/dist/routes/job-detail.js.map +1 -0
- package/dist/routes/project-route.d.ts +1 -1
- package/dist/routes/run-detail.d.ts.map +1 -1
- package/dist/routes/runs.d.ts.map +1 -1
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +10 -9
- package/src/components/{workflow-run-view → job-detail}/agent-config-failure-callout.stories.tsx +1 -1
- package/src/components/{workflow-run-view → job-detail}/agent-config-failure-callout.tsx +2 -2
- package/src/components/job-detail/job-detail-header.test.tsx +34 -0
- package/src/components/job-detail/job-detail-header.tsx +136 -0
- package/src/components/job-detail/job-detail-view.stories.tsx +531 -0
- package/src/components/job-detail/job-detail-view.tsx +467 -0
- package/src/components/job-detail/job-empty-states.test.ts +27 -0
- package/src/components/job-detail/job-empty-states.tsx +248 -0
- package/src/components/job-detail/job-selection.test.ts +188 -0
- package/src/components/job-detail/job-selection.ts +96 -0
- package/src/components/{workflow-run-view → job-detail}/step-attempt-log-panel.test.tsx +32 -9
- package/src/components/{workflow-run-view → job-detail}/step-attempt-log-panel.tsx +52 -18
- package/src/components/job-graph/job-graph-content.tsx +1 -1
- package/src/components/step-list/step-list-model.test.ts +1 -1
- package/src/components/step-list/step-list-model.ts +2 -2
- package/src/components/step-list/step-list.stories.tsx +1 -1
- package/src/components/step-list/step-list.test.tsx +33 -0
- package/src/components/step-list/step-list.tsx +40 -9
- package/src/components/workflow-run-duration-label.tsx +50 -12
- package/src/components/workflow-run-list/workflow-run-row.tsx +6 -4
- package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +38 -0
- package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +79 -5
- package/src/components/workflow-run-summary/workflow-run-summary.tsx +56 -60
- package/src/components/workflow-run-tabs/index.ts +3 -3
- package/src/components/workflow-run-tabs/run-annotation-count-chip.tsx +63 -0
- package/src/components/workflow-run-tabs/run-annotation-item.tsx +123 -0
- package/src/components/workflow-run-tabs/run-annotation-list.tsx +258 -0
- package/src/components/workflow-run-tabs/run-annotation-summary-line.test.tsx +77 -3
- package/src/components/workflow-run-tabs/run-annotation-summary-line.tsx +92 -17
- package/src/components/workflow-run-tabs/run-annotations-empty.tsx +6 -2
- package/src/components/workflow-run-tabs/run-annotations.stories.tsx +293 -0
- package/src/components/workflow-run-tabs/severity-visuals.ts +37 -0
- package/src/components/workflow-run-view/index.ts +0 -1
- package/src/components/workflow-run-view/run-workspace-nav.test.tsx +286 -0
- package/src/components/workflow-run-view/run-workspace-nav.tsx +314 -0
- package/src/components/workflow-run-view/workflow-run-selection.ts +1 -23
- package/src/components/workflow-run-view/workflow-run-view.test.tsx +237 -1085
- package/src/components/workflow-run-view/workflow-run-view.tsx +450 -323
- package/src/components/workflow-status/status-visuals.ts +5 -2
- package/src/components/workflow-status/workflow-status-icon.stories.tsx +1 -0
- package/src/components/workflow-status/workflow-status-icon.tsx +26 -1
- package/src/core/entities/job-execution.ts +17 -2
- package/src/core/entities/step.ts +26 -0
- package/src/core/entities/workflow-run-display.test.ts +159 -0
- package/src/core/entities/workflow-run.ts +139 -1
- package/src/core/run-annotation.test.ts +315 -0
- package/src/core/run-annotation.ts +238 -0
- package/src/core/workflow-run.ts +16 -1
- package/src/feature.ts +5 -0
- package/src/hooks/api/run-annotation-mapper.ts +30 -0
- package/src/hooks/api/run-annotations.ts +137 -0
- package/src/hooks/api/workflow-runs.ts +6 -2
- package/src/index.ts +1 -0
- package/src/page-harness-budget.test.ts +1 -0
- package/src/pages/workflow-job-detail-page.test.tsx +605 -0
- package/src/pages/workflow-job-detail-page.tsx +84 -0
- package/src/pages/workflow-run-detail-page.tsx +3 -43
- package/src/pages/workflow-run-pages.test.tsx +77 -332
- package/src/routes/inputs.test.ts +56 -6
- package/src/routes/inputs.ts +71 -19
- package/src/routes/job-detail.tsx +29 -0
- package/test/pages.tsx +25 -3
- package/test/render.tsx +6 -1
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/components/workflow-run-tabs/run-jobs-list.d.ts +0 -18
- package/dist/components/workflow-run-tabs/run-jobs-list.d.ts.map +0 -1
- package/dist/components/workflow-run-tabs/run-jobs-list.js +0 -107
- package/dist/components/workflow-run-tabs/run-jobs-list.js.map +0 -1
- package/dist/components/workflow-run-tabs/run-tab-count.d.ts +0 -6
- package/dist/components/workflow-run-tabs/run-tab-count.d.ts.map +0 -1
- package/dist/components/workflow-run-tabs/run-tab-count.js +0 -27
- package/dist/components/workflow-run-tabs/run-tab-count.js.map +0 -1
- package/dist/components/workflow-run-tabs/run-tab-strip.d.ts +0 -8
- package/dist/components/workflow-run-tabs/run-tab-strip.d.ts.map +0 -1
- package/dist/components/workflow-run-tabs/run-tab-strip.js +0 -82
- package/dist/components/workflow-run-tabs/run-tab-strip.js.map +0 -1
- package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts.map +0 -1
- package/dist/components/workflow-run-view/agent-config-failure-callout.js.map +0 -1
- package/dist/components/workflow-run-view/job-card.d.ts +0 -17
- package/dist/components/workflow-run-view/job-card.d.ts.map +0 -1
- package/dist/components/workflow-run-view/job-card.js +0 -496
- package/dist/components/workflow-run-view/job-card.js.map +0 -1
- package/dist/components/workflow-run-view/job-execution-switcher.d.ts.map +0 -1
- package/dist/components/workflow-run-view/job-execution-switcher.js.map +0 -1
- package/dist/components/workflow-run-view/job-execution-time-text.d.ts.map +0 -1
- package/dist/components/workflow-run-view/job-execution-time-text.js.map +0 -1
- package/dist/components/workflow-run-view/step-attempt-log-panel.d.ts +0 -9
- package/dist/components/workflow-run-view/step-attempt-log-panel.d.ts.map +0 -1
- package/dist/components/workflow-run-view/step-attempt-log-panel.js.map +0 -1
- package/dist/core/workflow-run-tabs.d.ts +0 -9
- package/dist/core/workflow-run-tabs.d.ts.map +0 -1
- package/dist/core/workflow-run-tabs.js +0 -3
- package/dist/core/workflow-run-tabs.js.map +0 -1
- package/src/components/workflow-run-tabs/run-jobs-list.tsx +0 -151
- package/src/components/workflow-run-tabs/run-tab-count.tsx +0 -28
- package/src/components/workflow-run-tabs/run-tab-strip.stories.tsx +0 -52
- package/src/components/workflow-run-tabs/run-tab-strip.test.tsx +0 -117
- package/src/components/workflow-run-tabs/run-tab-strip.tsx +0 -97
- package/src/components/workflow-run-view/job-card.stories.tsx +0 -789
- package/src/components/workflow-run-view/job-card.test.tsx +0 -115
- package/src/components/workflow-run-view/job-card.tsx +0 -614
- package/src/core/workflow-run-tabs.ts +0 -8
- /package/dist/components/{workflow-run-view → job-detail}/agent-config-failure-callout.d.ts +0 -0
- /package/dist/components/{workflow-run-view → job-detail}/job-execution-switcher.d.ts +0 -0
- /package/dist/components/{workflow-run-view → job-detail}/job-execution-switcher.js +0 -0
- /package/dist/components/{workflow-run-view → job-detail}/job-execution-time-text.d.ts +0 -0
- /package/dist/components/{workflow-run-view → job-detail}/job-execution-time-text.js +0 -0
- /package/src/components/{workflow-run-view → job-detail}/job-execution-switcher.tsx +0 -0
- /package/src/components/{workflow-run-view → job-detail}/job-execution-time-text.tsx +0 -0
|
@@ -13,12 +13,15 @@ export interface WorkflowStatusVisual {
|
|
|
13
13
|
|
|
14
14
|
// The status -> visual mapping shared by the run-header pill (color + label) and
|
|
15
15
|
// WorkflowStatusIcon (which renders the glyph per kind). The exhaustive switch turns any new
|
|
16
|
-
// status the API grows into (DESIGN.md section 9) into a compile error
|
|
17
|
-
// tone for the queued/awaiting-* states when they land.
|
|
16
|
+
// status the API grows into (DESIGN.md section 9) into a compile error.
|
|
18
17
|
export function getWorkflowStatusVisual(status: WorkflowDisplayStatus): WorkflowStatusVisual {
|
|
19
18
|
switch (status) {
|
|
20
19
|
case 'pending':
|
|
21
20
|
return {kind: 'pending', label: 'Pending', dot: 'neutral', badge: 'neutral'};
|
|
21
|
+
// Waiting on a runner is the awaiting-* state the warning tone was held for: the run is
|
|
22
|
+
// live but nothing is executing, which is the operator's problem to see, not ours to hide.
|
|
23
|
+
case 'queued':
|
|
24
|
+
return {kind: 'queued', label: 'Queued', dot: 'warning', badge: 'warning'};
|
|
22
25
|
case 'running':
|
|
23
26
|
return {kind: 'running', label: 'Running', dot: 'info', badge: 'info'};
|
|
24
27
|
case 'listening':
|
|
@@ -46,6 +46,8 @@ const PENDING_RING_MASK = 'radial-gradient(circle closest-side, transparent 0 52
|
|
|
46
46
|
|
|
47
47
|
export interface WorkflowStatusIconProps {
|
|
48
48
|
status: WorkflowDisplayStatus;
|
|
49
|
+
/** Accessible status label when the surrounding surface needs to name its scope. */
|
|
50
|
+
ariaLabel?: string | undefined;
|
|
49
51
|
/** Optical diameter in px: 14 in the DAG node and run row. */
|
|
50
52
|
size?: number;
|
|
51
53
|
/** Pulsing halo for the running state. */
|
|
@@ -65,6 +67,7 @@ export interface WorkflowStatusIconProps {
|
|
|
65
67
|
*/
|
|
66
68
|
export function WorkflowStatusIcon({
|
|
67
69
|
status,
|
|
70
|
+
ariaLabel,
|
|
68
71
|
size = 14,
|
|
69
72
|
ripple = true,
|
|
70
73
|
tooltip = true,
|
|
@@ -86,6 +89,24 @@ export function WorkflowStatusIcon({
|
|
|
86
89
|
<Icon name="pulseLine" size={Math.max(8, Math.round(size * 0.7))} className="text-white" />
|
|
87
90
|
</span>
|
|
88
91
|
);
|
|
92
|
+
} else if (visual.kind === 'queued') {
|
|
93
|
+
// An hourglass in the same filled disc the listening state uses, so waiting reads as its
|
|
94
|
+
// own shape rather than as a pending ring that happens to be a different colour.
|
|
95
|
+
glyph = (
|
|
96
|
+
<span
|
|
97
|
+
className={cn(
|
|
98
|
+
'inline-flex shrink-0 items-center justify-center rounded-full bg-current',
|
|
99
|
+
box,
|
|
100
|
+
toneByVariant.warning,
|
|
101
|
+
)}
|
|
102
|
+
>
|
|
103
|
+
<Icon
|
|
104
|
+
name="hourglassLine"
|
|
105
|
+
size={Math.max(8, Math.round(size * 0.7))}
|
|
106
|
+
className="text-white"
|
|
107
|
+
/>
|
|
108
|
+
</span>
|
|
109
|
+
);
|
|
89
110
|
} else if (visual.kind === 'running') {
|
|
90
111
|
glyph = <Dot variant="info" ripple={ripple} className={box} />;
|
|
91
112
|
} else if (visual.kind === 'pending') {
|
|
@@ -112,7 +133,11 @@ export function WorkflowStatusIcon({
|
|
|
112
133
|
}
|
|
113
134
|
|
|
114
135
|
const indicator = (
|
|
115
|
-
<span
|
|
136
|
+
<span
|
|
137
|
+
role="img"
|
|
138
|
+
aria-label={ariaLabel ?? visual.label}
|
|
139
|
+
className={cn('inline-flex shrink-0', className)}
|
|
140
|
+
>
|
|
116
141
|
{glyph}
|
|
117
142
|
</span>
|
|
118
143
|
);
|
|
@@ -113,9 +113,24 @@ export function jobExecutionRunTimeFromTimestamps({
|
|
|
113
113
|
finishedAt: string | null;
|
|
114
114
|
}): JobExecutionTime | null {
|
|
115
115
|
if (startedAt === null) return null;
|
|
116
|
-
|
|
116
|
+
return elapsedTimeFromTimestamps({from: startedAt, to: finishedAt});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The elapsed span between two marks, still accruing while the closing mark is missing. Every
|
|
121
|
+
* duration this app shows is one of these, so queue time and run time measure the same way and
|
|
122
|
+
* only differ in which marks they read.
|
|
123
|
+
*/
|
|
124
|
+
export function elapsedTimeFromTimestamps({
|
|
125
|
+
from,
|
|
126
|
+
to,
|
|
127
|
+
}: {
|
|
128
|
+
from: string;
|
|
129
|
+
to: string | null;
|
|
130
|
+
}): JobExecutionTime | null {
|
|
131
|
+
if (to === null) return {state: 'live', fromIso: from};
|
|
117
132
|
|
|
118
|
-
const elapsed = durationBetween(
|
|
133
|
+
const elapsed = durationBetween(from, to);
|
|
119
134
|
return elapsed === null ? null : {state: 'fixed', elapsed};
|
|
120
135
|
}
|
|
121
136
|
|
|
@@ -81,3 +81,29 @@ export interface Step {
|
|
|
81
81
|
updatedAt: string;
|
|
82
82
|
attempts: StepAttempt[];
|
|
83
83
|
}
|
|
84
|
+
|
|
85
|
+
export function resolveStepAttempt(
|
|
86
|
+
step: Step,
|
|
87
|
+
attemptId: string | undefined,
|
|
88
|
+
): StepAttempt | undefined {
|
|
89
|
+
const attemptById = attemptId
|
|
90
|
+
? step.attempts.find((attempt) => attempt.id === attemptId)
|
|
91
|
+
: undefined;
|
|
92
|
+
if (attemptById) return attemptById;
|
|
93
|
+
|
|
94
|
+
const currentAttempt = step.attempts.find((attempt) => attempt.attempt === step.currentAttempt);
|
|
95
|
+
if (currentAttempt) return currentAttempt;
|
|
96
|
+
|
|
97
|
+
return step.attempts.reduce<StepAttempt | undefined>((latest, attempt) => {
|
|
98
|
+
if (!latest) return attempt;
|
|
99
|
+
return compareStepAttempts(attempt, latest) > 0 ? attempt : latest;
|
|
100
|
+
}, undefined);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function compareStepAttempts(left: StepAttempt, right: StepAttempt): number {
|
|
104
|
+
return (
|
|
105
|
+
left.attempt - right.attempt ||
|
|
106
|
+
left.executionOrder - right.executionOrder ||
|
|
107
|
+
left.id.localeCompare(right.id)
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import {
|
|
2
|
+
workflowJob,
|
|
3
|
+
workflowJobExecutionDto,
|
|
4
|
+
workflowRunJobsFixture,
|
|
5
|
+
workflowRunListItem,
|
|
6
|
+
} from '#test/fixtures/workflow-run.js';
|
|
7
|
+
import {
|
|
8
|
+
workflowRunBlockingJob,
|
|
9
|
+
workflowRunDetailDisplay,
|
|
10
|
+
workflowRunListItemDisplay,
|
|
11
|
+
} from './workflow-run.js';
|
|
12
|
+
|
|
13
|
+
const ATTEMPT_STARTED_AT = '2026-05-07T01:00:00.000Z';
|
|
14
|
+
const WORK_STARTED_AT = '2026-05-07T01:02:00.000Z';
|
|
15
|
+
const FINISHED_AT = '2026-05-07T01:05:00.000Z';
|
|
16
|
+
|
|
17
|
+
describe('workflowRunDetailDisplay', () => {
|
|
18
|
+
test('reads a running attempt whose jobs have not started as queued', () => {
|
|
19
|
+
const display = workflowRunDetailDisplay(
|
|
20
|
+
runDetail({jobs: [pendingJob()], startedAt: ATTEMPT_STARTED_AT}),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
expect(display.status).toBe('queued');
|
|
24
|
+
expect(display.duration).toEqual({kind: 'queue', state: 'live', fromIso: ATTEMPT_STARTED_AT});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('measures run time from the first execution, not from the attempt', () => {
|
|
28
|
+
const display = workflowRunDetailDisplay(
|
|
29
|
+
runDetail({
|
|
30
|
+
jobs: [startedJob(), pendingJob()],
|
|
31
|
+
startedAt: ATTEMPT_STARTED_AT,
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
expect(display.status).toBe('running');
|
|
36
|
+
expect(display.duration).toEqual({kind: 'run', state: 'live', fromIso: WORK_STARTED_AT});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('does not treat skipped jobs as started work', () => {
|
|
40
|
+
const display = workflowRunDetailDisplay(
|
|
41
|
+
runDetail({
|
|
42
|
+
jobs: [workflowJob({key: 'skipped', status: 'skipped'}), pendingJob()],
|
|
43
|
+
startedAt: ATTEMPT_STARTED_AT,
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(display.status).toBe('queued');
|
|
48
|
+
expect(display.duration).toMatchObject({kind: 'queue'});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('keeps the queue reading for a run cancelled before anything started', () => {
|
|
52
|
+
const display = workflowRunDetailDisplay(
|
|
53
|
+
runDetail({
|
|
54
|
+
status: 'cancelled',
|
|
55
|
+
jobs: [pendingJob()],
|
|
56
|
+
startedAt: ATTEMPT_STARTED_AT,
|
|
57
|
+
finishedAt: FINISHED_AT,
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(display.status).toBe('cancelled');
|
|
62
|
+
expect(display.duration).toMatchObject({kind: 'queue', state: 'fixed'});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('does not claim a queue when the run carries no jobs to prove one', () => {
|
|
66
|
+
const display = workflowRunDetailDisplay(runDetail({jobs: [], startedAt: ATTEMPT_STARTED_AT}));
|
|
67
|
+
|
|
68
|
+
expect(display.status).toBe('running');
|
|
69
|
+
expect(display.duration).toEqual({kind: 'run', state: 'live', fromIso: ATTEMPT_STARTED_AT});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('has no duration before the attempt starts', () => {
|
|
73
|
+
expect(workflowRunDetailDisplay(runDetail({jobs: [pendingJob()]})).duration).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('workflowRunListItemDisplay', () => {
|
|
78
|
+
test('reaches the detail page verdict from job status counts alone', () => {
|
|
79
|
+
const run = workflowRunListItem({
|
|
80
|
+
status: 'running',
|
|
81
|
+
...workflowRunJobsFixture(['pending', 'pending']),
|
|
82
|
+
started_at: ATTEMPT_STARTED_AT,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const display = workflowRunListItemDisplay(run);
|
|
86
|
+
|
|
87
|
+
expect(display.status).toBe('queued');
|
|
88
|
+
expect(display.duration).toEqual({kind: 'queue', state: 'live', fromIso: ATTEMPT_STARTED_AT});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('stays running once any job has left pending', () => {
|
|
92
|
+
const run = workflowRunListItem({
|
|
93
|
+
status: 'running',
|
|
94
|
+
...workflowRunJobsFixture(['succeeded', 'pending']),
|
|
95
|
+
started_at: ATTEMPT_STARTED_AT,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const display = workflowRunListItemDisplay(run);
|
|
99
|
+
|
|
100
|
+
expect(display.status).toBe('running');
|
|
101
|
+
expect(display.duration).toMatchObject({kind: 'run'});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('workflowRunBlockingJob', () => {
|
|
106
|
+
test('ignores finished queued executions when finding the blocking job', () => {
|
|
107
|
+
const historicalJob = workflowJob({
|
|
108
|
+
key: 'historical',
|
|
109
|
+
job_executions: [
|
|
110
|
+
workflowJobExecutionDto({
|
|
111
|
+
status: 'succeeded',
|
|
112
|
+
queued_at: '2026-05-07T00:59:00.000Z',
|
|
113
|
+
finished_at: '2026-05-07T01:00:00.000Z',
|
|
114
|
+
}),
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
const currentJob = workflowJob({
|
|
118
|
+
key: 'current',
|
|
119
|
+
job_executions: [workflowJobExecutionDto({queued_at: '2026-05-07T01:01:00.000Z'})],
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(workflowRunBlockingJob([historicalJob, currentJob])?.key).toBe('current');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
function pendingJob() {
|
|
127
|
+
return workflowJob({
|
|
128
|
+
key: 'lint',
|
|
129
|
+
job_executions: [workflowJobExecutionDto({queued_at: ATTEMPT_STARTED_AT})],
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function startedJob() {
|
|
134
|
+
return workflowJob({
|
|
135
|
+
key: 'build',
|
|
136
|
+
status: 'running',
|
|
137
|
+
job_executions: [
|
|
138
|
+
workflowJobExecutionDto({
|
|
139
|
+
status: 'running',
|
|
140
|
+
queued_at: ATTEMPT_STARTED_AT,
|
|
141
|
+
started_at: WORK_STARTED_AT,
|
|
142
|
+
}),
|
|
143
|
+
],
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function runDetail({
|
|
148
|
+
status = 'running',
|
|
149
|
+
jobs,
|
|
150
|
+
startedAt = null,
|
|
151
|
+
finishedAt = null,
|
|
152
|
+
}: {
|
|
153
|
+
status?: 'running' | 'cancelled';
|
|
154
|
+
jobs: ReturnType<typeof workflowJob>[];
|
|
155
|
+
startedAt?: string | null;
|
|
156
|
+
finishedAt?: string | null;
|
|
157
|
+
}) {
|
|
158
|
+
return {runAttempt: {status, startedAt, finishedAt}, jobs};
|
|
159
|
+
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import {type Job, type JobStatus, WORKFLOW_JOB_STATUSES} from './job.js';
|
|
2
|
+
import {elapsedTimeFromTimestamps, type JobExecutionDisplayDuration} from './job-execution.js';
|
|
2
3
|
import type {WorkflowRunAttempt, WorkflowRunAttemptSummary} from './workflow-run-attempt.js';
|
|
3
4
|
|
|
4
5
|
export type WorkflowRunStatus = 'pending' | 'running' | 'succeeded' | 'failed' | 'cancelled';
|
|
5
6
|
export type WorkflowRunRerunMode = 'all' | 'failed';
|
|
6
7
|
export type WorkflowStatus = WorkflowRunStatus | (typeof WORKFLOW_JOB_STATUSES)[number];
|
|
7
|
-
|
|
8
|
+
/**
|
|
9
|
+
* `listening` and `queued` are display-only: the API never returns them, and both are derived
|
|
10
|
+
* from the jobs a run already carries.
|
|
11
|
+
*/
|
|
12
|
+
export type WorkflowDisplayStatus = WorkflowStatus | 'listening' | 'queued';
|
|
8
13
|
|
|
9
14
|
export const WORKFLOW_RUN_STATUSES = [
|
|
10
15
|
'pending',
|
|
@@ -14,6 +19,13 @@ export const WORKFLOW_RUN_STATUSES = [
|
|
|
14
19
|
'cancelled',
|
|
15
20
|
] as const satisfies readonly WorkflowRunStatus[];
|
|
16
21
|
|
|
22
|
+
export const WORKFLOW_DISPLAY_STATUSES = [
|
|
23
|
+
...WORKFLOW_RUN_STATUSES,
|
|
24
|
+
...WORKFLOW_JOB_STATUSES,
|
|
25
|
+
'listening',
|
|
26
|
+
'queued',
|
|
27
|
+
] as const satisfies readonly WorkflowDisplayStatus[];
|
|
28
|
+
|
|
17
29
|
export const TERMINAL_WORKFLOW_RUN_STATUSES = [
|
|
18
30
|
'succeeded',
|
|
19
31
|
'failed',
|
|
@@ -172,3 +184,129 @@ export function isWorkflowRunTerminal(status: WorkflowRunStatus): boolean {
|
|
|
172
184
|
export function isWorkflowStatus(status: string): status is WorkflowStatus {
|
|
173
185
|
return WORKFLOW_STATUSES.has(status as WorkflowStatus);
|
|
174
186
|
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* A run's headline duration, split the way a job's already is. An attempt's `startedAt` marks
|
|
190
|
+
* when the orchestrator picked the run up, not when work began, so a run whose jobs are all
|
|
191
|
+
* still waiting for a runner reads as queue time rather than as run time.
|
|
192
|
+
*/
|
|
193
|
+
export type WorkflowRunDisplayDuration = JobExecutionDisplayDuration;
|
|
194
|
+
|
|
195
|
+
/** What every surface shows for a run, derived together so status and duration cannot disagree. */
|
|
196
|
+
export interface WorkflowRunDisplay {
|
|
197
|
+
status: WorkflowDisplayStatus;
|
|
198
|
+
duration: WorkflowRunDisplayDuration | null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The evidence a surface can offer about a run's progress.
|
|
203
|
+
*
|
|
204
|
+
* `jobStatuses` is what every surface has: the list carries status counts, the detail carries
|
|
205
|
+
* whole jobs. `firstStartedAt` is the detail's extra precision, and where it is absent the
|
|
206
|
+
* attempt's own mark stands in.
|
|
207
|
+
*/
|
|
208
|
+
export interface WorkflowRunProgress {
|
|
209
|
+
runStatus: WorkflowRunStatus;
|
|
210
|
+
startedAt: string | null;
|
|
211
|
+
finishedAt: string | null;
|
|
212
|
+
jobStatuses: JobStatus[];
|
|
213
|
+
firstStartedAt?: string | null | undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** When the run's earliest execution began, or null while none of them has. */
|
|
217
|
+
export function workflowRunFirstStartedAt(jobs: Job[]): string | null {
|
|
218
|
+
let earliest: string | null = null;
|
|
219
|
+
let earliestMs = Number.POSITIVE_INFINITY;
|
|
220
|
+
|
|
221
|
+
for (const job of jobs) {
|
|
222
|
+
for (const {startedAt} of job.jobExecutions) {
|
|
223
|
+
if (startedAt === null) continue;
|
|
224
|
+
const startedMs = new Date(startedAt).getTime();
|
|
225
|
+
if (!Number.isFinite(startedMs) || startedMs >= earliestMs) continue;
|
|
226
|
+
earliest = startedAt;
|
|
227
|
+
earliestMs = startedMs;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return earliest;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Whether any work has begun. A run with no jobs on hand is a run whose jobs were not fetched,
|
|
236
|
+
* which is no evidence that nothing started, so it counts as started rather than claiming a
|
|
237
|
+
* queue this surface cannot see.
|
|
238
|
+
*/
|
|
239
|
+
function workflowRunHasStarted({jobStatuses, firstStartedAt}: WorkflowRunProgress): boolean {
|
|
240
|
+
if (firstStartedAt != null) return true;
|
|
241
|
+
if (jobStatuses.length === 0) return true;
|
|
242
|
+
return jobStatuses.some((status) => status !== 'pending' && status !== 'skipped');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The single rule behind every run readout. An attempt's `startedAt` marks when the
|
|
247
|
+
* orchestrator picked the run up, not when work began, so a run whose jobs are all still
|
|
248
|
+
* waiting reads as `Queued` for queue time. Calling that "running for 2h" sends an operator to
|
|
249
|
+
* debug a build that never began.
|
|
250
|
+
*/
|
|
251
|
+
export function deriveWorkflowRunDisplay(progress: WorkflowRunProgress): WorkflowRunDisplay {
|
|
252
|
+
const {runStatus, startedAt, finishedAt, firstStartedAt} = progress;
|
|
253
|
+
const hasStarted = workflowRunHasStarted(progress);
|
|
254
|
+
const status = runStatus === 'running' && !hasStarted ? 'queued' : runStatus;
|
|
255
|
+
|
|
256
|
+
if (startedAt === null) return {status, duration: null};
|
|
257
|
+
|
|
258
|
+
// A run cancelled before anything started keeps its queue reading rather than reporting a
|
|
259
|
+
// run that never was.
|
|
260
|
+
const time = elapsedTimeFromTimestamps({from: firstStartedAt ?? startedAt, to: finishedAt});
|
|
261
|
+
if (time === null) return {status, duration: null};
|
|
262
|
+
|
|
263
|
+
return {status, duration: {kind: hasStarted ? 'run' : 'queue', ...time}};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** The run detail's reading: whole jobs, so queue and run time split on the first execution. */
|
|
267
|
+
export function workflowRunDetailDisplay(run: {
|
|
268
|
+
runAttempt: Pick<WorkflowRunAttemptSummary, 'status' | 'startedAt' | 'finishedAt'>;
|
|
269
|
+
jobs: Job[];
|
|
270
|
+
}): WorkflowRunDisplay {
|
|
271
|
+
return deriveWorkflowRunDisplay({
|
|
272
|
+
runStatus: run.runAttempt.status,
|
|
273
|
+
startedAt: run.runAttempt.startedAt,
|
|
274
|
+
finishedAt: run.runAttempt.finishedAt,
|
|
275
|
+
jobStatuses: run.jobs.map((job) => job.status),
|
|
276
|
+
firstStartedAt: workflowRunFirstStartedAt(run.jobs),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* The run list's reading: status counts cover every job, not just the drawn preview, so a row
|
|
282
|
+
* reaches the same verdict as the detail page without fetching a single timestamp more.
|
|
283
|
+
*/
|
|
284
|
+
export function workflowRunListItemDisplay(run: WorkflowRunListItem): WorkflowRunDisplay {
|
|
285
|
+
return deriveWorkflowRunDisplay({
|
|
286
|
+
runStatus: run.status,
|
|
287
|
+
startedAt: run.runAttempt.startedAt,
|
|
288
|
+
finishedAt: run.runAttempt.finishedAt,
|
|
289
|
+
jobStatuses: run.jobs.statusCounts.filter(({count}) => count > 0).map(({status}) => status),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The job the run is waiting on: the one queued longest without starting. Named only so a
|
|
295
|
+
* queued run says what it waits for instead of showing a number with no subject.
|
|
296
|
+
*/
|
|
297
|
+
export function workflowRunBlockingJob(jobs: Job[]): Job | null {
|
|
298
|
+
let blocking: Job | null = null;
|
|
299
|
+
let queuedMs = Number.POSITIVE_INFINITY;
|
|
300
|
+
|
|
301
|
+
for (const job of jobs) {
|
|
302
|
+
for (const {queuedAt, startedAt, finishedAt} of job.jobExecutions) {
|
|
303
|
+
if (queuedAt === null || startedAt !== null || finishedAt !== null) continue;
|
|
304
|
+
const candidateMs = new Date(queuedAt).getTime();
|
|
305
|
+
if (!Number.isFinite(candidateMs) || candidateMs >= queuedMs) continue;
|
|
306
|
+
blocking = job;
|
|
307
|
+
queuedMs = candidateMs;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return blocking;
|
|
312
|
+
}
|