@shipfox/client-workflows 19.0.0 → 21.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 +43 -0
- package/dist/components/job-detail/job-detail-view.d.ts.map +1 -1
- package/dist/components/job-detail/job-detail-view.js +231 -42
- package/dist/components/job-detail/job-detail-view.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/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 +24 -7
- package/dist/components/step-list/step-list.js.map +1 -1
- package/dist/components/workflow-run-duration-label.d.ts +8 -7
- package/dist/components/workflow-run-duration-label.d.ts.map +1 -1
- package/dist/components/workflow-run-duration-label.js +15 -31
- package/dist/components/workflow-run-duration-label.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-states.js +2 -2
- 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 +22 -16
- 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 +8 -35
- package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
- package/dist/components/workflow-run-view/run-workspace-nav.js +1 -1
- 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 +43 -36
- 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/components/workflow-status/status-visuals.d.ts.map +1 -1
- package/dist/components/workflow-status/status-visuals.js +0 -9
- package/dist/components/workflow-status/status-visuals.js.map +1 -1
- package/dist/components/workflow-status/workflow-status-icon.d.ts.map +1 -1
- package/dist/components/workflow-status/workflow-status-icon.js +0 -11
- package/dist/components/workflow-status/workflow-status-icon.js.map +1 -1
- package/dist/core/entities/workflow-run.d.ts +10 -55
- package/dist/core/entities/workflow-run.d.ts.map +1 -1
- package/dist/core/entities/workflow-run.js +1 -94
- package/dist/core/entities/workflow-run.js.map +1 -1
- package/dist/core/workflow-run.d.ts +2 -2
- package/dist/core/workflow-run.d.ts.map +1 -1
- package/dist/core/workflow-run.js +1 -1
- package/dist/core/workflow-run.js.map +1 -1
- package/dist/hooks/api/workflow-run-mapper.d.ts.map +1 -1
- package/dist/hooks/api/workflow-run-mapper.js +3 -1
- package/dist/hooks/api/workflow-run-mapper.js.map +1 -1
- package/dist/hooks/api/workflow-runs.js +1 -0
- package/dist/hooks/api/workflow-runs.js.map +1 -1
- package/dist/pages/project-workflows-page.d.ts.map +1 -1
- package/dist/pages/project-workflows-page.js +3 -2
- 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 +263 -47
- 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.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 +28 -9
- package/src/components/workflow-run-duration-label.tsx +21 -44
- 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-states.tsx +2 -2
- package/src/components/workflow-run-list/workflow-run-list-view.test.tsx +101 -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 +24 -19
- package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +2 -40
- package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +75 -22
- package/src/components/workflow-run-summary/workflow-run-summary.tsx +7 -31
- package/src/components/workflow-run-view/run-workspace-nav.tsx +1 -1
- package/src/components/workflow-run-view/workflow-run-states.tsx +1 -4
- package/src/components/workflow-run-view/workflow-run-view.stories.tsx +140 -0
- package/src/components/workflow-run-view/workflow-run-view.test.tsx +38 -7
- package/src/components/workflow-run-view/workflow-run-view.tsx +45 -37
- package/src/components/workflow-source-panel/workflow-source-content.tsx +2 -2
- package/src/components/workflow-status/status-visuals.ts +0 -4
- package/src/components/workflow-status/workflow-status-icon.stories.tsx +0 -1
- package/src/components/workflow-status/workflow-status-icon.tsx +0 -18
- package/src/core/entities/workflow-run.ts +9 -136
- package/src/core/workflow-run.test.ts +2 -0
- package/src/core/workflow-run.ts +0 -8
- package/src/hooks/api/workflow-run-mapper.ts +2 -0
- package/src/hooks/api/workflow-runs.ts +1 -1
- package/src/pages/project-workflows-page.tsx +3 -2
- 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/test/fixtures/workflow-run.ts +30 -3
- package/tsconfig.build.tsbuildinfo +1 -1
- package/src/core/entities/workflow-run-display.test.ts +0 -187
|
@@ -75,15 +75,15 @@ const SAMPLE_RUNS: WorkflowRunListItem[] = [
|
|
|
75
75
|
|
|
76
76
|
// One story on the full-width page. The data states (loading / empty / errors / runs) are
|
|
77
77
|
// driven by args; search, the filter menus, "clear filters" and "no matches" are live in the
|
|
78
|
-
// rendered toolbar since the view owns that state. The decorator gives the list
|
|
79
|
-
//
|
|
78
|
+
// rendered toolbar since the view owns that state. The decorator gives the list a full-width
|
|
79
|
+
// subtle canvas and a real height, so `flex-1` scrolls the way it does in the app shell.
|
|
80
80
|
const meta = {
|
|
81
81
|
title: 'Workflows/WorkflowRunList',
|
|
82
82
|
component: WorkflowRunListView,
|
|
83
83
|
parameters: {layout: 'centered'},
|
|
84
84
|
decorators: [
|
|
85
85
|
(Story) => (
|
|
86
|
-
<div className="flex h-600 w-
|
|
86
|
+
<div className="flex h-600 w-full bg-background-subtle-base p-24">
|
|
87
87
|
<Story />
|
|
88
88
|
</div>
|
|
89
89
|
),
|
|
@@ -137,27 +137,18 @@ export const NarrowLayout: Story = {
|
|
|
137
137
|
};
|
|
138
138
|
|
|
139
139
|
/**
|
|
140
|
-
* Row density against the three strip sizes the
|
|
141
|
-
* it,
|
|
140
|
+
* Row density against the three strip sizes the preview bound has to survive: comfortably under
|
|
141
|
+
* it, just over it, and far enough over that the overflow count carries the failure.
|
|
142
142
|
*/
|
|
143
143
|
export const JobStripDensity: Story = {
|
|
144
144
|
args: {
|
|
145
145
|
runs: [
|
|
146
146
|
makeRun('succeeded', 'two-jobs', 2, {jobs: ['succeeded', 'succeeded']}),
|
|
147
|
-
makeRun('failed', '
|
|
147
|
+
makeRun('failed', 'seventeen-jobs', 6, {
|
|
148
148
|
jobs: [
|
|
149
|
-
'succeeded',
|
|
150
|
-
'succeeded',
|
|
151
|
-
'succeeded',
|
|
149
|
+
...(Array.from({length: 12}, () => 'succeeded') as JobStatusDto[]),
|
|
152
150
|
'failed',
|
|
153
|
-
'
|
|
154
|
-
'succeeded',
|
|
155
|
-
'skipped',
|
|
156
|
-
'skipped',
|
|
157
|
-
'succeeded',
|
|
158
|
-
'succeeded',
|
|
159
|
-
'succeeded',
|
|
160
|
-
'succeeded',
|
|
151
|
+
...(Array.from({length: 4}, () => 'skipped') as JobStatusDto[]),
|
|
161
152
|
],
|
|
162
153
|
}),
|
|
163
154
|
makeRun('running', 'forty-jobs', 9, {
|
|
@@ -229,9 +220,7 @@ function StateExample({label, children}: {label: string; children: ReactNode}) {
|
|
|
229
220
|
<Code variant="label" className="text-foreground-neutral-subtle">
|
|
230
221
|
{label}
|
|
231
222
|
</Code>
|
|
232
|
-
<div className="flex h-560
|
|
233
|
-
{children}
|
|
234
|
-
</div>
|
|
223
|
+
<div className="flex h-560 min-w-0 bg-background-subtle-base p-16">{children}</div>
|
|
235
224
|
</div>
|
|
236
225
|
);
|
|
237
226
|
}
|
|
@@ -20,10 +20,9 @@ import {
|
|
|
20
20
|
workflowRunActor,
|
|
21
21
|
workflowRunBranchLabel,
|
|
22
22
|
workflowRunCommitLabel,
|
|
23
|
-
workflowRunListItemDisplay,
|
|
24
23
|
} from '#core/workflow-run.js';
|
|
25
24
|
import {withoutWorkflowRunSelectionSearch} from '#core/workflow-run-url-state.js';
|
|
26
|
-
import {JobStatusStrip, jobStatusSummary} from './job-status-strip.js';
|
|
25
|
+
import {JOB_STATUS_STRIP_WIDTH, JobStatusStrip, jobStatusSummary} from './job-status-strip.js';
|
|
27
26
|
|
|
28
27
|
export function WorkflowRunRowList({
|
|
29
28
|
runs,
|
|
@@ -35,10 +34,10 @@ export function WorkflowRunRowList({
|
|
|
35
34
|
projectSlug?: string | undefined;
|
|
36
35
|
}) {
|
|
37
36
|
return (
|
|
38
|
-
// A container, not the viewport, drives the row's breakpoints.
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
<ul className="@container divide-y divide-border-neutral-base
|
|
37
|
+
// A container, not the viewport, drives the row's breakpoints. What a row can afford is its
|
|
38
|
+
// own width; keying off the viewport would keep the job strip hidden on a wide screen or
|
|
39
|
+
// crush it on a narrow one.
|
|
40
|
+
<ul className="@container divide-y divide-border-neutral-base">
|
|
42
41
|
{runs.map((run) => (
|
|
43
42
|
<li key={run.id}>
|
|
44
43
|
<WorkflowRunRow run={run} workspaceSlug={workspaceSlug} projectSlug={projectSlug} />
|
|
@@ -56,9 +55,9 @@ export function WorkflowRunRowList({
|
|
|
56
55
|
* drops beneath. The numeric columns are fixed-width so duration and time form real columns
|
|
57
56
|
* down the list rather than tracking each row's name length.
|
|
58
57
|
*
|
|
59
|
-
* The
|
|
60
|
-
*
|
|
61
|
-
*
|
|
58
|
+
* The 976px threshold keeps identity and provenance on one line; below it, provenance drops
|
|
59
|
+
* beneath the identity. The 1200px threshold keeps the full job preview and numeric columns
|
|
60
|
+
* readable together; below it, the strip is hidden so the numeric columns remain aligned.
|
|
62
61
|
*/
|
|
63
62
|
export function WorkflowRunRow({
|
|
64
63
|
run,
|
|
@@ -69,9 +68,11 @@ export function WorkflowRunRow({
|
|
|
69
68
|
workspaceSlug?: string | undefined;
|
|
70
69
|
projectSlug?: string | undefined;
|
|
71
70
|
}) {
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
const
|
|
71
|
+
const duration = run.runAttempt.displayDuration;
|
|
72
|
+
const hasStarted = run.jobs.hasStartedJobExecution;
|
|
73
|
+
const durationLabel = useWorkflowRunDurationAccessibleLabel(duration, hasStarted);
|
|
74
|
+
const status = run.runAttempt.status;
|
|
75
|
+
const statusLabel = getWorkflowStatusVisual(status).label;
|
|
75
76
|
const runNumberLabel = formatWorkflowRunNumberLabel(run);
|
|
76
77
|
const branch = workflowRunBranchLabel(run);
|
|
77
78
|
const commit = workflowRunCommitLabel(run);
|
|
@@ -79,7 +80,7 @@ export function WorkflowRunRow({
|
|
|
79
80
|
|
|
80
81
|
const body = (
|
|
81
82
|
<>
|
|
82
|
-
<WorkflowStatusIcon status={
|
|
83
|
+
<WorkflowStatusIcon status={status} size={14} className="shrink-0" />
|
|
83
84
|
|
|
84
85
|
<div className="flex min-w-0 flex-1 flex-col gap-tight @min-[976px]:flex-row @min-[976px]:items-center @min-[976px]:gap-cluster">
|
|
85
86
|
<span className="flex min-w-0 items-center gap-inline @min-[976px]:flex-1">
|
|
@@ -98,14 +99,18 @@ export function WorkflowRunRow({
|
|
|
98
99
|
</div>
|
|
99
100
|
|
|
100
101
|
<span className="flex shrink-0 items-center gap-cluster">
|
|
101
|
-
{/*
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
{/* Reserve the API preview's full width so the overflow count cannot paint over the
|
|
103
|
+
duration, and keep it even when a run has no jobs planned yet so the numerics stay in
|
|
104
|
+
line down the list instead of stepping in and out. The max-content width expands if
|
|
105
|
+
the payload ever exceeds the current API preview bound. */}
|
|
106
|
+
<span
|
|
107
|
+
className="hidden w-max shrink-0 @min-[1200px]:flex"
|
|
108
|
+
style={{minWidth: JOB_STATUS_STRIP_WIDTH}}
|
|
109
|
+
>
|
|
105
110
|
<JobStatusStrip jobs={run.jobs} />
|
|
106
111
|
</span>
|
|
107
112
|
<span className="flex w-64 justify-end">
|
|
108
|
-
<WorkflowRunDurationLabel duration={
|
|
113
|
+
<WorkflowRunDurationLabel duration={duration} hasStarted={hasStarted} />
|
|
109
114
|
</span>
|
|
110
115
|
<Code
|
|
111
116
|
variant="label"
|
|
@@ -120,7 +125,7 @@ export function WorkflowRunRow({
|
|
|
120
125
|
// Rows run edge to edge inside the list's scroll container, which would clip the standard
|
|
121
126
|
// outset focus ring, so this one is inset per the design system's focus-ring rule.
|
|
122
127
|
const rowClassName =
|
|
123
|
-
'flex w-full min-w-0 items-center gap-inline px-row py-row text-left transition-colors hover:bg-background-
|
|
128
|
+
'flex w-full min-w-0 items-center gap-inline px-row py-row text-left transition-colors hover:bg-background-neutral-hover focus-visible:shadow-[inset_0_0_0_2px_var(--color-primary-500)] focus-visible:outline-none @min-[976px]:h-44 @min-[976px]:py-0';
|
|
124
129
|
|
|
125
130
|
// Optimistic manual runs (temp-<uuid>) have no detail page until the canonical row
|
|
126
131
|
// replaces them on the next poll, so they render non-interactively instead of as a link
|
|
@@ -18,7 +18,6 @@ import {WorkflowRunAttempt, type WorkflowRunStatus} from '#core/workflow-run.js'
|
|
|
18
18
|
import {
|
|
19
19
|
runAttemptsResponseDto,
|
|
20
20
|
workflowJobDto,
|
|
21
|
-
workflowJobExecutionDto,
|
|
22
21
|
workflowRunAttemptDto,
|
|
23
22
|
workflowRunDetail,
|
|
24
23
|
} from '#test/fixtures/workflow-run.js';
|
|
@@ -57,10 +56,10 @@ const RUN_ATTEMPTS_RESPONSE = runAttemptsResponseDto({
|
|
|
57
56
|
});
|
|
58
57
|
|
|
59
58
|
const withFrame: Decorator = (Story) => (
|
|
60
|
-
<div className="min-h-screen bg-background-
|
|
59
|
+
<div className="min-h-screen bg-background-subtle-base">
|
|
61
60
|
<div className="mx-auto flex min-h-screen w-full max-w-[1120px] flex-col overflow-hidden border-x border-border-neutral-base bg-background-subtle-base">
|
|
62
61
|
<Story />
|
|
63
|
-
<div className="min-h-0 flex-1 bg-background-
|
|
62
|
+
<div className="min-h-0 flex-1 bg-background-subtle-base p-16" />
|
|
64
63
|
</div>
|
|
65
64
|
</div>
|
|
66
65
|
);
|
|
@@ -203,19 +202,6 @@ export const Durations: Story = {
|
|
|
203
202
|
),
|
|
204
203
|
};
|
|
205
204
|
|
|
206
|
-
/**
|
|
207
|
-
* A live attempt whose jobs have not started. The header reports the wait rather than a run
|
|
208
|
-
* that never began, and names the job the run is blocked on once there is more than one.
|
|
209
|
-
*/
|
|
210
|
-
export const Queued: Story = {
|
|
211
|
-
render: () => (
|
|
212
|
-
<div className="flex flex-col">
|
|
213
|
-
<WorkflowRunSummary run={queuedRun('release-queued', ['build'])} />
|
|
214
|
-
<WorkflowRunSummary run={queuedRun('release-queued-fan-out', ['build', 'lint', 'test'])} />
|
|
215
|
-
</div>
|
|
216
|
-
),
|
|
217
|
-
};
|
|
218
|
-
|
|
219
205
|
export const Cancellable: Story = {
|
|
220
206
|
args: {
|
|
221
207
|
run: workflowRunDetail({status: 'running'}),
|
|
@@ -223,30 +209,6 @@ export const Cancellable: Story = {
|
|
|
223
209
|
},
|
|
224
210
|
};
|
|
225
211
|
|
|
226
|
-
function queuedRun(name: string, jobNames: string[]) {
|
|
227
|
-
return workflowRunDetail({
|
|
228
|
-
status: 'running',
|
|
229
|
-
name,
|
|
230
|
-
run_attempt: workflowRunAttemptDto({
|
|
231
|
-
status: 'running',
|
|
232
|
-
created_at: RUN_STARTED_AT,
|
|
233
|
-
started_at: RUN_STARTED_AT,
|
|
234
|
-
finished_at: null,
|
|
235
|
-
}),
|
|
236
|
-
jobs: jobNames.map((jobName, index) =>
|
|
237
|
-
workflowJobDto({
|
|
238
|
-
name: jobName,
|
|
239
|
-
status: 'pending',
|
|
240
|
-
job_executions: [
|
|
241
|
-
workflowJobExecutionDto({
|
|
242
|
-
queued_at: new Date(Date.parse(RUN_STARTED_AT) + index * 1000).toISOString(),
|
|
243
|
-
}),
|
|
244
|
-
],
|
|
245
|
-
}),
|
|
246
|
-
),
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
|
|
250
212
|
export const Cancelling: Story = {
|
|
251
213
|
args: {
|
|
252
214
|
run: workflowRunDetail({status: 'running'}),
|
|
@@ -38,7 +38,8 @@ describe('WorkflowRunSummary', () => {
|
|
|
38
38
|
|
|
39
39
|
const summary = await screen.findByRole('region', {name: 'deploy-web'});
|
|
40
40
|
|
|
41
|
-
expect(summary).toHaveClass('
|
|
41
|
+
expect(summary).toHaveClass('px-row', 'py-row');
|
|
42
|
+
expect(summary).not.toHaveClass('bg-background-subtle-base');
|
|
42
43
|
expect(summary.firstElementChild).not.toHaveClass('max-w-[1120px]');
|
|
43
44
|
expect(within(summary).getByRole('heading', {name: 'deploy-web'})).toBeInTheDocument();
|
|
44
45
|
expect(within(summary).getAllByText('Running')).not.toHaveLength(0);
|
|
@@ -159,6 +160,17 @@ describe('WorkflowRunSummary', () => {
|
|
|
159
160
|
test('shows a live selected attempt duration for running runs', async () => {
|
|
160
161
|
vi.spyOn(Date, 'now').mockReturnValue(Date.parse('2026-05-07T01:02:14.000Z'));
|
|
161
162
|
renderSummary({
|
|
163
|
+
jobs: [
|
|
164
|
+
workflowJobDto({
|
|
165
|
+
status: 'running',
|
|
166
|
+
job_executions: [
|
|
167
|
+
workflowJobExecutionDto({
|
|
168
|
+
status: 'running',
|
|
169
|
+
started_at: '2026-05-07T01:02:00.000Z',
|
|
170
|
+
}),
|
|
171
|
+
],
|
|
172
|
+
}),
|
|
173
|
+
],
|
|
162
174
|
run_attempt: {
|
|
163
175
|
id: '11111111-1111-4111-8111-000000000001',
|
|
164
176
|
workflow_run_id: RUN_ID,
|
|
@@ -178,7 +190,9 @@ describe('WorkflowRunSummary', () => {
|
|
|
178
190
|
expect(duration).toHaveAttribute('aria-label', 'running 2m 14s');
|
|
179
191
|
});
|
|
180
192
|
|
|
181
|
-
|
|
193
|
+
// The header reports the attempt the API returned. Whether a job has reached a runner yet is
|
|
194
|
+
// the job surfaces' business, so an attempt whose jobs are all pending still reads as running.
|
|
195
|
+
test('reads a running attempt as running whatever its jobs have reached', async () => {
|
|
182
196
|
vi.spyOn(Date, 'now').mockReturnValue(Date.parse('2026-05-07T01:02:14.000Z'));
|
|
183
197
|
renderSummary({
|
|
184
198
|
jobs: [workflowJobDto({status: 'pending'})],
|
|
@@ -187,50 +201,89 @@ describe('WorkflowRunSummary', () => {
|
|
|
187
201
|
|
|
188
202
|
const summary = await screen.findByRole('region', {name: 'deploy-web'});
|
|
189
203
|
|
|
190
|
-
expect(within(summary).getAllByText('
|
|
191
|
-
expect(within(summary).queryByText('
|
|
192
|
-
expect(within(summary).getByText('2m 14s')).toHaveAttribute('aria-label', '
|
|
204
|
+
expect(within(summary).getAllByText('Running')).not.toHaveLength(0);
|
|
205
|
+
expect(within(summary).queryByText('Queued')).not.toBeInTheDocument();
|
|
206
|
+
expect(within(summary).getByText('2m 14s')).toHaveAttribute('aria-label', 'running 2m 14s');
|
|
193
207
|
});
|
|
194
208
|
|
|
195
|
-
test('
|
|
209
|
+
test('uses a neutral verb when an attempt finished before any job execution started', async () => {
|
|
196
210
|
renderSummary({
|
|
211
|
+
status: 'cancelled',
|
|
197
212
|
jobs: [
|
|
198
213
|
workflowJobDto({
|
|
199
|
-
name: 'lint',
|
|
200
214
|
status: 'pending',
|
|
201
215
|
job_executions: [workflowJobExecutionDto({queued_at: '2026-05-07T01:00:00.000Z'})],
|
|
202
216
|
}),
|
|
203
|
-
workflowJobDto({
|
|
204
|
-
name: 'build',
|
|
205
|
-
status: 'pending',
|
|
206
|
-
job_executions: [workflowJobExecutionDto({queued_at: '2026-05-07T00:59:00.000Z'})],
|
|
207
|
-
}),
|
|
208
217
|
],
|
|
209
|
-
run_attempt:
|
|
218
|
+
run_attempt: {
|
|
219
|
+
id: '11111111-1111-4111-8111-000000000001',
|
|
220
|
+
workflow_run_id: RUN_ID,
|
|
221
|
+
attempt: 1,
|
|
222
|
+
status: 'cancelled',
|
|
223
|
+
created_at: '2026-05-07T01:00:00.000Z',
|
|
224
|
+
started_at: '2026-05-07T01:00:00.000Z',
|
|
225
|
+
finished_at: '2026-05-07T01:02:14.000Z',
|
|
226
|
+
rerun_mode: null,
|
|
227
|
+
},
|
|
210
228
|
});
|
|
211
229
|
|
|
212
230
|
const summary = await screen.findByRole('region', {name: 'deploy-web'});
|
|
213
231
|
|
|
214
|
-
expect(within(summary).
|
|
215
|
-
expect(within(summary).getByText('
|
|
232
|
+
expect(within(summary).getAllByText('Cancelled')).not.toHaveLength(0);
|
|
233
|
+
expect(within(summary).getByText('2m 14s')).toHaveAttribute('aria-label', 'lasted 2m 14s');
|
|
216
234
|
});
|
|
217
235
|
|
|
218
|
-
test('
|
|
236
|
+
test('uses the run verb when a cancelled job execution started', async () => {
|
|
219
237
|
renderSummary({
|
|
238
|
+
status: 'cancelled',
|
|
220
239
|
jobs: [
|
|
221
240
|
workflowJobDto({
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
241
|
+
status: 'cancelled',
|
|
242
|
+
job_executions: [
|
|
243
|
+
workflowJobExecutionDto({
|
|
244
|
+
status: 'cancelled',
|
|
245
|
+
started_at: '2026-05-07T01:00:05.000Z',
|
|
246
|
+
}),
|
|
247
|
+
],
|
|
225
248
|
}),
|
|
226
249
|
],
|
|
227
|
-
run_attempt:
|
|
250
|
+
run_attempt: {
|
|
251
|
+
id: '11111111-1111-4111-8111-000000000001',
|
|
252
|
+
workflow_run_id: RUN_ID,
|
|
253
|
+
attempt: 1,
|
|
254
|
+
status: 'cancelled',
|
|
255
|
+
created_at: '2026-05-07T01:00:00.000Z',
|
|
256
|
+
started_at: '2026-05-07T01:00:00.000Z',
|
|
257
|
+
finished_at: '2026-05-07T01:02:14.000Z',
|
|
258
|
+
rerun_mode: null,
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const summary = await screen.findByRole('region', {name: 'deploy-web'});
|
|
263
|
+
|
|
264
|
+
expect(within(summary).getByText('2m 14s')).toHaveAttribute('aria-label', 'ran 2m 14s');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('uses a neutral verb when every job was skipped', async () => {
|
|
268
|
+
renderSummary({
|
|
269
|
+
status: 'succeeded',
|
|
270
|
+
jobs: [workflowJobDto({status: 'skipped'})],
|
|
271
|
+
run_attempt: {
|
|
272
|
+
id: '11111111-1111-4111-8111-000000000001',
|
|
273
|
+
workflow_run_id: RUN_ID,
|
|
274
|
+
attempt: 1,
|
|
275
|
+
status: 'succeeded',
|
|
276
|
+
created_at: '2026-05-07T01:00:00.000Z',
|
|
277
|
+
started_at: '2026-05-07T01:00:00.000Z',
|
|
278
|
+
finished_at: '2026-05-07T01:02:14.000Z',
|
|
279
|
+
rerun_mode: null,
|
|
280
|
+
},
|
|
228
281
|
});
|
|
229
282
|
|
|
230
283
|
const summary = await screen.findByRole('region', {name: 'deploy-web'});
|
|
231
284
|
|
|
232
|
-
expect(within(summary).getAllByText('
|
|
233
|
-
expect(within(summary).
|
|
285
|
+
expect(within(summary).getAllByText('Succeeded')).not.toHaveLength(0);
|
|
286
|
+
expect(within(summary).getByText('2m 14s')).toHaveAttribute('aria-label', 'lasted 2m 14s');
|
|
234
287
|
});
|
|
235
288
|
|
|
236
289
|
test('does not render a whole-run source control', async () => {
|
|
@@ -12,7 +12,7 @@ import {useIsTextTruncated} from '@shipfox/react-ui/hooks';
|
|
|
12
12
|
import {RelativeTime} from '@shipfox/react-ui/relative-time';
|
|
13
13
|
import {TimeTickerProvider} from '@shipfox/react-ui/time-ticker';
|
|
14
14
|
import {Tooltip, TooltipContent, TooltipTrigger} from '@shipfox/react-ui/tooltip';
|
|
15
|
-
import {
|
|
15
|
+
import {Header, Text} from '@shipfox/react-ui/typography';
|
|
16
16
|
import {useId} from 'react';
|
|
17
17
|
import {WorkflowRunNumberLabel} from '#components/workflow-run-number-label.js';
|
|
18
18
|
import {
|
|
@@ -21,17 +21,13 @@ import {
|
|
|
21
21
|
WORKFLOW_RUN_STATUSES,
|
|
22
22
|
type WorkflowRunDetail,
|
|
23
23
|
type WorkflowRunRerunMode,
|
|
24
|
-
workflowRunBlockingJob,
|
|
25
|
-
workflowRunDetailDisplay,
|
|
26
24
|
} from '#core/workflow-run.js';
|
|
27
25
|
import {WorkflowRunDurationLabel} from '../workflow-run-duration-label.js';
|
|
28
26
|
import {getWorkflowStatusVisual} from '../workflow-status/status-visuals.js';
|
|
29
27
|
import {WorkflowRunAttemptSwitcher} from './workflow-run-attempt-switcher.js';
|
|
30
28
|
|
|
31
29
|
const STATUS_BADGE_LABEL_WIDTH_CH = Math.max(
|
|
32
|
-
...
|
|
33
|
-
(status) => getWorkflowStatusVisual(status).label.length,
|
|
34
|
-
),
|
|
30
|
+
...WORKFLOW_RUN_STATUSES.map((status) => getWorkflowStatusVisual(status).label.length),
|
|
35
31
|
);
|
|
36
32
|
const RERUN_BUTTON_SURFACE_CLASS_NAME =
|
|
37
33
|
'bg-background-neutral-base hover:bg-background-neutral-hover active:bg-background-neutral-pressed';
|
|
@@ -60,28 +56,21 @@ export function WorkflowRunSummary({
|
|
|
60
56
|
latestAttempt,
|
|
61
57
|
}: WorkflowRunSummaryProps) {
|
|
62
58
|
const headingId = useId();
|
|
63
|
-
const
|
|
64
|
-
const status = getWorkflowStatusVisual(display.status);
|
|
59
|
+
const status = getWorkflowStatusVisual(run.runAttempt.status);
|
|
65
60
|
const action = workflowRunActionForRun(run);
|
|
66
61
|
const hasAction = canRenderWorkflowRunAction(action, onCancel, onRerun);
|
|
67
62
|
const attemptSwitcher =
|
|
68
63
|
latestAttempt && latestAttempt > 1 && workspaceSlug && projectSlug
|
|
69
64
|
? {workspaceSlug, projectSlug, latestAttempt}
|
|
70
65
|
: null;
|
|
71
|
-
const displayDuration =
|
|
72
|
-
|
|
73
|
-
// and the graph already say which one it is.
|
|
74
|
-
const blockingJob =
|
|
75
|
-
display.status === 'queued' && run.jobs.length > 1 ? workflowRunBlockingJob(run.jobs) : null;
|
|
66
|
+
const displayDuration = run.runAttempt.displayDuration;
|
|
67
|
+
const hasStarted = run.hasStartedJobExecution;
|
|
76
68
|
const {ref: headingTextRef, isTruncated: isHeadingTruncated} =
|
|
77
69
|
useIsTextTruncated<HTMLSpanElement>(run.name);
|
|
78
70
|
|
|
79
71
|
return (
|
|
80
72
|
<TimeTickerProvider intervalMs={1000} reducedMotionIntervalMs={10_000}>
|
|
81
|
-
<section
|
|
82
|
-
aria-labelledby={headingId}
|
|
83
|
-
className="bg-background-neutral-background px-row py-row"
|
|
84
|
-
>
|
|
73
|
+
<section aria-labelledby={headingId} className="px-row py-row">
|
|
85
74
|
<div className="grid min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-x-cluster gap-y-inline overflow-hidden max-[480px]:grid-cols-1">
|
|
86
75
|
<div className="col-start-1 row-start-1 min-w-0 max-[480px]:col-start-auto max-[480px]:row-start-auto">
|
|
87
76
|
<div className="flex min-w-0 items-center gap-inline">
|
|
@@ -184,24 +173,11 @@ export function WorkflowRunSummary({
|
|
|
184
173
|
<MetadataSeparator />
|
|
185
174
|
<WorkflowRunDurationLabel
|
|
186
175
|
duration={displayDuration}
|
|
176
|
+
hasStarted={hasStarted}
|
|
187
177
|
className="text-foreground-neutral-subtle"
|
|
188
178
|
/>
|
|
189
179
|
</>
|
|
190
180
|
) : null}
|
|
191
|
-
|
|
192
|
-
{blockingJob ? (
|
|
193
|
-
<>
|
|
194
|
-
<MetadataSeparator />
|
|
195
|
-
<span className="flex min-w-0 items-center gap-tight">
|
|
196
|
-
<Text as="span" size="xs" className="shrink-0">
|
|
197
|
-
waiting on
|
|
198
|
-
</Text>
|
|
199
|
-
<Code as="span" variant="label" className="min-w-0 truncate">
|
|
200
|
-
{blockingJob.displayName}
|
|
201
|
-
</Code>
|
|
202
|
-
</span>
|
|
203
|
-
</>
|
|
204
|
-
) : null}
|
|
205
181
|
</div>
|
|
206
182
|
</div>
|
|
207
183
|
</section>
|
|
@@ -53,7 +53,7 @@ export function RunWorkspaceNav({
|
|
|
53
53
|
|
|
54
54
|
return (
|
|
55
55
|
<TimeTickerProvider intervalMs={1000} reducedMotionIntervalMs={10_000}>
|
|
56
|
-
<aside className="flex min-h-0 w-full shrink-0 flex-col border-b border-border-neutral-base
|
|
56
|
+
<aside className="flex min-h-0 w-full shrink-0 flex-col border-b border-border-neutral-base min-[768px]:w-240 min-[768px]:border-b-0 min-[768px]:border-r">
|
|
57
57
|
<Collapsible
|
|
58
58
|
open={mobileOpen}
|
|
59
59
|
onOpenChange={setMobileOpen}
|
|
@@ -7,10 +7,7 @@ import {Text} from '@shipfox/react-ui/typography';
|
|
|
7
7
|
|
|
8
8
|
export function WorkflowRunSkeleton() {
|
|
9
9
|
return (
|
|
10
|
-
<section
|
|
11
|
-
aria-label="Loading workflow run"
|
|
12
|
-
className="bg-background-neutral-background px-row py-row"
|
|
13
|
-
>
|
|
10
|
+
<section aria-label="Loading workflow run" className="px-row py-row">
|
|
14
11
|
<div className="flex min-w-0 flex-wrap items-center gap-x-cluster gap-y-inline">
|
|
15
12
|
<div className="flex min-w-0 items-center gap-inline">
|
|
16
13
|
<Skeleton className="size-8 rounded-full" />
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type {WorkflowRunDetailResponseDto} from '@shipfox/api-workflows-dto';
|
|
2
|
+
import {configureApiClient} from '@shipfox/client-api';
|
|
3
|
+
import type {Decorator, Meta, StoryObj} from '@storybook/react';
|
|
4
|
+
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
|
|
5
|
+
import {type ReactNode, useEffect, useState} from 'react';
|
|
6
|
+
import {
|
|
7
|
+
runAttemptsResponseDto,
|
|
8
|
+
workflowJobDto,
|
|
9
|
+
workflowRunDetailDto,
|
|
10
|
+
} from '#test/fixtures/workflow-run.js';
|
|
11
|
+
import {WorkflowRunView} from './workflow-run-view.js';
|
|
12
|
+
|
|
13
|
+
const PROJECT_ID = '22222222-2222-4222-8222-222222222222';
|
|
14
|
+
const RUN_ID = '11111111-1111-4111-8111-111111111111';
|
|
15
|
+
const RUN_STARTED_AT = '2026-06-26T11:57:00.000Z';
|
|
16
|
+
const RUN_FINISHED_AT = '2026-06-26T11:59:00.000Z';
|
|
17
|
+
|
|
18
|
+
const RUN_RESPONSE: WorkflowRunDetailResponseDto = workflowRunDetailDto({
|
|
19
|
+
id: RUN_ID,
|
|
20
|
+
project_id: PROJECT_ID,
|
|
21
|
+
name: 'deploy-web',
|
|
22
|
+
status: 'succeeded',
|
|
23
|
+
started_at: RUN_STARTED_AT,
|
|
24
|
+
finished_at: RUN_FINISHED_AT,
|
|
25
|
+
has_started_job_execution: true,
|
|
26
|
+
jobs: [
|
|
27
|
+
workflowJobDto({
|
|
28
|
+
key: 'build',
|
|
29
|
+
name: 'build',
|
|
30
|
+
status: 'succeeded',
|
|
31
|
+
position: 0,
|
|
32
|
+
}),
|
|
33
|
+
workflowJobDto({
|
|
34
|
+
key: 'deploy',
|
|
35
|
+
name: 'deploy',
|
|
36
|
+
status: 'succeeded',
|
|
37
|
+
position: 1,
|
|
38
|
+
dependencies: ['build'],
|
|
39
|
+
}),
|
|
40
|
+
workflowJobDto({
|
|
41
|
+
key: 'notify',
|
|
42
|
+
name: 'notify',
|
|
43
|
+
status: 'succeeded',
|
|
44
|
+
position: 2,
|
|
45
|
+
dependencies: ['deploy'],
|
|
46
|
+
}),
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
const RUN_ATTEMPTS_RESPONSE = runAttemptsResponseDto({attempts: []});
|
|
50
|
+
|
|
51
|
+
const withRunApi: Decorator = (Story) => (
|
|
52
|
+
<RunWorkspaceStoryProviders>
|
|
53
|
+
<Story />
|
|
54
|
+
</RunWorkspaceStoryProviders>
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const meta = {
|
|
58
|
+
title: 'Workflows/WorkflowRunView',
|
|
59
|
+
component: WorkflowRunView,
|
|
60
|
+
parameters: {
|
|
61
|
+
layout: 'fullscreen',
|
|
62
|
+
viewport: {
|
|
63
|
+
defaultViewport: 'wide',
|
|
64
|
+
options: {
|
|
65
|
+
wide: {
|
|
66
|
+
name: 'Wide',
|
|
67
|
+
styles: {width: '1440px', height: '720px'},
|
|
68
|
+
type: 'desktop',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
globals: {
|
|
73
|
+
viewport: {value: 'wide'},
|
|
74
|
+
},
|
|
75
|
+
argos: {
|
|
76
|
+
modes: {dark: {theme: 'dark'}},
|
|
77
|
+
fitToContent: false,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
decorators: [withRunApi],
|
|
81
|
+
args: {
|
|
82
|
+
projectId: PROJECT_ID,
|
|
83
|
+
workspaceSlug: 'acme',
|
|
84
|
+
projectSlug: 'platform',
|
|
85
|
+
workflowRunId: RUN_ID,
|
|
86
|
+
},
|
|
87
|
+
} satisfies Meta<typeof WorkflowRunView>;
|
|
88
|
+
|
|
89
|
+
export default meta;
|
|
90
|
+
type Story = StoryObj<typeof meta>;
|
|
91
|
+
|
|
92
|
+
export const WideViewport: Story = {};
|
|
93
|
+
|
|
94
|
+
function RunWorkspaceStoryProviders({children}: {children: ReactNode}) {
|
|
95
|
+
const [queryClient] = useState(
|
|
96
|
+
() => new QueryClient({defaultOptions: {queries: {retry: false}}}),
|
|
97
|
+
);
|
|
98
|
+
const [configured, setConfigured] = useState(false);
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
configureApiClient({
|
|
102
|
+
baseUrl: 'https://api.example.test',
|
|
103
|
+
fetchImpl: (input) => {
|
|
104
|
+
const url =
|
|
105
|
+
input instanceof Request ? input.url : input instanceof URL ? input.href : String(input);
|
|
106
|
+
const path = new URL(url, 'https://api.example.test').pathname;
|
|
107
|
+
const response =
|
|
108
|
+
path === '/annotations'
|
|
109
|
+
? {
|
|
110
|
+
body: {annotations: [], has_more: false, next_cursor: null},
|
|
111
|
+
status: 200,
|
|
112
|
+
}
|
|
113
|
+
: path === `/workflows/runs/${RUN_ID}/attempts`
|
|
114
|
+
? {body: RUN_ATTEMPTS_RESPONSE, status: 200}
|
|
115
|
+
: path === `/workflows/runs/${RUN_ID}`
|
|
116
|
+
? {body: RUN_RESPONSE, status: 200}
|
|
117
|
+
: {body: {code: 'not-found'}, status: 404};
|
|
118
|
+
return new Response(JSON.stringify(response.body), {
|
|
119
|
+
status: response.status,
|
|
120
|
+
headers: {'content-type': 'application/json'},
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
setConfigured(true);
|
|
125
|
+
|
|
126
|
+
return () => {
|
|
127
|
+
configureApiClient({baseUrl: '', fetchImpl: undefined});
|
|
128
|
+
};
|
|
129
|
+
}, []);
|
|
130
|
+
|
|
131
|
+
if (!configured) return null;
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<QueryClientProvider client={queryClient}>
|
|
135
|
+
<div className="flex h-[720px] min-w-[1440px] w-[1440px] bg-background-subtle-base">
|
|
136
|
+
{children}
|
|
137
|
+
</div>
|
|
138
|
+
</QueryClientProvider>
|
|
139
|
+
);
|
|
140
|
+
}
|