@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
@@ -3,8 +3,12 @@ import {
3
3
  applyWorkflowRunFilterPatch,
4
4
  clearWorkflowRunFilters,
5
5
  hasWorkflowRunFilters,
6
+ validateWorkflowJobSearch,
6
7
  validateWorkflowRunsSearch,
8
+ type WorkflowJobSearch,
7
9
  type WorkflowRunsSearch,
10
+ workflowJobRouteParams,
11
+ workflowJobSearchParams,
8
12
  workflowRouteParams,
9
13
  workflowRunListSearchParams,
10
14
  workflowRunSearchParams,
@@ -62,12 +66,58 @@ describe('validateWorkflowRunsSearch', () => {
62
66
  expect(validateWorkflowRunsSearch({after: '2026-02-28'})).toEqual({after: '2026-02-28'});
63
67
  });
64
68
 
65
- test('keeps the run selection parameters the detail route owns', () => {
69
+ test('keeps legacy job selection on the run route', () => {
66
70
  expect(validateWorkflowRunsSearch({status: 'failed', runAttempt: '2', job: 'job-1'})).toEqual({
67
71
  status: ['failed'],
72
+ jobId: 'job-1',
68
73
  runAttempt: 2,
74
+ });
75
+ });
76
+ });
77
+
78
+ describe('the job detail URL contract', () => {
79
+ test('reads and writes job-scoped selection parameters', () => {
80
+ const search: WorkflowJobSearch = {
81
+ jobExecutionId: 'execution-1',
82
+ stepId: 'step-1',
83
+ stepAttemptId: 'attempt-2',
84
+ runAttempt: 3,
85
+ };
86
+ const query = stringifyAppSearch(workflowJobSearchParams(search));
87
+
88
+ expect(query).toBe(
89
+ '?jobExecution=execution-1&step=step-1&stepAttempt=attempt-2&runAttempt=%223%22',
90
+ );
91
+ expect(validateWorkflowJobSearch(parseAppSearch(query))).toEqual(search);
92
+ });
93
+
94
+ test('drops malformed job selection values', () => {
95
+ expect(
96
+ validateWorkflowJobSearch({
97
+ tab: 'annotations',
98
+ jobExecution: ['unexpected'],
99
+ step: '',
100
+ stepAttempt: 4,
101
+ runAttempt: '0',
102
+ }),
103
+ ).toEqual({});
104
+ });
105
+
106
+ test('requires all job route path parameters', () => {
107
+ expect(
108
+ workflowJobRouteParams({
109
+ workspaceSlug: 'workspace-1',
110
+ projectSlug: 'project-1',
111
+ workflowRunId: 'run-1',
112
+ jobId: 'job-1',
113
+ }),
114
+ ).toEqual({
115
+ workspaceSlug: 'workspace-1',
116
+ projectSlug: 'project-1',
117
+ workflowRunId: 'run-1',
69
118
  jobId: 'job-1',
70
119
  });
120
+ expect(() => workflowJobRouteParams({workspaceSlug: 'workspace-1'})).toThrow();
71
121
  });
72
122
 
73
123
  test('accepts known tabs and annotation severities while dropping unknown values', () => {
@@ -84,8 +134,8 @@ describe('validateWorkflowRunsSearch', () => {
84
134
  ['job execution', {jobExecutionId: 'execution-1'}],
85
135
  ['step', {stepId: 'step-1'}],
86
136
  ['step attempt', {stepAttemptId: 'attempt-1'}],
87
- ])('defaults a legacy %s selection URL to Jobs', (_selection, search) => {
88
- expect(workflowRunTab(search)).toBe('jobs');
137
+ ])('defaults a legacy %s selection URL to Summary', (_selection, search) => {
138
+ expect(workflowRunTab(search)).toBe('summary');
89
139
  });
90
140
 
91
141
  test('keeps an explicit Summary tab ahead of legacy selection inference', () => {
@@ -195,9 +245,9 @@ describe('applyWorkflowRunFilterPatch', () => {
195
245
  });
196
246
 
197
247
  test('leaves the run selection parameters alone', () => {
198
- const next = applyWorkflowRunFilterPatch({runAttempt: 2, jobId: 'job-1'}, {search: 'deploy'});
248
+ const next = applyWorkflowRunFilterPatch({runAttempt: 2}, {search: 'deploy'});
199
249
 
200
- expect(next).toEqual({runAttempt: 2, jobId: 'job-1', search: 'deploy'});
250
+ expect(next).toEqual({runAttempt: 2, search: 'deploy'});
201
251
  });
202
252
  });
203
253
 
@@ -232,7 +282,7 @@ describe('hasWorkflowRunFilters', () => {
232
282
  });
233
283
 
234
284
  test('does not count a run selection parameter as a filter', () => {
235
- expect(hasWorkflowRunFilters({runAttempt: 2, jobId: 'job-1'})).toBe(false);
285
+ expect(hasWorkflowRunFilters({runAttempt: 2})).toBe(false);
236
286
  });
237
287
  });
238
288
 
@@ -1,3 +1,4 @@
1
+ import {RUN_ANNOTATION_SEVERITIES, type RunAnnotationSeverity} from '#core/run-annotation.js';
1
2
  import type {WorkflowRunSelectionInput} from '#core/workflow-run-url-state.js';
2
3
 
3
4
  /**
@@ -12,9 +13,13 @@ export const WORKFLOW_RUN_TABS = ['summary', 'jobs', 'annotations', 'source'] as
12
13
 
13
14
  export type WorkflowRunTab = (typeof WORKFLOW_RUN_TABS)[number];
14
15
 
15
- export const WORKFLOW_RUN_ANNOTATION_SEVERITIES = ['error', 'warning', 'info', 'success'] as const;
16
+ /**
17
+ * The `severity` parameter's vocabulary is the display severity set, not a second list: a URL
18
+ * that could name a severity the list cannot rank would be a filter with no matching rows.
19
+ */
20
+ export const WORKFLOW_RUN_ANNOTATION_SEVERITIES = RUN_ANNOTATION_SEVERITIES;
16
21
 
17
- export type WorkflowRunAnnotationSeverity = (typeof WORKFLOW_RUN_ANNOTATION_SEVERITIES)[number];
22
+ export type WorkflowRunAnnotationSeverity = RunAnnotationSeverity;
18
23
 
19
24
  export interface WorkflowRunsSearch extends WorkflowRunSelectionInput {
20
25
  search?: string;
@@ -31,6 +36,8 @@ export interface WorkflowRunsSearch extends WorkflowRunSelectionInput {
31
36
  severity?: WorkflowRunAnnotationSeverity;
32
37
  }
33
38
 
39
+ export type WorkflowJobSearch = Omit<WorkflowRunSelectionInput, 'jobId'>;
40
+
34
41
  const STATUS_VALUES = new Set<string>(WORKFLOW_RUN_LIST_STATUSES);
35
42
  const TAB_VALUES = new Set<string>(WORKFLOW_RUN_TABS);
36
43
  const ANNOTATION_SEVERITY_VALUES = new Set<string>(WORKFLOW_RUN_ANNOTATION_SEVERITIES);
@@ -51,7 +58,6 @@ export function validateWorkflowRunsSearch(input: Record<string, unknown>): Work
51
58
  const event = repeatable(input.event);
52
59
  const after = calendarDate(input.after);
53
60
  const before = calendarDate(input.before);
54
- const runAttempt = positiveInteger(input.runAttempt);
55
61
  const tabValue = string(input.tab);
56
62
  const tab = tabValue && TAB_VALUES.has(tabValue) ? (tabValue as WorkflowRunTab) : undefined;
57
63
  const annotation = string(input.annotation);
@@ -72,14 +78,16 @@ export function validateWorkflowRunsSearch(input: Record<string, unknown>): Work
72
78
  ...(tab ? {tab} : {}),
73
79
  ...(annotation ? {annotation} : {}),
74
80
  ...(severity ? {severity} : {}),
75
- ...(string(input.job) ? {jobId: string(input.job)} : {}),
76
- ...(string(input.jobExecution) ? {jobExecutionId: string(input.jobExecution)} : {}),
77
- ...(string(input.step) ? {stepId: string(input.step)} : {}),
78
- ...(string(input.stepAttempt) ? {stepAttemptId: string(input.stepAttempt)} : {}),
79
- ...(runAttempt ? {runAttempt} : {}),
81
+ ...workflowSelectionFromSearch(input, true),
80
82
  };
81
83
  }
82
84
 
85
+ /** Reads the job-detail query string without allowing a job id to leak back into URL state. */
86
+ export function validateWorkflowJobSearch(input: Record<string, unknown>): WorkflowJobSearch {
87
+ const {jobId: _jobId, ...selection} = workflowSelectionFromSearch(input, false);
88
+ return selection;
89
+ }
90
+
83
91
  /**
84
92
  * Writes the query string back.
85
93
  *
@@ -101,23 +109,19 @@ export function workflowRunSearchParams(
101
109
  ...(search.tab && search.tab !== 'summary' ? {tab: search.tab} : {}),
102
110
  ...(search.annotation ? {annotation: search.annotation} : {}),
103
111
  ...(search.severity ? {severity: search.severity} : {}),
104
- ...(selection.jobId ? {job: selection.jobId} : {}),
105
- ...(selection.jobExecutionId ? {jobExecution: selection.jobExecutionId} : {}),
106
- ...(selection.stepId ? {step: selection.stepId} : {}),
107
- ...(selection.stepAttemptId ? {stepAttempt: selection.stepAttemptId} : {}),
108
- ...(selection.runAttempt ? {runAttempt: String(selection.runAttempt)} : {}),
112
+ ...workflowSelectionSearchParams(selection, true),
109
113
  };
110
114
  }
111
115
 
112
- /**
113
- * Resolves the detail surface for a URL. Explicit tabs win; legacy selection-only URLs belong
114
- * to Jobs so their selected detail remains visible instead of silently landing on Summary.
115
- */
116
+ export function workflowJobSearchParams(selection: WorkflowJobSearch) {
117
+ return workflowSelectionSearchParams(selection, false);
118
+ }
119
+
120
+ /** Resolves the run-level surface. Removed Jobs-tab and selection-only URLs fall back to Summary. */
116
121
  export function workflowRunTab(
117
122
  search: Pick<WorkflowRunsSearch, 'tab' | 'jobId' | 'jobExecutionId' | 'stepId' | 'stepAttemptId'>,
118
123
  ): WorkflowRunTab {
119
- if (search.tab) return search.tab;
120
- if (search.jobId || search.jobExecutionId || search.stepId || search.stepAttemptId) return 'jobs';
124
+ if (search.tab === 'annotations' || search.tab === 'source') return search.tab;
121
125
  return 'summary';
122
126
  }
123
127
 
@@ -203,6 +207,54 @@ export function workflowRouteParams(input: Record<string, unknown>): {
203
207
  return workflowRunId ? {workspaceSlug, projectSlug, workflowRunId} : {workspaceSlug, projectSlug};
204
208
  }
205
209
 
210
+ export function workflowJobRouteParams(input: Record<string, unknown>): {
211
+ workspaceSlug: string;
212
+ projectSlug: string;
213
+ workflowRunId: string;
214
+ jobId: string;
215
+ } {
216
+ const workspaceSlug = string(input.workspaceSlug);
217
+ const projectSlug = string(input.projectSlug);
218
+ const workflowRunId = string(input.workflowRunId);
219
+ const jobId = string(input.jobId);
220
+ if (!workspaceSlug || !projectSlug || !workflowRunId || !jobId) {
221
+ throw new Error('Workflow job route is missing required path parameters.');
222
+ }
223
+ return {workspaceSlug, projectSlug, workflowRunId, jobId};
224
+ }
225
+
226
+ function workflowSelectionFromSearch(
227
+ input: Record<string, unknown>,
228
+ includeJobId: boolean,
229
+ ): WorkflowRunSelectionInput {
230
+ const jobId = includeJobId ? string(input.job) : undefined;
231
+ const jobExecutionId = string(input.jobExecution);
232
+ const stepId = string(input.step);
233
+ const stepAttemptId = string(input.stepAttempt);
234
+ const runAttempt = positiveInteger(input.runAttempt);
235
+
236
+ return {
237
+ ...(jobId ? {jobId} : {}),
238
+ ...(jobExecutionId ? {jobExecutionId} : {}),
239
+ ...(stepId ? {stepId} : {}),
240
+ ...(stepAttemptId ? {stepAttemptId} : {}),
241
+ ...(runAttempt ? {runAttempt} : {}),
242
+ };
243
+ }
244
+
245
+ function workflowSelectionSearchParams(
246
+ selection: WorkflowRunSelectionInput,
247
+ includeJobId: boolean,
248
+ ) {
249
+ return {
250
+ ...(includeJobId && selection.jobId ? {job: selection.jobId} : {}),
251
+ ...(selection.jobExecutionId ? {jobExecution: selection.jobExecutionId} : {}),
252
+ ...(selection.stepId ? {step: selection.stepId} : {}),
253
+ ...(selection.stepAttemptId ? {stepAttempt: selection.stepAttemptId} : {}),
254
+ ...(selection.runAttempt ? {runAttempt: String(selection.runAttempt)} : {}),
255
+ };
256
+ }
257
+
206
258
  function setOrDelete<TKey extends keyof WorkflowRunsSearch>(
207
259
  target: WorkflowRunsSearch,
208
260
  key: TKey,
@@ -0,0 +1,29 @@
1
+ import {defineRoute, useRouteParams, useRouteSearch} from '@shipfox/client-shell/runtime';
2
+ import {WorkflowJobDetailPage} from '#pages/workflow-job-detail-page.js';
3
+ import {validateWorkflowJobSearch, workflowJobRouteParams} from './inputs.js';
4
+ import {ProjectRoute} from './project-route.js';
5
+
6
+ export default defineRoute({
7
+ staticData: {layout: 'full-bleed'},
8
+ validateSearch: validateWorkflowJobSearch,
9
+ component: () => {
10
+ const {workspaceSlug, projectSlug, workflowRunId, jobId} =
11
+ useRouteParams(workflowJobRouteParams);
12
+ const search = useRouteSearch(validateWorkflowJobSearch);
13
+
14
+ return (
15
+ <ProjectRoute>
16
+ {(project) => (
17
+ <WorkflowJobDetailPage
18
+ projectId={project.id}
19
+ workspaceSlug={workspaceSlug}
20
+ projectSlug={projectSlug}
21
+ workflowRunId={workflowRunId}
22
+ jobId={jobId}
23
+ search={search}
24
+ />
25
+ )}
26
+ </ProjectRoute>
27
+ );
28
+ },
29
+ });
package/test/pages.tsx CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  import {type RenderResult, render} from '@testing-library/react';
21
21
  import {createStore, Provider as JotaiProvider} from 'jotai';
22
22
  import type {ReactElement} from 'react';
23
- import {validateWorkflowRunsSearch} from '#routes/inputs.js';
23
+ import {validateWorkflowJobSearch, validateWorkflowRunsSearch} from '#routes/inputs.js';
24
24
 
25
25
  // The workflow run page navigates with the router (run rows are links and the page redirects
26
26
  // to the first run), so the harness mounts the page under a memory router whose route tree
@@ -39,6 +39,10 @@ const authState: AuthState = {
39
39
  workspaces: [{id: PROJECT_TEST_WID, name: 'Acme', slug: PROJECT_TEST_WSLUG, membershipId: 'm-1'}],
40
40
  };
41
41
 
42
+ type TestPageSearch =
43
+ | ReturnType<typeof validateWorkflowRunsSearch>
44
+ | ReturnType<typeof validateWorkflowJobSearch>;
45
+
42
46
  export function jsonResponse(body: unknown, init: ResponseInit = {}) {
43
47
  return new Response(JSON.stringify(body), {
44
48
  status: 200,
@@ -51,7 +55,8 @@ function createTestRouter(
51
55
  path: string,
52
56
  renderPage: (params: {
53
57
  workflowRunId?: string | undefined;
54
- search: ReturnType<typeof validateWorkflowRunsSearch>;
58
+ jobId?: string | undefined;
59
+ search: TestPageSearch;
55
60
  }) => ReactElement,
56
61
  ) {
57
62
  const rootRoute = createRootRoute({component: Outlet});
@@ -75,6 +80,21 @@ function createTestRouter(
75
80
  });
76
81
  },
77
82
  });
83
+ const jobDetailRoute = createRoute({
84
+ getParentRoute: () => rootRoute,
85
+ path: '/w/$workspaceSlug/p/$projectSlug/runs/$workflowRunId/jobs/$jobId',
86
+ component: function JobDetailRoute() {
87
+ const {workflowRunId, jobId} = useParams({strict: false}) as {
88
+ workflowRunId?: string;
89
+ jobId?: string;
90
+ };
91
+ return renderPage({
92
+ workflowRunId,
93
+ jobId,
94
+ search: validateWorkflowJobSearch(useSearch({strict: false}) as Record<string, unknown>),
95
+ });
96
+ },
97
+ });
78
98
  const workflowsRoute = createRoute({
79
99
  getParentRoute: () => rootRoute,
80
100
  path: '/w/$workspaceSlug/p/$projectSlug/workflows',
@@ -95,6 +115,7 @@ function createTestRouter(
95
115
  routeTree: rootRoute.addChildren([
96
116
  runsRoute,
97
117
  runDetailRoute,
118
+ jobDetailRoute,
98
119
  workflowsRoute,
99
120
  modelProviderSettingsRoute,
100
121
  ]),
@@ -105,7 +126,8 @@ export function renderProjectPage(
105
126
  path: string,
106
127
  renderPage: (params: {
107
128
  workflowRunId?: string | undefined;
108
- search: ReturnType<typeof validateWorkflowRunsSearch>;
129
+ jobId?: string | undefined;
130
+ search: TestPageSearch;
109
131
  }) => ReactElement,
110
132
  ): RenderResult & {
111
133
  queryClient: QueryClient;
package/test/render.tsx CHANGED
@@ -22,6 +22,11 @@ function createComponentRouter(element: ReactElement) {
22
22
  path: '/w/$workspaceSlug/p/$projectSlug/runs/$workflowRunId',
23
23
  component: () => null,
24
24
  });
25
+ const jobDetailRoute = createRoute({
26
+ getParentRoute: () => rootRoute,
27
+ path: '/w/$workspaceSlug/p/$projectSlug/runs/$workflowRunId/jobs/$jobId',
28
+ component: () => null,
29
+ });
25
30
  // The unfiltered empty state links here.
26
31
  const workflowsRoute = createRoute({
27
32
  getParentRoute: () => rootRoute,
@@ -31,7 +36,7 @@ function createComponentRouter(element: ReactElement) {
31
36
 
32
37
  return createRouter({
33
38
  history: createMemoryHistory({initialEntries: ['/']}),
34
- routeTree: rootRoute.addChildren([indexRoute, runDetailRoute, workflowsRoute]),
39
+ routeTree: rootRoute.addChildren([indexRoute, runDetailRoute, jobDetailRoute, workflowsRoute]),
35
40
  });
36
41
  }
37
42