@shipfox/client-workflows 19.0.0 → 20.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.
Files changed (50) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +14 -0
  3. package/dist/components/workflow-run-duration-label.d.ts +8 -7
  4. package/dist/components/workflow-run-duration-label.d.ts.map +1 -1
  5. package/dist/components/workflow-run-duration-label.js +15 -31
  6. package/dist/components/workflow-run-duration-label.js.map +1 -1
  7. package/dist/components/workflow-run-list/workflow-run-row.d.ts.map +1 -1
  8. package/dist/components/workflow-run-list/workflow-run-row.js +9 -6
  9. package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
  10. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
  11. package/dist/components/workflow-run-summary/workflow-run-summary.js +7 -34
  12. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
  13. package/dist/components/workflow-status/status-visuals.d.ts.map +1 -1
  14. package/dist/components/workflow-status/status-visuals.js +0 -9
  15. package/dist/components/workflow-status/status-visuals.js.map +1 -1
  16. package/dist/components/workflow-status/workflow-status-icon.d.ts.map +1 -1
  17. package/dist/components/workflow-status/workflow-status-icon.js +0 -11
  18. package/dist/components/workflow-status/workflow-status-icon.js.map +1 -1
  19. package/dist/core/entities/workflow-run.d.ts +10 -55
  20. package/dist/core/entities/workflow-run.d.ts.map +1 -1
  21. package/dist/core/entities/workflow-run.js +1 -94
  22. package/dist/core/entities/workflow-run.js.map +1 -1
  23. package/dist/core/workflow-run.d.ts +2 -2
  24. package/dist/core/workflow-run.d.ts.map +1 -1
  25. package/dist/core/workflow-run.js +1 -1
  26. package/dist/core/workflow-run.js.map +1 -1
  27. package/dist/hooks/api/workflow-run-mapper.d.ts.map +1 -1
  28. package/dist/hooks/api/workflow-run-mapper.js +3 -1
  29. package/dist/hooks/api/workflow-run-mapper.js.map +1 -1
  30. package/dist/hooks/api/workflow-runs.js +1 -0
  31. package/dist/hooks/api/workflow-runs.js.map +1 -1
  32. package/dist/tsconfig.test.tsbuildinfo +1 -1
  33. package/package.json +2 -2
  34. package/src/components/workflow-run-duration-label.tsx +21 -44
  35. package/src/components/workflow-run-list/workflow-run-list-view.test.tsx +48 -0
  36. package/src/components/workflow-run-list/workflow-run-row.tsx +7 -6
  37. package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +0 -38
  38. package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +73 -21
  39. package/src/components/workflow-run-summary/workflow-run-summary.tsx +6 -27
  40. package/src/components/workflow-status/status-visuals.ts +0 -4
  41. package/src/components/workflow-status/workflow-status-icon.stories.tsx +0 -1
  42. package/src/components/workflow-status/workflow-status-icon.tsx +0 -18
  43. package/src/core/entities/workflow-run.ts +9 -136
  44. package/src/core/workflow-run.test.ts +2 -0
  45. package/src/core/workflow-run.ts +0 -8
  46. package/src/hooks/api/workflow-run-mapper.ts +2 -0
  47. package/src/hooks/api/workflow-runs.ts +1 -1
  48. package/test/fixtures/workflow-run.ts +30 -3
  49. package/tsconfig.build.tsbuildinfo +1 -1
  50. package/src/core/entities/workflow-run-display.test.ts +0 -187
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/client-workflows",
3
3
  "license": "MIT",
4
- "version": "19.0.0",
4
+ "version": "20.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -29,7 +29,7 @@
29
29
  "date-fns": "^4.1.0",
30
30
  "@shipfox/annotations-dto": "12.3.0",
31
31
  "@shipfox/api-definitions-dto": "12.3.0",
32
- "@shipfox/api-workflows-dto": "12.6.0",
32
+ "@shipfox/api-workflows-dto": "12.7.0",
33
33
  "@shipfox/client-api": "6.0.1",
34
34
  "@shipfox/client-logs": "17.0.0",
35
35
  "@shipfox/client-projects": "17.0.0",
@@ -2,50 +2,37 @@ import {Icon} from '@shipfox/react-ui/icon';
2
2
  import {useTimeTick} from '@shipfox/react-ui/time-ticker';
3
3
  import {Code} from '@shipfox/react-ui/typography';
4
4
  import {cn, humanDuration} from '@shipfox/react-ui/utils';
5
- import type {
6
- WorkflowRunAttemptDisplayDuration,
7
- WorkflowRunDisplayDuration,
8
- } from '#core/workflow-run.js';
5
+ import type {WorkflowRunAttemptDisplayDuration} from '#core/workflow-run.js';
9
6
 
10
7
  /**
11
- * The run list reads an attempt's own marks and cannot tell queue from run, so an unkinded
12
- * duration still renders as run time. Only surfaces holding the run's jobs pass a kind.
8
+ * A run reports one span: how long its attempt has been under way. Queueing is a job
9
+ * execution's own affair, and the job surfaces report it there.
13
10
  */
14
- export type WorkflowRunDurationDisplay =
15
- | WorkflowRunAttemptDisplayDuration
16
- | WorkflowRunDisplayDuration;
17
-
18
- type DurationKind = 'queue' | 'run';
19
-
20
- const GLYPH_BY_KIND = {queue: 'hourglassLine', run: 'timerLine'} as const;
21
- const LIVE_VERB_BY_KIND = {queue: 'queued', run: 'running'} as const;
22
- const FIXED_VERB_BY_KIND = {queue: 'queued', run: 'ran'} as const;
11
+ export type WorkflowRunDurationDisplay = WorkflowRunAttemptDisplayDuration;
23
12
 
24
13
  export function WorkflowRunDurationLabel({
25
14
  duration,
15
+ hasStarted,
26
16
  className,
27
17
  }: {
28
18
  duration: WorkflowRunDurationDisplay | null;
19
+ hasStarted: boolean;
29
20
  className?: string | undefined;
30
21
  }) {
31
22
  if (duration === null) return null;
32
- const kind = durationKind(duration);
33
23
 
34
24
  switch (duration.state) {
35
25
  case 'fixed': {
36
26
  const display = formatFixedDurationLabel(duration.elapsed);
27
+ const verb = hasStarted ? 'ran' : 'lasted';
37
28
  return (
38
- <DurationText
39
- className={className}
40
- kind={kind}
41
- ariaLabel={`${FIXED_VERB_BY_KIND[kind]} ${display}`}
42
- >
29
+ <DurationText className={className} ariaLabel={`${verb} ${display}`}>
43
30
  {display}
44
31
  </DurationText>
45
32
  );
46
33
  }
47
34
  case 'live':
48
- return <LiveDurationText duration={duration} kind={kind} className={className} />;
35
+ return <LiveDurationText duration={duration} className={className} />;
49
36
  default: {
50
37
  const exhaustive: never = duration;
51
38
  return exhaustive;
@@ -55,22 +42,25 @@ export function WorkflowRunDurationLabel({
55
42
 
56
43
  export function useWorkflowRunDurationAccessibleLabel(
57
44
  duration: WorkflowRunDurationDisplay | null,
45
+ hasStarted: boolean,
58
46
  ): string | undefined {
59
47
  useTimeTick();
60
- return workflowRunDurationAccessibleLabel(duration);
48
+ return workflowRunDurationAccessibleLabel(duration, hasStarted);
61
49
  }
62
50
 
63
51
  export function workflowRunDurationAccessibleLabel(
64
52
  duration: WorkflowRunDurationDisplay | null,
53
+ hasStarted: boolean,
65
54
  ): string | undefined {
66
55
  if (duration === null) return undefined;
67
- const kind = durationKind(duration);
68
56
 
69
57
  switch (duration.state) {
70
58
  case 'live':
71
- return `${LIVE_VERB_BY_KIND[kind]} ${humanDuration(duration.fromIso)}`;
72
- case 'fixed':
73
- return `${FIXED_VERB_BY_KIND[kind]} ${formatFixedDurationLabel(duration.elapsed)}`;
59
+ return `running ${humanDuration(duration.fromIso)}`;
60
+ case 'fixed': {
61
+ const verb = hasStarted ? 'ran' : 'lasted';
62
+ return `${verb} ${formatFixedDurationLabel(duration.elapsed)}`;
63
+ }
74
64
  default: {
75
65
  const exhaustive: never = duration;
76
66
  return exhaustive;
@@ -78,27 +68,17 @@ export function workflowRunDurationAccessibleLabel(
78
68
  }
79
69
  }
80
70
 
81
- function durationKind(duration: WorkflowRunDurationDisplay): DurationKind {
82
- return 'kind' in duration ? duration.kind : 'run';
83
- }
84
-
85
71
  function LiveDurationText({
86
72
  duration,
87
- kind,
88
73
  className,
89
74
  }: {
90
75
  duration: Extract<WorkflowRunDurationDisplay, {state: 'live'}>;
91
- kind: DurationKind;
92
76
  className?: string | undefined;
93
77
  }) {
94
78
  useTimeTick();
95
79
  const display = humanDuration(duration.fromIso);
96
80
  return (
97
- <DurationText
98
- className={className}
99
- kind={kind}
100
- ariaLabel={`${LIVE_VERB_BY_KIND[kind]} ${display}`}
101
- >
81
+ <DurationText className={className} ariaLabel={`running ${display}`}>
102
82
  {display}
103
83
  </DurationText>
104
84
  );
@@ -107,12 +87,10 @@ function LiveDurationText({
107
87
  function DurationText({
108
88
  children,
109
89
  className,
110
- kind,
111
90
  ariaLabel,
112
91
  }: {
113
92
  children: string;
114
93
  className?: string | undefined;
115
- kind: DurationKind;
116
94
  ariaLabel?: string | undefined;
117
95
  }) {
118
96
  return (
@@ -125,10 +103,9 @@ function DurationText({
125
103
  className,
126
104
  )}
127
105
  >
128
- {/* The glyph is the only visible mark of queue vs run: the status badge already writes
129
- "Queued", and the numeric columns are fixed width. The verb rides the accessible
130
- name instead. */}
131
- <Icon name={GLYPH_BY_KIND[kind]} className="size-12 shrink-0" aria-hidden="true" />
106
+ {/* The verb rides the accessible name: the numeric columns are fixed width and the
107
+ status badge already says whether the run is still going. */}
108
+ <Icon name="timerLine" className="size-12 shrink-0" aria-hidden="true" />
132
109
  {children}
133
110
  </Code>
134
111
  );
@@ -470,6 +470,7 @@ describe('WorkflowRunListView', () => {
470
470
  run('succeeded', 'build-image', 'run-build-image', {
471
471
  started_at: '2026-05-07T01:00:00.000Z',
472
472
  finished_at: '2026-05-07T01:02:14.000Z',
473
+ has_started_job_execution: true,
473
474
  }),
474
475
  ]);
475
476
 
@@ -496,6 +497,53 @@ describe('WorkflowRunListView', () => {
496
497
  expect(duration).toHaveAttribute('aria-label', 'running 2m 14s');
497
498
  });
498
499
 
500
+ test('uses the attempt status when all listed jobs are pending', async () => {
501
+ vi.spyOn(Date, 'now').mockReturnValue(Date.parse('2026-05-07T01:02:14.000Z'));
502
+
503
+ renderListView([
504
+ run('running', 'waiting-for-runner', 'run-waiting-for-runner', {
505
+ ...workflowRunJobsFixture(['pending', 'pending']),
506
+ started_at: '2026-05-07T01:00:00.000Z',
507
+ finished_at: null,
508
+ }),
509
+ ]);
510
+
511
+ const duration = await screen.findByText('2m 14s');
512
+ expect(duration).toHaveAttribute('aria-label', 'running 2m 14s');
513
+ expect(
514
+ screen.getByRole('link', {
515
+ name: (name) => name.includes('Running') && name.includes('running 2m 14s'),
516
+ }),
517
+ ).toBeInTheDocument();
518
+ });
519
+
520
+ test('uses a neutral verb when no listed job execution started', async () => {
521
+ renderListView([
522
+ run('cancelled', 'cancelled-before-runner', 'run-cancelled-before-runner', {
523
+ ...workflowRunJobsFixture(['pending']),
524
+ started_at: '2026-05-07T01:00:00.000Z',
525
+ finished_at: '2026-05-07T01:02:14.000Z',
526
+ }),
527
+ ]);
528
+
529
+ const duration = await screen.findByText('2m 14s');
530
+ expect(duration).toHaveAttribute('aria-label', 'lasted 2m 14s');
531
+ });
532
+
533
+ test('uses the started-execution flag when a cancelled job reached a runner', async () => {
534
+ renderListView([
535
+ run('cancelled', 'cancelled-after-start', 'run-cancelled-after-start', {
536
+ ...workflowRunJobsFixture(['cancelled']),
537
+ has_started_job_execution: true,
538
+ started_at: '2026-05-07T01:00:00.000Z',
539
+ finished_at: '2026-05-07T01:02:14.000Z',
540
+ }),
541
+ ]);
542
+
543
+ const duration = await screen.findByText('2m 14s');
544
+ expect(duration).toHaveAttribute('aria-label', 'ran 2m 14s');
545
+ });
546
+
499
547
  test('renders the workflow name beside the run number', async () => {
500
548
  renderListView([
501
549
  run('succeeded', 'Deploy production', 'run-deploy-production', {
@@ -20,7 +20,6 @@ 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
25
  import {JobStatusStrip, jobStatusSummary} from './job-status-strip.js';
@@ -69,9 +68,11 @@ export function WorkflowRunRow({
69
68
  workspaceSlug?: string | undefined;
70
69
  projectSlug?: string | undefined;
71
70
  }) {
72
- const display = workflowRunListItemDisplay(run);
73
- const durationLabel = useWorkflowRunDurationAccessibleLabel(display.duration);
74
- const statusLabel = getWorkflowStatusVisual(display.status).label;
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={display.status} size={14} className="shrink-0" />
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">
@@ -105,7 +106,7 @@ export function WorkflowRunRow({
105
106
  <JobStatusStrip jobs={run.jobs} />
106
107
  </span>
107
108
  <span className="flex w-64 justify-end">
108
- <WorkflowRunDurationLabel duration={display.duration} />
109
+ <WorkflowRunDurationLabel duration={duration} hasStarted={hasStarted} />
109
110
  </span>
110
111
  <Code
111
112
  variant="label"
@@ -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';
@@ -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'}),
@@ -159,6 +159,17 @@ describe('WorkflowRunSummary', () => {
159
159
  test('shows a live selected attempt duration for running runs', async () => {
160
160
  vi.spyOn(Date, 'now').mockReturnValue(Date.parse('2026-05-07T01:02:14.000Z'));
161
161
  renderSummary({
162
+ jobs: [
163
+ workflowJobDto({
164
+ status: 'running',
165
+ job_executions: [
166
+ workflowJobExecutionDto({
167
+ status: 'running',
168
+ started_at: '2026-05-07T01:02:00.000Z',
169
+ }),
170
+ ],
171
+ }),
172
+ ],
162
173
  run_attempt: {
163
174
  id: '11111111-1111-4111-8111-000000000001',
164
175
  workflow_run_id: RUN_ID,
@@ -178,7 +189,9 @@ describe('WorkflowRunSummary', () => {
178
189
  expect(duration).toHaveAttribute('aria-label', 'running 2m 14s');
179
190
  });
180
191
 
181
- test('reads a running run whose jobs have not started as queued', async () => {
192
+ // The header reports the attempt the API returned. Whether a job has reached a runner yet is
193
+ // the job surfaces' business, so an attempt whose jobs are all pending still reads as running.
194
+ test('reads a running attempt as running whatever its jobs have reached', async () => {
182
195
  vi.spyOn(Date, 'now').mockReturnValue(Date.parse('2026-05-07T01:02:14.000Z'));
183
196
  renderSummary({
184
197
  jobs: [workflowJobDto({status: 'pending'})],
@@ -187,50 +200,89 @@ describe('WorkflowRunSummary', () => {
187
200
 
188
201
  const summary = await screen.findByRole('region', {name: 'deploy-web'});
189
202
 
190
- expect(within(summary).getAllByText('Queued')).not.toHaveLength(0);
191
- expect(within(summary).queryByText('Running')).not.toBeInTheDocument();
192
- expect(within(summary).getByText('2m 14s')).toHaveAttribute('aria-label', 'queued 2m 14s');
203
+ expect(within(summary).getAllByText('Running')).not.toHaveLength(0);
204
+ expect(within(summary).queryByText('Queued')).not.toBeInTheDocument();
205
+ expect(within(summary).getByText('2m 14s')).toHaveAttribute('aria-label', 'running 2m 14s');
193
206
  });
194
207
 
195
- test('names the job a queued run is waiting on when there is more than one', async () => {
208
+ test('uses a neutral verb when an attempt finished before any job execution started', async () => {
196
209
  renderSummary({
210
+ status: 'cancelled',
197
211
  jobs: [
198
212
  workflowJobDto({
199
- name: 'lint',
200
213
  status: 'pending',
201
214
  job_executions: [workflowJobExecutionDto({queued_at: '2026-05-07T01:00:00.000Z'})],
202
215
  }),
203
- workflowJobDto({
204
- name: 'build',
205
- status: 'pending',
206
- job_executions: [workflowJobExecutionDto({queued_at: '2026-05-07T00:59:00.000Z'})],
207
- }),
208
216
  ],
209
- run_attempt: runningAttemptDto(),
217
+ run_attempt: {
218
+ id: '11111111-1111-4111-8111-000000000001',
219
+ workflow_run_id: RUN_ID,
220
+ attempt: 1,
221
+ status: 'cancelled',
222
+ created_at: '2026-05-07T01:00:00.000Z',
223
+ started_at: '2026-05-07T01:00:00.000Z',
224
+ finished_at: '2026-05-07T01:02:14.000Z',
225
+ rerun_mode: null,
226
+ },
210
227
  });
211
228
 
212
229
  const summary = await screen.findByRole('region', {name: 'deploy-web'});
213
230
 
214
- expect(within(summary).getByText('waiting on')).toBeInTheDocument();
215
- expect(within(summary).getByText('build')).toBeInTheDocument();
231
+ expect(within(summary).getAllByText('Cancelled')).not.toHaveLength(0);
232
+ expect(within(summary).getByText('2m 14s')).toHaveAttribute('aria-label', 'lasted 2m 14s');
216
233
  });
217
234
 
218
- test('leaves the waiting job unnamed when the run has a single job', async () => {
235
+ test('uses the run verb when a cancelled job execution started', async () => {
219
236
  renderSummary({
237
+ status: 'cancelled',
220
238
  jobs: [
221
239
  workflowJobDto({
222
- name: 'build',
223
- status: 'pending',
224
- job_executions: [workflowJobExecutionDto({queued_at: '2026-05-07T01:00:00.000Z'})],
240
+ status: 'cancelled',
241
+ job_executions: [
242
+ workflowJobExecutionDto({
243
+ status: 'cancelled',
244
+ started_at: '2026-05-07T01:00:05.000Z',
245
+ }),
246
+ ],
225
247
  }),
226
248
  ],
227
- run_attempt: runningAttemptDto(),
249
+ run_attempt: {
250
+ id: '11111111-1111-4111-8111-000000000001',
251
+ workflow_run_id: RUN_ID,
252
+ attempt: 1,
253
+ status: 'cancelled',
254
+ created_at: '2026-05-07T01:00:00.000Z',
255
+ started_at: '2026-05-07T01:00:00.000Z',
256
+ finished_at: '2026-05-07T01:02:14.000Z',
257
+ rerun_mode: null,
258
+ },
259
+ });
260
+
261
+ const summary = await screen.findByRole('region', {name: 'deploy-web'});
262
+
263
+ expect(within(summary).getByText('2m 14s')).toHaveAttribute('aria-label', 'ran 2m 14s');
264
+ });
265
+
266
+ test('uses a neutral verb when every job was skipped', async () => {
267
+ renderSummary({
268
+ status: 'succeeded',
269
+ jobs: [workflowJobDto({status: 'skipped'})],
270
+ run_attempt: {
271
+ id: '11111111-1111-4111-8111-000000000001',
272
+ workflow_run_id: RUN_ID,
273
+ attempt: 1,
274
+ status: 'succeeded',
275
+ created_at: '2026-05-07T01:00:00.000Z',
276
+ started_at: '2026-05-07T01:00:00.000Z',
277
+ finished_at: '2026-05-07T01:02:14.000Z',
278
+ rerun_mode: null,
279
+ },
228
280
  });
229
281
 
230
282
  const summary = await screen.findByRole('region', {name: 'deploy-web'});
231
283
 
232
- expect(within(summary).getAllByText('Queued')).not.toHaveLength(0);
233
- expect(within(summary).queryByText('waiting on')).not.toBeInTheDocument();
284
+ expect(within(summary).getAllByText('Succeeded')).not.toHaveLength(0);
285
+ expect(within(summary).getByText('2m 14s')).toHaveAttribute('aria-label', 'lasted 2m 14s');
234
286
  });
235
287
 
236
288
  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 {Code, Header, Text} from '@shipfox/react-ui/typography';
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
- ...[...WORKFLOW_RUN_STATUSES, 'queued' as const].map(
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,19 +56,15 @@ export function WorkflowRunSummary({
60
56
  latestAttempt,
61
57
  }: WorkflowRunSummaryProps) {
62
58
  const headingId = useId();
63
- const display = workflowRunDetailDisplay(run);
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 = display.duration;
72
- // Named only while the run is waiting and has more than one job: with a single job the rail
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
 
@@ -184,24 +176,11 @@ export function WorkflowRunSummary({
184
176
  <MetadataSeparator />
185
177
  <WorkflowRunDurationLabel
186
178
  duration={displayDuration}
179
+ hasStarted={hasStarted}
187
180
  className="text-foreground-neutral-subtle"
188
181
  />
189
182
  </>
190
183
  ) : 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
184
  </div>
206
185
  </div>
207
186
  </section>
@@ -18,10 +18,6 @@ export function getWorkflowStatusVisual(status: WorkflowDisplayStatus): Workflow
18
18
  switch (status) {
19
19
  case 'pending':
20
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'};
25
21
  case 'running':
26
22
  return {kind: 'running', label: 'Running', dot: 'info', badge: 'info'};
27
23
  case 'listening':
@@ -6,7 +6,6 @@ import {WorkflowStatusIcon} from './workflow-status-icon.js';
6
6
 
7
7
  const statuses: WorkflowDisplayStatus[] = [
8
8
  'pending',
9
- 'queued',
10
9
  'running',
11
10
  'listening',
12
11
  'succeeded',
@@ -89,24 +89,6 @@ export function WorkflowStatusIcon({
89
89
  <Icon name="pulseLine" size={Math.max(8, Math.round(size * 0.7))} className="text-white" />
90
90
  </span>
91
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
- );
110
92
  } else if (visual.kind === 'running') {
111
93
  glyph = <Dot variant="info" ripple={ripple} className={box} />;
112
94
  } else if (visual.kind === 'pending') {