@shipfox/client-workflows 12.0.2 → 13.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 (232) hide show
  1. package/.storybook/preview.tsx +3 -3
  2. package/.turbo/turbo-build.log +1 -1
  3. package/CHANGELOG.md +56 -0
  4. package/dist/components/job-graph/job-duration-format.d.ts +2 -2
  5. package/dist/components/job-graph/job-duration-format.d.ts.map +1 -1
  6. package/dist/components/job-graph/job-duration-format.js +4 -2
  7. package/dist/components/job-graph/job-duration-format.js.map +1 -1
  8. package/dist/components/job-graph/job-node.d.ts +1 -1
  9. package/dist/components/job-graph/job-node.d.ts.map +1 -1
  10. package/dist/components/job-graph/job-node.js +5 -4
  11. package/dist/components/job-graph/job-node.js.map +1 -1
  12. package/dist/components/step-list/step-list.d.ts +2 -2
  13. package/dist/components/step-list/step-list.d.ts.map +1 -1
  14. package/dist/components/step-list/step-list.js.map +1 -1
  15. package/dist/components/workflow-run-list/job-status-strip.d.ts +21 -0
  16. package/dist/components/workflow-run-list/job-status-strip.d.ts.map +1 -0
  17. package/dist/components/workflow-run-list/job-status-strip.js +185 -0
  18. package/dist/components/workflow-run-list/job-status-strip.js.map +1 -0
  19. package/dist/components/workflow-run-list/run-display.d.ts +27 -3
  20. package/dist/components/workflow-run-list/run-display.d.ts.map +1 -1
  21. package/dist/components/workflow-run-list/run-display.js +78 -7
  22. package/dist/components/workflow-run-list/run-display.js.map +1 -1
  23. package/dist/components/workflow-run-list/types.d.ts +23 -24
  24. package/dist/components/workflow-run-list/types.d.ts.map +1 -1
  25. package/dist/components/workflow-run-list/types.js.map +1 -1
  26. package/dist/components/workflow-run-list/workflow-run-filter-menu.d.ts +22 -0
  27. package/dist/components/workflow-run-list/workflow-run-filter-menu.d.ts.map +1 -0
  28. package/dist/components/workflow-run-list/workflow-run-filter-menu.js +68 -0
  29. package/dist/components/workflow-run-list/workflow-run-filter-menu.js.map +1 -0
  30. package/dist/components/workflow-run-list/workflow-run-filters.d.ts +18 -0
  31. package/dist/components/workflow-run-list/workflow-run-filters.d.ts.map +1 -0
  32. package/dist/components/workflow-run-list/workflow-run-filters.js +236 -0
  33. package/dist/components/workflow-run-list/workflow-run-filters.js.map +1 -0
  34. package/dist/components/workflow-run-list/workflow-run-list-content.d.ts +8 -4
  35. package/dist/components/workflow-run-list/workflow-run-list-content.d.ts.map +1 -1
  36. package/dist/components/workflow-run-list/workflow-run-list-content.js +40 -12
  37. package/dist/components/workflow-run-list/workflow-run-list-content.js.map +1 -1
  38. package/dist/components/workflow-run-list/workflow-run-list-states.d.ts +29 -2
  39. package/dist/components/workflow-run-list/workflow-run-list-states.d.ts.map +1 -1
  40. package/dist/components/workflow-run-list/workflow-run-list-states.js +103 -44
  41. package/dist/components/workflow-run-list/workflow-run-list-states.js.map +1 -1
  42. package/dist/components/workflow-run-list/workflow-run-list-view.d.ts +1 -1
  43. package/dist/components/workflow-run-list/workflow-run-list-view.d.ts.map +1 -1
  44. package/dist/components/workflow-run-list/workflow-run-list-view.js +60 -43
  45. package/dist/components/workflow-run-list/workflow-run-list-view.js.map +1 -1
  46. package/dist/components/workflow-run-list/workflow-run-list.d.ts +1 -1
  47. package/dist/components/workflow-run-list/workflow-run-list.d.ts.map +1 -1
  48. package/dist/components/workflow-run-list/workflow-run-list.js +20 -8
  49. package/dist/components/workflow-run-list/workflow-run-list.js.map +1 -1
  50. package/dist/components/workflow-run-list/workflow-run-row.d.ts +19 -9
  51. package/dist/components/workflow-run-list/workflow-run-row.d.ts.map +1 -1
  52. package/dist/components/workflow-run-list/workflow-run-row.js +179 -112
  53. package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
  54. package/dist/components/workflow-run-number-label.d.ts +15 -0
  55. package/dist/components/workflow-run-number-label.d.ts.map +1 -0
  56. package/dist/components/workflow-run-number-label.js +41 -0
  57. package/dist/components/workflow-run-number-label.js.map +1 -0
  58. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.d.ts +3 -3
  59. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.d.ts.map +1 -1
  60. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js +8 -7
  61. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js.map +1 -1
  62. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts +3 -3
  63. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
  64. package/dist/components/workflow-run-summary/workflow-run-summary.js +16 -7
  65. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
  66. package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts +2 -2
  67. package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts.map +1 -1
  68. package/dist/components/workflow-run-view/agent-config-failure-callout.js +4 -4
  69. package/dist/components/workflow-run-view/agent-config-failure-callout.js.map +1 -1
  70. package/dist/components/workflow-run-view/job-card.d.ts +2 -2
  71. package/dist/components/workflow-run-view/job-card.d.ts.map +1 -1
  72. package/dist/components/workflow-run-view/job-card.js +51 -32
  73. package/dist/components/workflow-run-view/job-card.js.map +1 -1
  74. package/dist/components/workflow-run-view/job-execution-switcher.d.ts +1 -1
  75. package/dist/components/workflow-run-view/job-execution-switcher.d.ts.map +1 -1
  76. package/dist/components/workflow-run-view/job-execution-switcher.js +20 -8
  77. package/dist/components/workflow-run-view/job-execution-switcher.js.map +1 -1
  78. package/dist/components/workflow-run-view/workflow-run-view.d.ts +3 -2
  79. package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
  80. package/dist/components/workflow-run-view/workflow-run-view.js +15 -11
  81. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
  82. package/dist/components/workflow-status/status-visuals.d.ts +4 -4
  83. package/dist/components/workflow-status/status-visuals.d.ts.map +1 -1
  84. package/dist/components/workflow-status/status-visuals.js +7 -0
  85. package/dist/components/workflow-status/status-visuals.js.map +1 -1
  86. package/dist/components/workflow-status/workflow-status-icon.d.ts +3 -4
  87. package/dist/components/workflow-status/workflow-status-icon.d.ts.map +1 -1
  88. package/dist/components/workflow-status/workflow-status-icon.js +6 -4
  89. package/dist/components/workflow-status/workflow-status-icon.js.map +1 -1
  90. package/dist/core/entities/job-execution.d.ts +10 -0
  91. package/dist/core/entities/job-execution.d.ts.map +1 -1
  92. package/dist/core/entities/job-execution.js +11 -0
  93. package/dist/core/entities/job-execution.js.map +1 -1
  94. package/dist/core/entities/job.d.ts +4 -2
  95. package/dist/core/entities/job.d.ts.map +1 -1
  96. package/dist/core/entities/job.js +8 -1
  97. package/dist/core/entities/job.js.map +1 -1
  98. package/dist/core/entities/workflow-run.d.ts +52 -4
  99. package/dist/core/entities/workflow-run.d.ts.map +1 -1
  100. package/dist/core/entities/workflow-run.js +22 -3
  101. package/dist/core/entities/workflow-run.js.map +1 -1
  102. package/dist/core/workflow-run.d.ts +6 -6
  103. package/dist/core/workflow-run.d.ts.map +1 -1
  104. package/dist/core/workflow-run.js +3 -3
  105. package/dist/core/workflow-run.js.map +1 -1
  106. package/dist/feature.d.ts +7 -7
  107. package/dist/feature.js +5 -5
  108. package/dist/feature.js.map +1 -1
  109. package/dist/hooks/api/workflow-run-mapper.d.ts +4 -3
  110. package/dist/hooks/api/workflow-run-mapper.d.ts.map +1 -1
  111. package/dist/hooks/api/workflow-run-mapper.js +31 -3
  112. package/dist/hooks/api/workflow-run-mapper.js.map +1 -1
  113. package/dist/hooks/api/workflow-runs.d.ts +4 -4
  114. package/dist/hooks/api/workflow-runs.d.ts.map +1 -1
  115. package/dist/hooks/api/workflow-runs.js +12 -4
  116. package/dist/hooks/api/workflow-runs.js.map +1 -1
  117. package/dist/index.d.ts +4 -3
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +3 -2
  120. package/dist/index.js.map +1 -1
  121. package/dist/pages/project-workflows-page.js +46 -1
  122. package/dist/pages/project-workflows-page.js.map +1 -1
  123. package/dist/pages/workflow-run-detail-page.d.ts +11 -0
  124. package/dist/pages/workflow-run-detail-page.d.ts.map +1 -0
  125. package/dist/pages/workflow-run-detail-page.js +30 -0
  126. package/dist/pages/workflow-run-detail-page.js.map +1 -0
  127. package/dist/pages/workflow-run-list-page.d.ts +10 -0
  128. package/dist/pages/workflow-run-list-page.d.ts.map +1 -0
  129. package/dist/pages/workflow-run-list-page.js +43 -0
  130. package/dist/pages/workflow-run-list-page.js.map +1 -0
  131. package/dist/routes/inputs.d.ts +61 -5
  132. package/dist/routes/inputs.d.ts.map +1 -1
  133. package/dist/routes/inputs.js +150 -13
  134. package/dist/routes/inputs.js.map +1 -1
  135. package/dist/routes/project-route.d.ts +6 -0
  136. package/dist/routes/project-route.d.ts.map +1 -0
  137. package/dist/routes/project-route.js +25 -0
  138. package/dist/routes/project-route.js.map +1 -0
  139. package/dist/routes/run-detail.d.ts.map +1 -1
  140. package/dist/routes/run-detail.js +12 -7
  141. package/dist/routes/run-detail.js.map +1 -1
  142. package/dist/routes/runs.d.ts.map +1 -1
  143. package/dist/routes/runs.js +11 -6
  144. package/dist/routes/runs.js.map +1 -1
  145. package/dist/routes/workflows.d.ts.map +1 -1
  146. package/dist/routes/workflows.js +6 -5
  147. package/dist/routes/workflows.js.map +1 -1
  148. package/dist/tsconfig.test.tsbuildinfo +1 -1
  149. package/package.json +9 -9
  150. package/src/components/job-graph/job-duration-format.ts +11 -2
  151. package/src/components/job-graph/job-graph-view.test.tsx +19 -8
  152. package/src/components/job-graph/job-graph.stories.tsx +23 -19
  153. package/src/components/job-graph/job-node.stories.tsx +10 -5
  154. package/src/components/job-graph/job-node.test.tsx +55 -10
  155. package/src/components/job-graph/job-node.tsx +16 -9
  156. package/src/components/step-list/step-list.stories.tsx +2 -3
  157. package/src/components/step-list/step-list.tsx +3 -2
  158. package/src/components/workflow-run-list/job-status-strip.tsx +189 -0
  159. package/src/components/workflow-run-list/run-display.test.ts +169 -30
  160. package/src/components/workflow-run-list/run-display.ts +116 -9
  161. package/src/components/workflow-run-list/types.ts +21 -17
  162. package/src/components/workflow-run-list/workflow-run-filter-menu.tsx +100 -0
  163. package/src/components/workflow-run-list/workflow-run-filters.tsx +243 -0
  164. package/src/components/workflow-run-list/workflow-run-list-content.tsx +44 -16
  165. package/src/components/workflow-run-list/workflow-run-list-states.tsx +128 -27
  166. package/src/components/workflow-run-list/workflow-run-list-view.test.tsx +441 -104
  167. package/src/components/workflow-run-list/workflow-run-list-view.tsx +73 -43
  168. package/src/components/workflow-run-list/workflow-run-list.stories.tsx +140 -20
  169. package/src/components/workflow-run-list/workflow-run-list.tsx +20 -10
  170. package/src/components/workflow-run-list/workflow-run-row.tsx +173 -106
  171. package/src/components/workflow-run-number-label.test.tsx +45 -0
  172. package/src/components/workflow-run-number-label.tsx +47 -0
  173. package/src/components/workflow-run-summary/workflow-run-attempt-switcher.test.tsx +7 -8
  174. package/src/components/workflow-run-summary/workflow-run-attempt-switcher.tsx +13 -12
  175. package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +6 -8
  176. package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +27 -2
  177. package/src/components/workflow-run-summary/workflow-run-summary.tsx +19 -9
  178. package/src/components/workflow-run-view/agent-config-failure-callout.stories.tsx +2 -3
  179. package/src/components/workflow-run-view/agent-config-failure-callout.tsx +4 -4
  180. package/src/components/workflow-run-view/job-card.stories.tsx +11 -10
  181. package/src/components/workflow-run-view/job-card.test.tsx +115 -0
  182. package/src/components/workflow-run-view/job-card.tsx +74 -35
  183. package/src/components/workflow-run-view/job-execution-switcher.tsx +31 -9
  184. package/src/components/workflow-run-view/workflow-run-view.test.tsx +88 -30
  185. package/src/components/workflow-run-view/workflow-run-view.tsx +24 -14
  186. package/src/components/workflow-status/status-visuals.test.ts +9 -0
  187. package/src/components/workflow-status/status-visuals.ts +6 -4
  188. package/src/components/workflow-status/workflow-status-icon.stories.tsx +10 -2
  189. package/src/components/workflow-status/workflow-status-icon.test.tsx +18 -0
  190. package/src/components/workflow-status/workflow-status-icon.tsx +17 -24
  191. package/src/core/entities/job-execution.ts +22 -0
  192. package/src/core/entities/job-status.test.ts +104 -0
  193. package/src/core/entities/job.ts +18 -3
  194. package/src/core/entities/workflow-run.ts +76 -7
  195. package/src/core/workflow-run.test.ts +24 -9
  196. package/src/core/workflow-run.ts +17 -2
  197. package/src/feature.ts +5 -5
  198. package/src/hooks/api/workflow-run-mapper.ts +30 -3
  199. package/src/hooks/api/workflow-runs.test.tsx +2 -0
  200. package/src/hooks/api/workflow-runs.ts +12 -7
  201. package/src/index.ts +7 -2
  202. package/src/page-harness-budget.test.ts +1 -1
  203. package/src/pages/project-workflows-page.test.tsx +59 -5
  204. package/src/pages/project-workflows-page.tsx +34 -1
  205. package/src/pages/workflow-run-detail-page.tsx +45 -0
  206. package/src/pages/workflow-run-list-page.tsx +65 -0
  207. package/src/pages/{workflow-run-page.test.tsx → workflow-run-pages.test.tsx} +211 -47
  208. package/src/routes/inputs.test.ts +193 -12
  209. package/src/routes/inputs.ts +170 -11
  210. package/src/routes/project-route.tsx +25 -0
  211. package/src/routes/run-detail.tsx +15 -8
  212. package/src/routes/runs.tsx +14 -7
  213. package/src/routes/workflows.tsx +5 -4
  214. package/test/fixtures/workflow-run.ts +76 -7
  215. package/test/pages.tsx +18 -8
  216. package/test/render.tsx +8 -2
  217. package/tsconfig.build.tsbuildinfo +1 -1
  218. package/dist/components/workflow-run-list/workflow-run-list-header.d.ts +0 -10
  219. package/dist/components/workflow-run-list/workflow-run-list-header.d.ts.map +0 -1
  220. package/dist/components/workflow-run-list/workflow-run-list-header.js +0 -55
  221. package/dist/components/workflow-run-list/workflow-run-list-header.js.map +0 -1
  222. package/dist/pages/workflow-run-first-time-use.d.ts +0 -6
  223. package/dist/pages/workflow-run-first-time-use.d.ts.map +0 -1
  224. package/dist/pages/workflow-run-first-time-use.js +0 -17
  225. package/dist/pages/workflow-run-first-time-use.js.map +0 -1
  226. package/dist/pages/workflow-run-page.d.ts +0 -10
  227. package/dist/pages/workflow-run-page.d.ts.map +0 -1
  228. package/dist/pages/workflow-run-page.js +0 -90
  229. package/dist/pages/workflow-run-page.js.map +0 -1
  230. package/src/components/workflow-run-list/workflow-run-list-header.tsx +0 -53
  231. package/src/pages/workflow-run-first-time-use.tsx +0 -17
  232. package/src/pages/workflow-run-page.tsx +0 -94
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/client-workflows",
3
3
  "license": "MIT",
4
- "version": "12.0.2",
4
+ "version": "13.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -27,15 +27,15 @@
27
27
  "dependencies": {
28
28
  "@swc/helpers": "^0.5.17",
29
29
  "date-fns": "^4.1.0",
30
- "@shipfox/api-definitions-dto": "11.0.0",
31
- "@shipfox/api-workflows-dto": "10.0.0",
30
+ "@shipfox/api-workflows-dto": "12.0.0",
32
31
  "@shipfox/client-api": "6.0.1",
33
- "@shipfox/client-logs": "12.0.1",
34
- "@shipfox/client-projects": "12.0.2",
35
- "@shipfox/client-shell": "12.0.2",
36
- "@shipfox/client-triggers": "12.0.2",
37
- "@shipfox/client-ui": "6.0.2",
38
- "@shipfox/react-ui": "0.3.7"
32
+ "@shipfox/client-logs": "13.0.0",
33
+ "@shipfox/api-definitions-dto": "12.0.0",
34
+ "@shipfox/client-projects": "13.0.0",
35
+ "@shipfox/client-shell": "13.0.0",
36
+ "@shipfox/client-triggers": "13.0.0",
37
+ "@shipfox/react-ui": "0.4.0",
38
+ "@shipfox/client-ui": "13.0.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@tanstack/react-query": "^5.101.0",
@@ -1,5 +1,9 @@
1
1
  import {humanDuration} from '@shipfox/react-ui/utils';
2
- import type {JobDisplayDuration, JobExecutionTime} from '#core/workflow-run.js';
2
+ import type {
3
+ JobDisplayDuration,
4
+ JobExecutionDisplayStatus,
5
+ JobExecutionTime,
6
+ } from '#core/workflow-run.js';
3
7
 
4
8
  export function formatJobExecutionTimeLabel(time: JobExecutionTime): string {
5
9
  switch (time.state) {
@@ -16,6 +20,7 @@ export function formatJobExecutionTimeLabel(time: JobExecutionTime): string {
16
20
 
17
21
  export function formatJobDurationAccessibleLabel(
18
22
  duration: JobDisplayDuration | null,
23
+ displayStatus?: JobExecutionDisplayStatus,
19
24
  ): string | undefined {
20
25
  if (duration === null) return undefined;
21
26
 
@@ -24,7 +29,11 @@ export function formatJobDurationAccessibleLabel(
24
29
  case 'queue':
25
30
  return duration.state === 'live' ? `queueing ${label}` : `queued ${label}`;
26
31
  case 'run':
27
- return duration.state === 'live' ? `running ${label}` : `ran ${label}`;
32
+ if (duration.state !== 'live') return `ran ${label}`;
33
+ if (displayStatus === 'running') return `running ${label}`;
34
+ return displayStatus === 'pending'
35
+ ? `runner assigned ${label}`
36
+ : `execution elapsed ${label}`;
28
37
  default: {
29
38
  const exhaustive: never = duration;
30
39
  return exhaustive;
@@ -6,6 +6,7 @@ import {
6
6
  workflowJob,
7
7
  workflowJobExecutionDto,
8
8
  workflowRunDetail,
9
+ workflowStepDto,
9
10
  } from '#test/fixtures/workflow-run.js';
10
11
  import {JobGraph} from './job-graph.js';
11
12
 
@@ -46,10 +47,7 @@ describe('JobGraph', () => {
46
47
  render(
47
48
  <JobGraph
48
49
  run={makeRun({
49
- jobs: [
50
- makeJob({name: 'linux', status: 'running'}),
51
- makeJob({name: 'macos', status: 'running', position: 1}),
52
- ],
50
+ jobs: [makeRunningJob({name: 'linux'}), makeRunningJob({name: 'macos', position: 1})],
53
51
  })}
54
52
  />,
55
53
  );
@@ -99,7 +97,7 @@ describe('JobGraph', () => {
99
97
  run={makeRun({
100
98
  jobs: [
101
99
  makeJob({name: 'build'}),
102
- makeJob({name: 'deploy', position: 1, dependencies: ['build'], status: 'running'}),
100
+ makeRunningJob({name: 'deploy', position: 1, dependencies: ['build']}),
103
101
  ],
104
102
  })}
105
103
  />,
@@ -161,7 +159,7 @@ describe('JobGraph', () => {
161
159
  />,
162
160
  );
163
161
 
164
- const build = screen.getByRole('button', {name: 'build, Pending, 2 executions'});
162
+ const build = screen.getByRole('button', {name: 'build, Succeeded, 2 executions'});
165
163
  const deploy = screen.getByRole('button', {name: 'deploy, Pending'});
166
164
  expect(within(build).getByText('2')).toBeInTheDocument();
167
165
  expect(within(build).queryByText('2 exec')).not.toBeInTheDocument();
@@ -224,7 +222,7 @@ describe('JobGraph', () => {
224
222
  dependencies: ['build'],
225
223
  status: 'succeeded',
226
224
  });
227
- const testJob = makeJob({
225
+ const testJob = makeRunningJob({
228
226
  name: 'test',
229
227
  position: 2,
230
228
  dependencies: ['build'],
@@ -257,7 +255,7 @@ describe('JobGraph', () => {
257
255
  test('routes skipped-column join edges away from intermediate nodes', () => {
258
256
  const packageJob = makeJob({name: 'package', position: 0, status: 'succeeded'});
259
257
  const securityScan = makeJob({name: 'security-scan', position: 1, status: 'succeeded'});
260
- const smokeTests = makeJob({
258
+ const smokeTests = makeRunningJob({
261
259
  name: 'smoke-tests',
262
260
  position: 2,
263
261
  dependencies: ['package'],
@@ -296,3 +294,16 @@ function makeRun(overrides: Partial<WorkflowRunDetail> = {}): WorkflowRunDetail
296
294
  function makeJob(overrides: Partial<WorkflowRunJobDetailDto> & {name: string}): Job {
297
295
  return workflowJob(overrides);
298
296
  }
297
+
298
+ function makeRunningJob(overrides: Partial<WorkflowRunJobDetailDto> & {name: string}): Job {
299
+ return makeJob({
300
+ ...overrides,
301
+ status: 'running',
302
+ job_executions: [
303
+ workflowJobExecutionDto({
304
+ status: 'running',
305
+ steps: [workflowStepDto({status: 'running'})],
306
+ }),
307
+ ],
308
+ });
309
+ }
@@ -2,7 +2,7 @@ import type {WorkflowRunJobDetailDto} from '@shipfox/api-workflows-dto';
2
2
  import type {Meta, StoryObj} from '@storybook/react';
3
3
  import {within} from 'storybook/test';
4
4
  import type {Job, WorkflowRunDetail} from '#core/workflow-run.js';
5
- import {workflowJob, workflowRunDetail} from '#test/fixtures/workflow-run.js';
5
+ import {workflowJob, workflowRunDetail, workflowStepDto} from '#test/fixtures/workflow-run.js';
6
6
  import {JobGraph} from './job-graph.js';
7
7
 
8
8
  const meta = {
@@ -93,6 +93,23 @@ export const Empty: Story = {
93
93
  args: {run: makeRun({jobs: []})},
94
94
  };
95
95
 
96
+ export const LargeWideAndTall: Story = {
97
+ args: {
98
+ run: makeRun({
99
+ jobs: [
100
+ ...linearJobs(10),
101
+ ...Array.from({length: 10}, (_, index) =>
102
+ makeJob({
103
+ name: `parallel-${String(index + 1).padStart(2, '0')}`,
104
+ position: 20 + index,
105
+ status: index < 2 ? 'running' : 'pending',
106
+ }),
107
+ ),
108
+ ],
109
+ }),
110
+ },
111
+ };
112
+
96
113
  export const Selected: Story = {
97
114
  args: {
98
115
  run: makeRun({
@@ -111,23 +128,6 @@ export const Selected: Story = {
111
128
  },
112
129
  };
113
130
 
114
- export const LargeWideAndTall: Story = {
115
- args: {
116
- run: makeRun({
117
- jobs: [
118
- ...linearJobs(10),
119
- ...Array.from({length: 10}, (_, index) =>
120
- makeJob({
121
- name: `parallel-${String(index + 1).padStart(2, '0')}`,
122
- position: 20 + index,
123
- status: index < 2 ? 'running' : 'pending',
124
- }),
125
- ),
126
- ],
127
- }),
128
- },
129
- };
130
-
131
131
  function linearJobs(count: number): Job[] {
132
132
  return Array.from({length: count}, (_, index) =>
133
133
  makeJob({
@@ -178,5 +178,9 @@ function makeRun(overrides: Partial<WorkflowRunDetail> = {}): WorkflowRunDetail
178
178
  }
179
179
 
180
180
  function makeJob(overrides: Partial<WorkflowRunJobDetailDto> & {name: string}): Job {
181
- return workflowJob(overrides);
181
+ const job = workflowJob(overrides);
182
+ if (overrides.status === 'running' && job.jobExecutions.length === 0) {
183
+ return workflowJob({...overrides, steps: [workflowStepDto({status: 'running'})]});
184
+ }
185
+ return job;
182
186
  }
@@ -2,7 +2,11 @@ import type {WorkflowRunJobDetailDto} from '@shipfox/api-workflows-dto';
2
2
  import type {Meta, StoryObj} from '@storybook/react';
3
3
  import type {KeyboardEventHandler} from 'react';
4
4
  import type {JobExecutionStatus, JobStatus} from '#core/workflow-run.js';
5
- import {workflowJob, workflowJobExecutionDto} from '#test/fixtures/workflow-run.js';
5
+ import {
6
+ workflowJob,
7
+ workflowJobExecutionDto,
8
+ workflowStepDto,
9
+ } from '#test/fixtures/workflow-run.js';
6
10
  import type {JobGraphNode} from './graph-model.js';
7
11
  import {JobNode} from './job-node.js';
8
12
 
@@ -183,7 +187,7 @@ export const ListeningProgression: Story = {
183
187
  <JobNode
184
188
  node={makeNode({
185
189
  id: 'job-listening-running',
186
- label: 'running, 0 executions',
190
+ label: 'listening, 0 executions',
187
191
  mode: 'listening',
188
192
  status: 'running',
189
193
  listenerStatus: 'listening',
@@ -200,7 +204,7 @@ export const ListeningProgression: Story = {
200
204
  <JobNode
201
205
  node={makeNode({
202
206
  id: 'job-one-running-execution',
203
- label: 'running, 1 running execution',
207
+ label: 'listening, 1 running execution',
204
208
  mode: 'listening',
205
209
  status: 'running',
206
210
  listenerStatus: 'listening',
@@ -217,7 +221,7 @@ export const ListeningProgression: Story = {
217
221
  <JobNode
218
222
  node={makeNode({
219
223
  id: 'job-mixed-executions',
220
- label: 'running, mixed executions',
224
+ label: 'listening, mixed executions',
221
225
  mode: 'listening',
222
226
  status: 'running',
223
227
  listenerStatus: 'listening',
@@ -330,6 +334,7 @@ function makeNode({
330
334
  queued_at: queuedAt,
331
335
  started_at: startedAt,
332
336
  finished_at: finishedAt,
337
+ ...(status === 'running' ? {steps: [workflowStepDto({status: 'running'})]} : {}),
333
338
  }),
334
339
  ]
335
340
  : jobExecutions,
@@ -359,6 +364,6 @@ function makeExecutionsByStatus(
359
364
  timed_out_at: null,
360
365
  created_at: '2026-06-26T11:54:00.000Z',
361
366
  updated_at: '2026-06-26T12:00:00.000Z',
362
- steps: [],
367
+ steps: status === 'running' ? [workflowStepDto({status: 'running'})] : [],
363
368
  }));
364
369
  }
@@ -2,14 +2,18 @@ import type {WorkflowRunJobDetailDto} from '@shipfox/api-workflows-dto';
2
2
  import {TimeTickerProvider} from '@shipfox/react-ui/time-ticker';
3
3
  import {act, render, screen, within} from '@testing-library/react';
4
4
  import userEvent from '@testing-library/user-event';
5
- import {workflowJob, workflowJobExecutionDto} from '#test/fixtures/workflow-run.js';
5
+ import {
6
+ workflowJob,
7
+ workflowJobExecutionDto,
8
+ workflowStepDto,
9
+ } from '#test/fixtures/workflow-run.js';
6
10
  import type {JobGraphNode} from './graph-model.js';
7
11
  import {JobNode} from './job-node.js';
8
12
 
9
13
  const NOW = Date.parse('2026-06-26T12:00:00.000Z');
10
14
 
11
15
  type NodeOverrides = Omit<Partial<WorkflowRunJobDetailDto>, 'job_executions'> & {
12
- name: string;
16
+ name?: string | null;
13
17
  job_executions?: WorkflowRunJobDetailDto['job_executions'];
14
18
  queued_at?: string | null;
15
19
  started_at?: string | null;
@@ -32,6 +36,9 @@ function makeNode(overrides: NodeOverrides): JobGraphNode {
32
36
  queued_at: queued_at ?? null,
33
37
  started_at: started_at ?? null,
34
38
  finished_at: finished_at ?? null,
39
+ ...(jobOverrides.status === 'running'
40
+ ? {steps: [workflowStepDto({status: 'running'})]}
41
+ : {}),
35
42
  }),
36
43
  ];
37
44
  } else if (job_executions !== undefined) {
@@ -200,7 +207,7 @@ describe('JobNode duration', () => {
200
207
  renderNode(node);
201
208
 
202
209
  expect(screen.queryByText('2m 14s')).not.toBeInTheDocument();
203
- expect(screen.getByRole('button', {name: 'deploy-window, Running'})).toBeInTheDocument();
210
+ expect(screen.getByRole('button', {name: 'deploy-window, Listening'})).toBeInTheDocument();
204
211
  });
205
212
  });
206
213
 
@@ -227,12 +234,12 @@ describe('JobNode listening indicator', () => {
227
234
 
228
235
  renderNode(node);
229
236
 
230
- expect(screen.getByRole('button', {name: 'deploy-window, Running'})).toBeInTheDocument();
237
+ expect(screen.getByRole('button', {name: 'deploy-window, Listening'})).toBeInTheDocument();
231
238
  expect(screen.queryByLabelText('Waiting for events to start job')).not.toBeInTheDocument();
232
239
 
233
- await user.hover(screen.getByRole('img', {name: 'Running'}));
240
+ await user.hover(screen.getByRole('img', {name: 'Listening'}));
234
241
 
235
- expect(await screen.findByRole('tooltip')).toHaveTextContent('Running');
242
+ expect(await screen.findByRole('tooltip')).toHaveTextContent('Listening');
236
243
  });
237
244
 
238
245
  test('does not show the listener icon before the listener is armed or after it resolves', () => {
@@ -277,6 +284,12 @@ describe('JobNode status indicator', () => {
277
284
  const node = makeNode({
278
285
  name: 'deploy',
279
286
  status: 'running',
287
+ job_executions: [
288
+ workflowJobExecutionDto({
289
+ status: 'running',
290
+ steps: [workflowStepDto({status: 'running'})],
291
+ }),
292
+ ],
280
293
  });
281
294
 
282
295
  renderNode(node);
@@ -286,7 +299,15 @@ describe('JobNode status indicator', () => {
286
299
  expect(await screen.findByRole('tooltip')).toHaveTextContent('Running');
287
300
  });
288
301
 
289
- test('shows a running execution as running while the job is still pending', () => {
302
+ test('uses the job key when the static name is missing', () => {
303
+ const node = makeNode({key: 'deploy-prod', name: null, status: 'succeeded'});
304
+
305
+ renderNode(node);
306
+
307
+ expect(screen.getByRole('button', {name: 'deploy-prod, Succeeded'})).toBeInTheDocument();
308
+ });
309
+
310
+ test('shows a running lifecycle without a running step as pending', () => {
290
311
  const node = makeNode({
291
312
  name: 'deploy',
292
313
  status: 'pending',
@@ -295,6 +316,23 @@ describe('JobNode status indicator', () => {
295
316
 
296
317
  renderNode(node);
297
318
 
319
+ expect(screen.getByRole('button', {name: 'deploy, Pending'})).toBeInTheDocument();
320
+ });
321
+
322
+ test('shows a pending lifecycle as running while a step is active', () => {
323
+ const node = makeNode({
324
+ name: 'deploy',
325
+ status: 'pending',
326
+ job_executions: [
327
+ workflowJobExecutionDto({
328
+ status: 'pending',
329
+ steps: [workflowStepDto({status: 'running'})],
330
+ }),
331
+ ],
332
+ });
333
+
334
+ renderNode(node);
335
+
298
336
  expect(screen.getByRole('button', {name: 'deploy, Running'})).toBeInTheDocument();
299
337
  });
300
338
  });
@@ -317,10 +355,17 @@ describe('JobNode execution count indicator', () => {
317
355
  name: 'release-gates',
318
356
  mode: 'listening',
319
357
  status: 'running',
358
+ listener_status: 'listening',
320
359
  job_executions: [
321
360
  workflowJobExecutionDto({status: 'pending'}),
322
- workflowJobExecutionDto({status: 'running'}),
323
- workflowJobExecutionDto({status: 'running'}),
361
+ workflowJobExecutionDto({
362
+ status: 'running',
363
+ steps: [workflowStepDto({status: 'running'})],
364
+ }),
365
+ workflowJobExecutionDto({
366
+ status: 'running',
367
+ steps: [workflowStepDto({status: 'running'})],
368
+ }),
324
369
  workflowJobExecutionDto({status: 'succeeded'}),
325
370
  workflowJobExecutionDto({status: 'succeeded'}),
326
371
  workflowJobExecutionDto({status: 'succeeded'}),
@@ -331,7 +376,7 @@ describe('JobNode execution count indicator', () => {
331
376
 
332
377
  renderNode(node);
333
378
 
334
- const button = screen.getByRole('button', {name: 'release-gates, Running, 8 executions'});
379
+ const button = screen.getByRole('button', {name: 'release-gates, Listening, 8 executions'});
335
380
  expect(within(button).getByText('8')).toBeInTheDocument();
336
381
  expect(within(button).queryByText('8 exec')).not.toBeInTheDocument();
337
382
  expect(button.querySelector('[data-execution-status-segment]')).not.toBeInTheDocument();
@@ -9,7 +9,14 @@ import {cn} from '@shipfox/react-ui/utils';
9
9
  import type {KeyboardEventHandler, Ref} from 'react';
10
10
  import {getWorkflowStatusVisual} from '#components/workflow-status/status-visuals.js';
11
11
  import {WorkflowStatusIcon} from '#components/workflow-status/workflow-status-icon.js';
12
- import type {JobExecution, JobExecutionStatus, WorkflowRunDetail} from '#core/workflow-run.js';
12
+ import {
13
+ defaultJobExecution,
14
+ deriveJobDisplayStatus,
15
+ deriveJobExecutionDisplayStatus,
16
+ type JobExecution,
17
+ type JobExecutionStatus,
18
+ type WorkflowRunDetail,
19
+ } from '#core/workflow-run.js';
13
20
  import type {JobGraphNode} from './graph-model.js';
14
21
  import {formatJobDurationAccessibleLabel} from './job-duration-format.js';
15
22
  import {JobDurationLabel} from './job-duration-label.js';
@@ -69,16 +76,16 @@ export function JobNode({
69
76
  ref?: Ref<HTMLButtonElement>;
70
77
  }) {
71
78
  useTimeTick();
72
- const status =
73
- node.status === 'pending' &&
74
- node.jobExecutions.some((jobExecution) => jobExecution.status === 'running')
75
- ? 'running'
76
- : node.status;
79
+ const status = deriveJobDisplayStatus(node);
80
+ const execution = defaultJobExecution(node);
77
81
  const visual = getWorkflowStatusVisual(status);
78
82
  const accessibleLabel = [
79
83
  node.displayName,
80
84
  visual.label,
81
- formatJobDurationAccessibleLabel(node.displayDuration),
85
+ formatJobDurationAccessibleLabel(
86
+ node.displayDuration,
87
+ execution ? deriveJobExecutionDisplayStatus(execution) : undefined,
88
+ ),
82
89
  node.executionCountVisible
83
90
  ? executionCountAccessibleLabel(node.jobExecutions.length)
84
91
  : undefined,
@@ -111,7 +118,7 @@ export function JobNode({
111
118
  />
112
119
  ) : null}
113
120
  <div className="flex min-w-0 flex-1 items-center gap-8">
114
- <WorkflowStatusIcon status={status} jobMode={node.mode} size={14} />
121
+ <WorkflowStatusIcon status={status} size={14} />
115
122
  <JobLabel label={node.displayName} />
116
123
  </div>
117
124
  <JobDurationLabel duration={node.displayDuration} />
@@ -199,7 +206,7 @@ const EXECUTION_STATUSES = [
199
206
  function executionStatusCounts(executions: JobExecution[]) {
200
207
  return executions.reduce(
201
208
  (counts, execution) => {
202
- counts[execution.status] += 1;
209
+ counts[deriveJobExecutionDisplayStatus(execution)] += 1;
203
210
  return counts;
204
211
  },
205
212
  {pending: 0, running: 0, succeeded: 0, failed: 0, cancelled: 0},
@@ -21,7 +21,6 @@ import {
21
21
  import {AgentConfigFailureCallout as AgentConfigFailureCalloutView} from '../workflow-run-view/agent-config-failure-callout.js';
22
22
  import {StepList} from './step-list.js';
23
23
 
24
- const WORKSPACE_ID = '44444444-4444-4444-8444-444444444444';
25
24
  const AGENTS_LINK_NAME = 'Configure Agents';
26
25
 
27
26
  const meta = {
@@ -70,7 +69,7 @@ const withAgentSettingsRoute: Decorator = (Story) => {
70
69
  });
71
70
  const agentSettingsRoute = createRoute({
72
71
  getParentRoute: () => rootRoute,
73
- path: '/workspaces/$wid/settings/agents',
72
+ path: '/w/$workspaceSlug/settings/agents',
74
73
  component: () => null,
75
74
  });
76
75
  const router = createRouter({
@@ -390,7 +389,7 @@ function renderAgentConfigFailureCallout() {
390
389
  defaultSelectedAttemptId={attempt.id}
391
390
  renderExpandedStep={() => (
392
391
  <AgentConfigFailureCalloutView
393
- workspaceId={WORKSPACE_ID}
392
+ workspaceSlug="acme"
394
393
  config={{provider: 'anthropic', model: 'claude-opus-4-8', thinking: 'high'}}
395
394
  error={{
396
395
  message: 'Model provider credentials are not configured',
@@ -18,6 +18,7 @@ import {WorkflowStatusIcon} from '#components/workflow-status/workflow-status-ic
18
18
  import {
19
19
  isWorkflowStatus,
20
20
  type Job,
21
+ type JobDisplayStatus,
21
22
  type JobExecution,
22
23
  type Step,
23
24
  type StepSourceLocation,
@@ -47,7 +48,7 @@ export interface StepExpandedContext {
47
48
  export interface StepListEmptyState {
48
49
  title: string;
49
50
  description: string;
50
- status?: Job['status'] | undefined;
51
+ status?: JobDisplayStatus | undefined;
51
52
  }
52
53
 
53
54
  export interface StepListProps {
@@ -267,7 +268,7 @@ function StepListEmptyStateView({
267
268
  );
268
269
  }
269
270
 
270
- function StepListEmptyStateIcon({status}: {status: Job['status']}) {
271
+ function StepListEmptyStateIcon({status}: {status: JobDisplayStatus}) {
271
272
  if (status !== 'running') {
272
273
  return (
273
274
  <div className="flex size-32 items-center justify-center rounded-6 border border-border-neutral-strong bg-background-neutral-base p-8">