@shipfox/client-workflows 14.0.1 → 16.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/.storybook/preview.css +4 -0
  2. package/.storybook/preview.tsx +7 -2
  3. package/.turbo/turbo-build.log +1 -1
  4. package/CHANGELOG.md +42 -0
  5. package/dist/components/job-detail/agent-config-failure-callout.d.ts.map +1 -0
  6. package/dist/components/{workflow-run-view → job-detail}/agent-config-failure-callout.js +2 -2
  7. package/dist/components/job-detail/agent-config-failure-callout.js.map +1 -0
  8. package/dist/components/job-detail/job-detail-header.d.ts +15 -0
  9. package/dist/components/job-detail/job-detail-header.d.ts.map +1 -0
  10. package/dist/components/job-detail/job-detail-header.js +124 -0
  11. package/dist/components/job-detail/job-detail-header.js.map +1 -0
  12. package/dist/components/job-detail/job-detail-view.d.ts +16 -0
  13. package/dist/components/job-detail/job-detail-view.d.ts.map +1 -0
  14. package/dist/components/job-detail/job-detail-view.js +420 -0
  15. package/dist/components/job-detail/job-detail-view.js.map +1 -0
  16. package/dist/components/job-detail/job-empty-states.d.ts +10 -0
  17. package/dist/components/job-detail/job-empty-states.d.ts.map +1 -0
  18. package/dist/components/job-detail/job-empty-states.js +195 -0
  19. package/dist/components/job-detail/job-empty-states.js.map +1 -0
  20. package/dist/components/job-detail/job-execution-switcher.d.ts.map +1 -0
  21. package/dist/components/job-detail/job-execution-switcher.js.map +1 -0
  22. package/dist/components/job-detail/job-execution-time-text.d.ts.map +1 -0
  23. package/dist/components/job-detail/job-execution-time-text.js.map +1 -0
  24. package/dist/components/job-detail/job-selection.d.ts +24 -0
  25. package/dist/components/job-detail/job-selection.d.ts.map +1 -0
  26. package/dist/components/job-detail/job-selection.js +66 -0
  27. package/dist/components/job-detail/job-selection.js.map +1 -0
  28. package/dist/components/job-detail/step-attempt-log-panel.d.ts +15 -0
  29. package/dist/components/job-detail/step-attempt-log-panel.d.ts.map +1 -0
  30. package/dist/components/{workflow-run-view → job-detail}/step-attempt-log-panel.js +61 -16
  31. package/dist/components/job-detail/step-attempt-log-panel.js.map +1 -0
  32. package/dist/components/job-graph/job-graph-content.js +1 -1
  33. package/dist/components/job-graph/job-graph-content.js.map +1 -1
  34. package/dist/components/step-list/step-list-model.js +2 -2
  35. package/dist/components/step-list/step-list-model.js.map +1 -1
  36. package/dist/components/step-list/step-list.d.ts +1 -0
  37. package/dist/components/step-list/step-list.d.ts.map +1 -1
  38. package/dist/components/step-list/step-list.js +29 -4
  39. package/dist/components/step-list/step-list.js.map +1 -1
  40. package/dist/components/workflow-run-duration-label.d.ts +9 -4
  41. package/dist/components/workflow-run-duration-label.d.ts.map +1 -1
  42. package/dist/components/workflow-run-duration-label.js +27 -7
  43. package/dist/components/workflow-run-duration-label.js.map +1 -1
  44. package/dist/components/workflow-run-list/workflow-run-row.d.ts.map +1 -1
  45. package/dist/components/workflow-run-list/workflow-run-row.js +6 -5
  46. package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
  47. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
  48. package/dist/components/workflow-run-summary/workflow-run-summary.js +69 -64
  49. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
  50. package/dist/components/workflow-run-tabs/index.d.ts +3 -3
  51. package/dist/components/workflow-run-tabs/index.d.ts.map +1 -1
  52. package/dist/components/workflow-run-tabs/index.js +3 -3
  53. package/dist/components/workflow-run-tabs/index.js.map +1 -1
  54. package/dist/components/workflow-run-tabs/run-annotation-count-chip.d.ts +16 -0
  55. package/dist/components/workflow-run-tabs/run-annotation-count-chip.d.ts.map +1 -0
  56. package/dist/components/workflow-run-tabs/run-annotation-count-chip.js +50 -0
  57. package/dist/components/workflow-run-tabs/run-annotation-count-chip.js.map +1 -0
  58. package/dist/components/workflow-run-tabs/run-annotation-item.d.ts +20 -0
  59. package/dist/components/workflow-run-tabs/run-annotation-item.d.ts.map +1 -0
  60. package/dist/components/workflow-run-tabs/run-annotation-item.js +102 -0
  61. package/dist/components/workflow-run-tabs/run-annotation-item.js.map +1 -0
  62. package/dist/components/workflow-run-tabs/run-annotation-list.d.ts +32 -0
  63. package/dist/components/workflow-run-tabs/run-annotation-list.d.ts.map +1 -0
  64. package/dist/components/workflow-run-tabs/run-annotation-list.js +202 -0
  65. package/dist/components/workflow-run-tabs/run-annotation-list.js.map +1 -0
  66. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts +1 -1
  67. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts.map +1 -1
  68. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js +87 -22
  69. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js.map +1 -1
  70. package/dist/components/workflow-run-tabs/run-annotations-empty.d.ts +3 -1
  71. package/dist/components/workflow-run-tabs/run-annotations-empty.d.ts.map +1 -1
  72. package/dist/components/workflow-run-tabs/run-annotations-empty.js +2 -2
  73. package/dist/components/workflow-run-tabs/run-annotations-empty.js.map +1 -1
  74. package/dist/components/workflow-run-tabs/severity-visuals.d.ts +20 -0
  75. package/dist/components/workflow-run-tabs/severity-visuals.d.ts.map +1 -0
  76. package/dist/components/workflow-run-tabs/severity-visuals.js +31 -0
  77. package/dist/components/workflow-run-tabs/severity-visuals.js.map +1 -0
  78. package/dist/components/workflow-run-view/index.d.ts +0 -1
  79. package/dist/components/workflow-run-view/index.d.ts.map +1 -1
  80. package/dist/components/workflow-run-view/index.js +0 -1
  81. package/dist/components/workflow-run-view/index.js.map +1 -1
  82. package/dist/components/workflow-run-view/run-workspace-nav.d.ts +20 -0
  83. package/dist/components/workflow-run-view/run-workspace-nav.d.ts.map +1 -0
  84. package/dist/components/workflow-run-view/run-workspace-nav.js +277 -0
  85. package/dist/components/workflow-run-view/run-workspace-nav.js.map +1 -0
  86. package/dist/components/workflow-run-view/workflow-run-selection.d.ts.map +1 -1
  87. package/dist/components/workflow-run-view/workflow-run-selection.js +1 -14
  88. package/dist/components/workflow-run-view/workflow-run-selection.js.map +1 -1
  89. package/dist/components/workflow-run-view/workflow-run-view.d.ts +10 -10
  90. package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
  91. package/dist/components/workflow-run-view/workflow-run-view.js +351 -239
  92. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
  93. package/dist/components/workflow-status/status-visuals.d.ts.map +1 -1
  94. package/dist/components/workflow-status/status-visuals.js +10 -2
  95. package/dist/components/workflow-status/status-visuals.js.map +1 -1
  96. package/dist/components/workflow-status/workflow-status-icon.d.ts +3 -1
  97. package/dist/components/workflow-status/workflow-status-icon.d.ts.map +1 -1
  98. package/dist/components/workflow-status/workflow-status-icon.js +13 -2
  99. package/dist/components/workflow-status/workflow-status-icon.js.map +1 -1
  100. package/dist/core/entities/job-execution.d.ts +9 -0
  101. package/dist/core/entities/job-execution.d.ts.map +1 -1
  102. package/dist/core/entities/job-execution.js +13 -3
  103. package/dist/core/entities/job-execution.js.map +1 -1
  104. package/dist/core/entities/job.d.ts.map +1 -1
  105. package/dist/core/entities/step-attempt.d.ts.map +1 -1
  106. package/dist/core/entities/step.d.ts +2 -0
  107. package/dist/core/entities/step.d.ts.map +1 -1
  108. package/dist/core/entities/step.js +13 -0
  109. package/dist/core/entities/step.js.map +1 -1
  110. package/dist/core/entities/workflow-run-attempt.d.ts.map +1 -1
  111. package/dist/core/entities/workflow-run.d.ts +56 -1
  112. package/dist/core/entities/workflow-run.d.ts.map +1 -1
  113. package/dist/core/entities/workflow-run.js +98 -0
  114. package/dist/core/entities/workflow-run.js.map +1 -1
  115. package/dist/core/run-annotation.d.ts +73 -0
  116. package/dist/core/run-annotation.d.ts.map +1 -0
  117. package/dist/core/run-annotation.js +125 -0
  118. package/dist/core/run-annotation.js.map +1 -0
  119. package/dist/core/workflow-run.d.ts +4 -4
  120. package/dist/core/workflow-run.d.ts.map +1 -1
  121. package/dist/core/workflow-run.js +3 -3
  122. package/dist/core/workflow-run.js.map +1 -1
  123. package/dist/feature.d.ts +20 -16
  124. package/dist/feature.d.ts.map +1 -1
  125. package/dist/feature.js +5 -0
  126. package/dist/feature.js.map +1 -1
  127. package/dist/hooks/api/run-annotation-mapper.d.ts +10 -0
  128. package/dist/hooks/api/run-annotation-mapper.d.ts.map +1 -0
  129. package/dist/hooks/api/run-annotation-mapper.js +22 -0
  130. package/dist/hooks/api/run-annotation-mapper.js.map +1 -0
  131. package/dist/hooks/api/run-annotations.d.ts +25 -0
  132. package/dist/hooks/api/run-annotations.d.ts.map +1 -0
  133. package/dist/hooks/api/run-annotations.js +78 -0
  134. package/dist/hooks/api/run-annotations.js.map +1 -0
  135. package/dist/hooks/api/workflow-runs.d.ts +5 -3
  136. package/dist/hooks/api/workflow-runs.d.ts.map +1 -1
  137. package/dist/hooks/api/workflow-runs.js +5 -4
  138. package/dist/hooks/api/workflow-runs.js.map +1 -1
  139. package/dist/index.d.ts +1 -0
  140. package/dist/index.d.ts.map +1 -1
  141. package/dist/index.js +1 -0
  142. package/dist/index.js.map +1 -1
  143. package/dist/pages/workflow-job-detail-page.d.ts +11 -0
  144. package/dist/pages/workflow-job-detail-page.d.ts.map +1 -0
  145. package/dist/pages/workflow-job-detail-page.js +66 -0
  146. package/dist/pages/workflow-job-detail-page.js.map +1 -0
  147. package/dist/pages/workflow-run-detail-page.d.ts.map +1 -1
  148. package/dist/pages/workflow-run-detail-page.js +4 -37
  149. package/dist/pages/workflow-run-detail-page.js.map +1 -1
  150. package/dist/routes/inputs.d.ts +52 -34
  151. package/dist/routes/inputs.d.ts.map +1 -1
  152. package/dist/routes/inputs.js +74 -43
  153. package/dist/routes/inputs.js.map +1 -1
  154. package/dist/routes/job-detail.d.ts +10 -0
  155. package/dist/routes/job-detail.d.ts.map +1 -0
  156. package/dist/routes/job-detail.js +27 -0
  157. package/dist/routes/job-detail.js.map +1 -0
  158. package/dist/routes/project-route.d.ts +1 -1
  159. package/dist/routes/run-detail.d.ts.map +1 -1
  160. package/dist/routes/runs.d.ts.map +1 -1
  161. package/dist/routes/workflows.d.ts.map +1 -1
  162. package/dist/tsconfig.test.tsbuildinfo +1 -1
  163. package/package.json +10 -9
  164. package/src/components/{workflow-run-view → job-detail}/agent-config-failure-callout.stories.tsx +1 -1
  165. package/src/components/{workflow-run-view → job-detail}/agent-config-failure-callout.tsx +2 -2
  166. package/src/components/job-detail/job-detail-header.test.tsx +34 -0
  167. package/src/components/job-detail/job-detail-header.tsx +136 -0
  168. package/src/components/job-detail/job-detail-view.stories.tsx +531 -0
  169. package/src/components/job-detail/job-detail-view.tsx +467 -0
  170. package/src/components/job-detail/job-empty-states.test.ts +27 -0
  171. package/src/components/job-detail/job-empty-states.tsx +248 -0
  172. package/src/components/job-detail/job-selection.test.ts +188 -0
  173. package/src/components/job-detail/job-selection.ts +96 -0
  174. package/src/components/{workflow-run-view → job-detail}/step-attempt-log-panel.test.tsx +32 -9
  175. package/src/components/{workflow-run-view → job-detail}/step-attempt-log-panel.tsx +52 -18
  176. package/src/components/job-graph/job-graph-content.tsx +1 -1
  177. package/src/components/step-list/step-list-model.test.ts +1 -1
  178. package/src/components/step-list/step-list-model.ts +2 -2
  179. package/src/components/step-list/step-list.stories.tsx +1 -1
  180. package/src/components/step-list/step-list.test.tsx +33 -0
  181. package/src/components/step-list/step-list.tsx +40 -9
  182. package/src/components/workflow-run-duration-label.tsx +50 -12
  183. package/src/components/workflow-run-list/workflow-run-row.tsx +6 -4
  184. package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +38 -0
  185. package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +79 -5
  186. package/src/components/workflow-run-summary/workflow-run-summary.tsx +56 -60
  187. package/src/components/workflow-run-tabs/index.ts +3 -3
  188. package/src/components/workflow-run-tabs/run-annotation-count-chip.tsx +63 -0
  189. package/src/components/workflow-run-tabs/run-annotation-item.tsx +123 -0
  190. package/src/components/workflow-run-tabs/run-annotation-list.tsx +258 -0
  191. package/src/components/workflow-run-tabs/run-annotation-summary-line.test.tsx +77 -3
  192. package/src/components/workflow-run-tabs/run-annotation-summary-line.tsx +92 -17
  193. package/src/components/workflow-run-tabs/run-annotations-empty.tsx +6 -2
  194. package/src/components/workflow-run-tabs/run-annotations.stories.tsx +293 -0
  195. package/src/components/workflow-run-tabs/severity-visuals.ts +37 -0
  196. package/src/components/workflow-run-view/index.ts +0 -1
  197. package/src/components/workflow-run-view/run-workspace-nav.test.tsx +286 -0
  198. package/src/components/workflow-run-view/run-workspace-nav.tsx +314 -0
  199. package/src/components/workflow-run-view/workflow-run-selection.ts +1 -23
  200. package/src/components/workflow-run-view/workflow-run-view.test.tsx +237 -1085
  201. package/src/components/workflow-run-view/workflow-run-view.tsx +450 -323
  202. package/src/components/workflow-status/status-visuals.ts +5 -2
  203. package/src/components/workflow-status/workflow-status-icon.stories.tsx +1 -0
  204. package/src/components/workflow-status/workflow-status-icon.tsx +26 -1
  205. package/src/core/entities/job-execution.ts +17 -2
  206. package/src/core/entities/step.ts +26 -0
  207. package/src/core/entities/workflow-run-display.test.ts +159 -0
  208. package/src/core/entities/workflow-run.ts +139 -1
  209. package/src/core/run-annotation.test.ts +315 -0
  210. package/src/core/run-annotation.ts +238 -0
  211. package/src/core/workflow-run.ts +16 -1
  212. package/src/feature.ts +5 -0
  213. package/src/hooks/api/run-annotation-mapper.ts +30 -0
  214. package/src/hooks/api/run-annotations.ts +137 -0
  215. package/src/hooks/api/workflow-runs.ts +6 -2
  216. package/src/index.ts +1 -0
  217. package/src/page-harness-budget.test.ts +1 -0
  218. package/src/pages/workflow-job-detail-page.test.tsx +605 -0
  219. package/src/pages/workflow-job-detail-page.tsx +84 -0
  220. package/src/pages/workflow-run-detail-page.tsx +3 -43
  221. package/src/pages/workflow-run-pages.test.tsx +77 -332
  222. package/src/routes/inputs.test.ts +56 -6
  223. package/src/routes/inputs.ts +71 -19
  224. package/src/routes/job-detail.tsx +29 -0
  225. package/test/pages.tsx +25 -3
  226. package/test/render.tsx +6 -1
  227. package/tsconfig.build.tsbuildinfo +1 -1
  228. package/dist/components/workflow-run-tabs/run-jobs-list.d.ts +0 -18
  229. package/dist/components/workflow-run-tabs/run-jobs-list.d.ts.map +0 -1
  230. package/dist/components/workflow-run-tabs/run-jobs-list.js +0 -107
  231. package/dist/components/workflow-run-tabs/run-jobs-list.js.map +0 -1
  232. package/dist/components/workflow-run-tabs/run-tab-count.d.ts +0 -6
  233. package/dist/components/workflow-run-tabs/run-tab-count.d.ts.map +0 -1
  234. package/dist/components/workflow-run-tabs/run-tab-count.js +0 -27
  235. package/dist/components/workflow-run-tabs/run-tab-count.js.map +0 -1
  236. package/dist/components/workflow-run-tabs/run-tab-strip.d.ts +0 -8
  237. package/dist/components/workflow-run-tabs/run-tab-strip.d.ts.map +0 -1
  238. package/dist/components/workflow-run-tabs/run-tab-strip.js +0 -82
  239. package/dist/components/workflow-run-tabs/run-tab-strip.js.map +0 -1
  240. package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts.map +0 -1
  241. package/dist/components/workflow-run-view/agent-config-failure-callout.js.map +0 -1
  242. package/dist/components/workflow-run-view/job-card.d.ts +0 -17
  243. package/dist/components/workflow-run-view/job-card.d.ts.map +0 -1
  244. package/dist/components/workflow-run-view/job-card.js +0 -496
  245. package/dist/components/workflow-run-view/job-card.js.map +0 -1
  246. package/dist/components/workflow-run-view/job-execution-switcher.d.ts.map +0 -1
  247. package/dist/components/workflow-run-view/job-execution-switcher.js.map +0 -1
  248. package/dist/components/workflow-run-view/job-execution-time-text.d.ts.map +0 -1
  249. package/dist/components/workflow-run-view/job-execution-time-text.js.map +0 -1
  250. package/dist/components/workflow-run-view/step-attempt-log-panel.d.ts +0 -9
  251. package/dist/components/workflow-run-view/step-attempt-log-panel.d.ts.map +0 -1
  252. package/dist/components/workflow-run-view/step-attempt-log-panel.js.map +0 -1
  253. package/dist/core/workflow-run-tabs.d.ts +0 -9
  254. package/dist/core/workflow-run-tabs.d.ts.map +0 -1
  255. package/dist/core/workflow-run-tabs.js +0 -3
  256. package/dist/core/workflow-run-tabs.js.map +0 -1
  257. package/src/components/workflow-run-tabs/run-jobs-list.tsx +0 -151
  258. package/src/components/workflow-run-tabs/run-tab-count.tsx +0 -28
  259. package/src/components/workflow-run-tabs/run-tab-strip.stories.tsx +0 -52
  260. package/src/components/workflow-run-tabs/run-tab-strip.test.tsx +0 -117
  261. package/src/components/workflow-run-tabs/run-tab-strip.tsx +0 -97
  262. package/src/components/workflow-run-view/job-card.stories.tsx +0 -789
  263. package/src/components/workflow-run-view/job-card.test.tsx +0 -115
  264. package/src/components/workflow-run-view/job-card.tsx +0 -614
  265. package/src/core/workflow-run-tabs.ts +0 -8
  266. /package/dist/components/{workflow-run-view → job-detail}/agent-config-failure-callout.d.ts +0 -0
  267. /package/dist/components/{workflow-run-view → job-detail}/job-execution-switcher.d.ts +0 -0
  268. /package/dist/components/{workflow-run-view → job-detail}/job-execution-switcher.js +0 -0
  269. /package/dist/components/{workflow-run-view → job-detail}/job-execution-time-text.d.ts +0 -0
  270. /package/dist/components/{workflow-run-view → job-detail}/job-execution-time-text.js +0 -0
  271. /package/src/components/{workflow-run-view → job-detail}/job-execution-switcher.tsx +0 -0
  272. /package/src/components/{workflow-run-view → job-detail}/job-execution-time-text.tsx +0 -0
@@ -0,0 +1,293 @@
1
+ import type {Meta, StoryObj} from '@storybook/react';
2
+ import type {ReactNode} from 'react';
3
+ import {
4
+ buildRunAnnotationList,
5
+ type RunAnnotationRecord,
6
+ type RunAnnotationStyle,
7
+ summarizeRunAnnotations,
8
+ } from '#core/run-annotation.js';
9
+ import type {Job} from '#core/workflow-run.js';
10
+ import {
11
+ workflowJob,
12
+ workflowJobExecutionDto,
13
+ workflowStepAttemptDto,
14
+ workflowStepDto,
15
+ } from '#test/fixtures/workflow-run.js';
16
+ import {RunAnnotationCountChip} from './run-annotation-count-chip.js';
17
+ import {RunAnnotationList, type RunAnnotationListQuery} from './run-annotation-list.js';
18
+ import {RunAnnotationSummaryLine} from './run-annotation-summary-line.js';
19
+
20
+ const WORKSPACE_SLUG = 'acme';
21
+ const PROJECT_SLUG = 'platform';
22
+ const RUN_ID = '11111111-1111-4111-8111-111111111111';
23
+ const BUILD_JOB_ID = '44444444-4444-4444-8444-00000000000b';
24
+ const TEST_JOB_ID = '44444444-4444-4444-8444-00000000000c';
25
+ const BUILD_EXECUTION_ID = '77777777-7777-4777-8777-00000000000b';
26
+ const TEST_EXECUTION_ID = '77777777-7777-4777-8777-00000000000c';
27
+ const BUILD_STEP_ID = '55555555-5555-4555-8555-00000000000b';
28
+ const TEST_STEP_ID = '55555555-5555-4555-8555-00000000000c';
29
+ const BUILD_ATTEMPT_ID = '66666666-6666-4666-8666-00000000000b';
30
+ const TEST_ATTEMPT_ID = '66666666-6666-4666-8666-00000000000c';
31
+
32
+ const LONG_BODY = [
33
+ '### Failing specs',
34
+ '',
35
+ ...Array.from(
36
+ {length: 40},
37
+ (_unused, index) => `- \`packages/web/src/route-${index}.test.ts\` expected 200, received 503`,
38
+ ),
39
+ ].join('\n');
40
+
41
+ const meta = {
42
+ title: 'Workflows/RunAnnotations',
43
+ parameters: {
44
+ layout: 'fullscreen',
45
+ },
46
+ } satisfies Meta;
47
+
48
+ export default meta;
49
+ type Story = StoryObj<typeof meta>;
50
+
51
+ export const Playground: Story = {
52
+ render: () => {
53
+ const annotations = [
54
+ annotation({
55
+ id: 'a1',
56
+ context: 'smoke check',
57
+ style: 'error',
58
+ sequence: 3,
59
+ body: 'Task nine failed the smoke check against `https://preview.example.com`.\n\n```sh\ncurl -sSf https://preview.example.com/healthz\n```',
60
+ }),
61
+ annotation({
62
+ id: 'a2',
63
+ context: 'flaky tests',
64
+ style: 'warning',
65
+ sequence: 1,
66
+ jobId: TEST_JOB_ID,
67
+ jobExecutionId: TEST_EXECUTION_ID,
68
+ originStepId: TEST_STEP_ID,
69
+ body: '2 specs retried before passing.\n\n| Spec | Retries |\n| --- | --- |\n| `checkout.spec.ts` | 1 |\n| `billing.spec.ts` | 2 |',
70
+ }),
71
+ annotation({
72
+ id: 'a3',
73
+ context: 'deploy',
74
+ style: 'success',
75
+ sequence: 2,
76
+ body: 'Deployed **v42** to staging: [preview](https://preview.example.com)',
77
+ }),
78
+ annotation({
79
+ id: 'a4',
80
+ context: 'coverage',
81
+ style: 'info',
82
+ sequence: 4,
83
+ body: 'Line coverage held at 87.4%.',
84
+ }),
85
+ ];
86
+
87
+ return <AnnotationsFrame annotations={annotations} />;
88
+ },
89
+ };
90
+
91
+ export const BoundedBody: Story = {
92
+ // The only story in this package that earns a second Argos mode. The clamp's fade is the one
93
+ // gradient in the client, and it blends a surface token that flips with the theme, so a
94
+ // dark-only capture would never catch it breaking on a light card.
95
+ parameters: {
96
+ argos: {modes: {dark: {theme: 'dark'}, light: {theme: 'light'}}},
97
+ },
98
+ render: () => (
99
+ <AnnotationsFrame
100
+ annotations={[
101
+ annotation({id: 'long', context: 'test results', style: 'error', body: LONG_BODY}),
102
+ ]}
103
+ />
104
+ ),
105
+ };
106
+
107
+ export const DataStates: Story = {
108
+ render: () => {
109
+ const loaded = [annotation({id: 'a1', context: 'deploy', style: 'success'})];
110
+ return (
111
+ <div className="grid max-w-[1120px] gap-16 p-16 md:grid-cols-2">
112
+ <StateExample label="Loading">
113
+ <AnnotationsList annotations={[]} query={storyQuery({isPending: true})} />
114
+ </StateExample>
115
+ <StateExample label="Empty">
116
+ <AnnotationsList annotations={[]} />
117
+ </StateExample>
118
+ <StateExample label="Error">
119
+ <AnnotationsList
120
+ annotations={[]}
121
+ query={storyQuery({isError: true, data: undefined, error: new Error('Storybook')})}
122
+ />
123
+ </StateExample>
124
+ <StateExample label="Stale">
125
+ <AnnotationsList
126
+ annotations={loaded}
127
+ query={storyQuery({isError: true, error: new Error('Storybook')})}
128
+ />
129
+ </StateExample>
130
+ <StateExample label="Filtered miss">
131
+ <AnnotationsList annotations={[]} filtered filteredJobName="build" />
132
+ </StateExample>
133
+ <StateExample label="Truncated counts">
134
+ <RunAnnotationSummaryLine
135
+ summary={{...summarizeRunAnnotations(loaded), total: 500, error: 12, truncated: true}}
136
+ workspaceSlug={WORKSPACE_SLUG}
137
+ projectSlug={PROJECT_SLUG}
138
+ workflowRunId={RUN_ID}
139
+ />
140
+ </StateExample>
141
+ </div>
142
+ );
143
+ },
144
+ };
145
+
146
+ export const CountChips: Story = {
147
+ render: () => (
148
+ <div className="flex max-w-[1120px] flex-wrap items-center gap-12 p-16">
149
+ {(['error', 'warning', 'info', 'success'] as const).map((style) => (
150
+ <RunAnnotationCountChip
151
+ key={style}
152
+ summary={summarizeRunAnnotations([annotation({id: style, context: style, style})])}
153
+ workspaceSlug={WORKSPACE_SLUG}
154
+ projectSlug={PROJECT_SLUG}
155
+ workflowRunId={RUN_ID}
156
+ jobId={BUILD_JOB_ID}
157
+ />
158
+ ))}
159
+ <RunAnnotationCountChip
160
+ summary={{total: 500, error: 12, warning: 3, info: 0, success: 0, truncated: true}}
161
+ workspaceSlug={WORKSPACE_SLUG}
162
+ projectSlug={PROJECT_SLUG}
163
+ workflowRunId={RUN_ID}
164
+ />
165
+ </div>
166
+ ),
167
+ };
168
+
169
+ function AnnotationsFrame({annotations}: {annotations: RunAnnotationRecord[]}) {
170
+ return (
171
+ <div className="min-h-screen bg-background-neutral-base py-16">
172
+ <div className="mx-auto flex w-full max-w-[1120px] flex-col gap-16 px-24">
173
+ <RunAnnotationSummaryLine
174
+ summary={summarizeRunAnnotations(annotations)}
175
+ workspaceSlug={WORKSPACE_SLUG}
176
+ projectSlug={PROJECT_SLUG}
177
+ workflowRunId={RUN_ID}
178
+ />
179
+ <AnnotationsList annotations={annotations} />
180
+ </div>
181
+ </div>
182
+ );
183
+ }
184
+
185
+ function AnnotationsList({
186
+ annotations,
187
+ query = storyQuery(),
188
+ filtered = false,
189
+ filteredJobName,
190
+ }: {
191
+ annotations: RunAnnotationRecord[];
192
+ query?: RunAnnotationListQuery;
193
+ filtered?: boolean;
194
+ filteredJobName?: string;
195
+ }) {
196
+ return (
197
+ <RunAnnotationList
198
+ query={query}
199
+ entries={query.data === undefined ? undefined : buildRunAnnotationList({annotations, jobs})}
200
+ workspaceSlug={WORKSPACE_SLUG}
201
+ projectSlug={PROJECT_SLUG}
202
+ workflowRunId={RUN_ID}
203
+ runAttempt={1}
204
+ filtered={filtered}
205
+ filteredJobName={filteredJobName}
206
+ onClearFilters={() => undefined}
207
+ />
208
+ );
209
+ }
210
+
211
+ function StateExample({label, children}: {label: string; children: ReactNode}) {
212
+ return (
213
+ <div className="flex flex-col gap-8">
214
+ <span className="text-xs font-medium text-foreground-neutral-muted">{label}</span>
215
+ {children}
216
+ </div>
217
+ );
218
+ }
219
+
220
+ function storyQuery(overrides: Partial<RunAnnotationListQuery> = {}): RunAnnotationListQuery {
221
+ return {
222
+ isPending: false,
223
+ isError: false,
224
+ isFetching: false,
225
+ data: {pages: []},
226
+ error: null,
227
+ refetch: () => undefined,
228
+ hasNextPage: false,
229
+ isFetchingNextPage: false,
230
+ fetchNextPage: () => undefined,
231
+ ...overrides,
232
+ };
233
+ }
234
+
235
+ function annotation(
236
+ overrides: Partial<RunAnnotationRecord> & {id: string; style: RunAnnotationStyle},
237
+ ): RunAnnotationRecord {
238
+ return {
239
+ jobId: BUILD_JOB_ID,
240
+ jobExecutionId: BUILD_EXECUTION_ID,
241
+ originStepId: BUILD_STEP_ID,
242
+ originStepAttempt: 1,
243
+ context: 'default',
244
+ sequence: 1,
245
+ body: 'Body',
246
+ ...overrides,
247
+ };
248
+ }
249
+
250
+ const jobs: Job[] = [
251
+ workflowJob({
252
+ id: BUILD_JOB_ID,
253
+ key: 'build',
254
+ name: 'build',
255
+ position: 0,
256
+ job_executions: [
257
+ workflowJobExecutionDto({
258
+ id: BUILD_EXECUTION_ID,
259
+ job_id: BUILD_JOB_ID,
260
+ steps: [
261
+ workflowStepDto({
262
+ id: BUILD_STEP_ID,
263
+ name: 'run smoke checks',
264
+ attempts: [
265
+ workflowStepAttemptDto({id: BUILD_ATTEMPT_ID, step_id: BUILD_STEP_ID, attempt: 1}),
266
+ ],
267
+ }),
268
+ ],
269
+ }),
270
+ ],
271
+ }),
272
+ workflowJob({
273
+ id: TEST_JOB_ID,
274
+ key: 'test',
275
+ name: 'test',
276
+ position: 1,
277
+ job_executions: [
278
+ workflowJobExecutionDto({
279
+ id: TEST_EXECUTION_ID,
280
+ job_id: TEST_JOB_ID,
281
+ steps: [
282
+ workflowStepDto({
283
+ id: TEST_STEP_ID,
284
+ name: 'vitest',
285
+ attempts: [
286
+ workflowStepAttemptDto({id: TEST_ATTEMPT_ID, step_id: TEST_STEP_ID, attempt: 1}),
287
+ ],
288
+ }),
289
+ ],
290
+ }),
291
+ ],
292
+ }),
293
+ ];
@@ -0,0 +1,37 @@
1
+ import type {IconName} from '@shipfox/react-ui/icon';
2
+ import type {RunAnnotationSeverity} from '#core/run-annotation.js';
3
+
4
+ /**
5
+ * The glyph vocabulary for annotation severity, shared by every count surface.
6
+ *
7
+ * Deliberately the same marks the `Callout` uses on the cards themselves, so the glyph beside
8
+ * `1 error` in the summary is the glyph on the error card below it.
9
+ */
10
+ export const SEVERITY_ICON: Record<RunAnnotationSeverity, IconName> = {
11
+ error: 'closeCircleFill',
12
+ warning: 'errorWarningFill',
13
+ info: 'info',
14
+ success: 'checkboxCircleFill',
15
+ };
16
+
17
+ /**
18
+ * Severity tone lives in the glyph, never in the label.
19
+ *
20
+ * Coloring the text would make severity compete with the brand accent, which this system
21
+ * reserves for links, focus, and "you are here". Shape plus tone in a 12px mark distinguishes
22
+ * an error from a warning without either of them shouting.
23
+ */
24
+ export const SEVERITY_ICON_TONE: Record<RunAnnotationSeverity, string> = {
25
+ error: 'text-tag-error-icon',
26
+ warning: 'text-tag-warning-icon',
27
+ info: 'text-tag-blue-icon',
28
+ success: 'text-tag-success-icon',
29
+ };
30
+
31
+ /** Hairline tone for a bordered count chip, matching the glyph it sits beside. */
32
+ export const SEVERITY_CHIP_TONE: Record<RunAnnotationSeverity, string> = {
33
+ error: 'border-tag-error-border text-tag-error-icon',
34
+ warning: 'border-tag-warning-border text-tag-warning-icon',
35
+ info: 'border-tag-blue-border text-tag-blue-icon',
36
+ success: 'border-tag-success-border text-tag-success-icon',
37
+ };
@@ -1,2 +1 @@
1
- export {JobCard} from './job-card.js';
2
1
  export {WorkflowRunView} from './workflow-run-view.js';
@@ -0,0 +1,286 @@
1
+ import {act, screen, within} from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import {useState} from 'react';
4
+ import {
5
+ workflowJobDto,
6
+ workflowJobExecutionDto,
7
+ workflowRunDetail,
8
+ } from '#test/fixtures/workflow-run.js';
9
+ import {renderWithRouter} from '#test/render.js';
10
+ import {RunWorkspaceNav} from './run-workspace-nav.js';
11
+
12
+ const RUN_ID = '66666666-6666-4666-8666-666666666666';
13
+ const CURRENT_JOB_ID = '88888888-8888-4888-8888-888888888888';
14
+ const BUILD_LINK_PATTERN = /build/;
15
+ const DEPLOY_LINK_PATTERN = /deploy/;
16
+ const NOW = Date.parse('2026-06-26T12:00:00.000Z');
17
+
18
+ describe('RunWorkspaceNav', () => {
19
+ beforeEach(() => {
20
+ Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', {
21
+ configurable: true,
22
+ value: vi.fn(),
23
+ });
24
+ });
25
+
26
+ afterEach(() => {
27
+ vi.useRealTimers();
28
+ });
29
+
30
+ test('announces the annotation count with its unit and marks a truncated read', async () => {
31
+ const run = workflowRunDetail({
32
+ id: RUN_ID,
33
+ jobs: [workflowJobDto({id: CURRENT_JOB_ID, name: 'build', position: 0})],
34
+ });
35
+
36
+ renderWithRouter(
37
+ <RunWorkspaceNav
38
+ workspaceSlug="acme"
39
+ projectSlug="project"
40
+ run={run}
41
+ activeSection="summary"
42
+ annotationSummary={{
43
+ total: 500,
44
+ error: 12,
45
+ warning: 3,
46
+ info: 0,
47
+ success: 0,
48
+ truncated: true,
49
+ }}
50
+ />,
51
+ );
52
+
53
+ expect(await screen.findByRole('link', {name: 'Annotations, 500 or more'})).toBeVisible();
54
+ });
55
+
56
+ test('omits the annotation count until the read resolves', async () => {
57
+ const run = workflowRunDetail({
58
+ id: RUN_ID,
59
+ jobs: [workflowJobDto({id: CURRENT_JOB_ID, name: 'build', position: 0})],
60
+ });
61
+
62
+ renderWithRouter(
63
+ <RunWorkspaceNav
64
+ workspaceSlug="acme"
65
+ projectSlug="project"
66
+ run={run}
67
+ activeSection="summary"
68
+ />,
69
+ );
70
+
71
+ expect(await screen.findByRole('link', {name: 'Annotations'})).toBeVisible();
72
+ });
73
+
74
+ test('shows the complete run hierarchy and marks the current job', async () => {
75
+ const run = workflowRunDetail({
76
+ id: RUN_ID,
77
+ jobs: [
78
+ workflowJobDto({id: 'job-deploy', name: 'deploy', position: 2, status: 'failed'}),
79
+ workflowJobDto({id: CURRENT_JOB_ID, name: 'build', position: 1, status: 'running'}),
80
+ workflowJobDto({id: 'job-setup', name: 'setup', position: 0, status: 'succeeded'}),
81
+ ],
82
+ });
83
+
84
+ renderWithRouter(
85
+ <RunWorkspaceNav
86
+ workspaceSlug="acme"
87
+ projectSlug="project"
88
+ run={run}
89
+ activeSection="summary"
90
+ currentJobId={CURRENT_JOB_ID}
91
+ jobSearch={{runAttempt: 2}}
92
+ />,
93
+ );
94
+
95
+ expect(await screen.findByRole('link', {name: 'Summary'})).toBeInTheDocument();
96
+ expect(screen.getByRole('heading', {name: 'Jobs'})).toBeInTheDocument();
97
+ expect(screen.getByRole('heading', {name: 'Run details'})).toBeInTheDocument();
98
+ expect(screen.getByRole('link', {name: 'Annotations'})).toBeInTheDocument();
99
+ expect(screen.getByRole('link', {name: 'Source'})).toBeInTheDocument();
100
+
101
+ const jobs = screen.getByRole('heading', {name: 'Jobs'}).closest('section');
102
+ if (!jobs) throw new Error('Missing jobs section');
103
+ expect(
104
+ within(jobs)
105
+ .getAllByRole('link')
106
+ .map((link) => link.textContent?.trim()),
107
+ ).toEqual([
108
+ expect.stringContaining('setup'),
109
+ expect.stringContaining('build'),
110
+ expect.stringContaining('deploy'),
111
+ ]);
112
+
113
+ const current = within(jobs).getByRole('link', {name: BUILD_LINK_PATTERN});
114
+ expect(current).toHaveAttribute('aria-current', 'page');
115
+ expect(current).toHaveAttribute(
116
+ 'href',
117
+ expect.stringContaining(`/runs/${RUN_ID}/jobs/${CURRENT_JOB_ID}`),
118
+ );
119
+ expect(current).not.toHaveAttribute('href', expect.stringContaining('tab='));
120
+ expect(current).toHaveAttribute('href', expect.stringContaining('runAttempt=%222%22'));
121
+ });
122
+
123
+ test('keeps the job index visible for a single-job run', async () => {
124
+ const run = workflowRunDetail({
125
+ id: RUN_ID,
126
+ jobs: [workflowJobDto({id: CURRENT_JOB_ID, name: 'build', status: 'succeeded'})],
127
+ });
128
+
129
+ renderWithRouter(
130
+ <RunWorkspaceNav
131
+ workspaceSlug="acme"
132
+ projectSlug="project"
133
+ run={run}
134
+ activeSection="summary"
135
+ />,
136
+ );
137
+
138
+ expect(await screen.findByRole('heading', {name: 'Jobs'})).toBeInTheDocument();
139
+ expect(screen.getByRole('link', {name: BUILD_LINK_PATTERN})).toBeInTheDocument();
140
+ expect(screen.getByRole('link', {name: 'Summary'})).toHaveAttribute('aria-current', 'page');
141
+ });
142
+
143
+ test('scrolls the current job into view when the mobile rail opens', async () => {
144
+ const user = userEvent.setup();
145
+ const scrollIntoView = vi.fn();
146
+ Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', {
147
+ configurable: true,
148
+ value: scrollIntoView,
149
+ });
150
+ const run = workflowRunDetail({
151
+ id: RUN_ID,
152
+ jobs: [workflowJobDto({id: CURRENT_JOB_ID, name: 'build', status: 'running'})],
153
+ });
154
+
155
+ renderWithRouter(
156
+ <RunWorkspaceNav
157
+ workspaceSlug="acme"
158
+ projectSlug="project"
159
+ run={run}
160
+ activeSection="summary"
161
+ currentJobId={CURRENT_JOB_ID}
162
+ />,
163
+ );
164
+
165
+ await screen.findByRole('link', {name: BUILD_LINK_PATTERN});
166
+ await user.click(await screen.findByRole('button', {name: 'Toggle run navigation'}));
167
+
168
+ expect(scrollIntoView).toHaveBeenCalledTimes(2);
169
+ });
170
+
171
+ test('closes the mobile rail after navigating to a job', async () => {
172
+ const user = userEvent.setup();
173
+ const run = workflowRunDetail({
174
+ id: RUN_ID,
175
+ jobs: [
176
+ workflowJobDto({id: CURRENT_JOB_ID, name: 'build', status: 'running'}),
177
+ workflowJobDto({id: 'job-deploy', name: 'deploy', position: 1, status: 'succeeded'}),
178
+ ],
179
+ });
180
+
181
+ renderWithRouter(
182
+ <RunWorkspaceNav
183
+ workspaceSlug="acme"
184
+ projectSlug="project"
185
+ run={run}
186
+ activeSection="summary"
187
+ currentJobId={CURRENT_JOB_ID}
188
+ />,
189
+ );
190
+
191
+ const trigger = await screen.findByRole('button', {name: 'Toggle run navigation'});
192
+ await user.click(trigger);
193
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
194
+
195
+ await user.click(screen.getByRole('link', {name: DEPLOY_LINK_PATTERN}));
196
+
197
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
198
+ });
199
+
200
+ test('scrolls once when the active job changes while the rail is open', async () => {
201
+ const user = userEvent.setup();
202
+ const scrollIntoView = vi.fn();
203
+ Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', {
204
+ configurable: true,
205
+ value: scrollIntoView,
206
+ });
207
+ const firstJob = workflowJobDto({id: CURRENT_JOB_ID, name: 'build', status: 'running'});
208
+ const secondJob = workflowJobDto({
209
+ id: 'job-deploy',
210
+ name: 'deploy',
211
+ position: 1,
212
+ status: 'running',
213
+ });
214
+ const initialRun = workflowRunDetail({id: RUN_ID, jobs: [firstJob, secondJob]});
215
+ renderWithRouter(<ActiveJobHarness run={initialRun} />);
216
+
217
+ await screen.findByRole('link', {name: BUILD_LINK_PATTERN});
218
+ await user.click(await screen.findByRole('button', {name: 'Toggle run navigation'}));
219
+ const callsBeforeJobChange = scrollIntoView.mock.calls.length;
220
+
221
+ await user.click(screen.getByRole('button', {name: 'Activate deploy'}));
222
+
223
+ expect(scrollIntoView).toHaveBeenCalledTimes(callsBeforeJobChange + 1);
224
+ });
225
+
226
+ test('updates live job durations on the ticker cadence', async () => {
227
+ vi.useFakeTimers({shouldAdvanceTime: true});
228
+ vi.setSystemTime(NOW);
229
+ const run = workflowRunDetail({
230
+ id: RUN_ID,
231
+ jobs: [
232
+ workflowJobDto({
233
+ id: CURRENT_JOB_ID,
234
+ name: 'build',
235
+ status: 'running',
236
+ job_executions: [
237
+ workflowJobExecutionDto({
238
+ job_id: CURRENT_JOB_ID,
239
+ status: 'running',
240
+ queued_at: '2026-06-26T11:54:00.000Z',
241
+ started_at: '2026-06-26T11:57:46.000Z',
242
+ }),
243
+ ],
244
+ }),
245
+ ],
246
+ });
247
+
248
+ renderWithRouter(
249
+ <RunWorkspaceNav
250
+ workspaceSlug="acme"
251
+ projectSlug="project"
252
+ run={run}
253
+ activeSection="summary"
254
+ currentJobId={CURRENT_JOB_ID}
255
+ />,
256
+ );
257
+
258
+ const jobLink = await screen.findByRole('link', {name: BUILD_LINK_PATTERN});
259
+ expect(jobLink).toHaveTextContent('2m 14s');
260
+
261
+ act(() => {
262
+ vi.advanceTimersByTime(1000);
263
+ });
264
+
265
+ expect(jobLink).toHaveTextContent('2m 15s');
266
+ });
267
+ });
268
+
269
+ function ActiveJobHarness({run}: {run: ReturnType<typeof workflowRunDetail>}) {
270
+ const [currentJobId, setCurrentJobId] = useState(CURRENT_JOB_ID);
271
+
272
+ return (
273
+ <>
274
+ <RunWorkspaceNav
275
+ workspaceSlug="acme"
276
+ projectSlug="project"
277
+ run={run}
278
+ activeSection="summary"
279
+ currentJobId={currentJobId}
280
+ />
281
+ <button type="button" onClick={() => setCurrentJobId('job-deploy')}>
282
+ Activate deploy
283
+ </button>
284
+ </>
285
+ );
286
+ }