@shipfox/client-workflows 12.0.2 → 14.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 (325) hide show
  1. package/.storybook/main.ts +16 -1
  2. package/.storybook/preview.tsx +3 -3
  3. package/.storybook/workspace-source.ts +31 -0
  4. package/.turbo/turbo-build.log +1 -1
  5. package/CHANGELOG.md +79 -0
  6. package/dist/components/job-graph/job-duration-format.d.ts +2 -2
  7. package/dist/components/job-graph/job-duration-format.d.ts.map +1 -1
  8. package/dist/components/job-graph/job-duration-format.js +4 -2
  9. package/dist/components/job-graph/job-duration-format.js.map +1 -1
  10. package/dist/components/job-graph/job-duration-label.js +1 -1
  11. package/dist/components/job-graph/job-duration-label.js.map +1 -1
  12. package/dist/components/job-graph/job-graph-content.d.ts +2 -1
  13. package/dist/components/job-graph/job-graph-content.d.ts.map +1 -1
  14. package/dist/components/job-graph/job-graph-content.js +3 -3
  15. package/dist/components/job-graph/job-graph-content.js.map +1 -1
  16. package/dist/components/job-graph/job-graph-view.d.ts +2 -1
  17. package/dist/components/job-graph/job-graph-view.d.ts.map +1 -1
  18. package/dist/components/job-graph/job-graph-view.js +2 -2
  19. package/dist/components/job-graph/job-graph-view.js.map +1 -1
  20. package/dist/components/job-graph/job-node.d.ts +1 -1
  21. package/dist/components/job-graph/job-node.d.ts.map +1 -1
  22. package/dist/components/job-graph/job-node.js +10 -9
  23. package/dist/components/job-graph/job-node.js.map +1 -1
  24. package/dist/components/job-graph/types.d.ts +2 -1
  25. package/dist/components/job-graph/types.d.ts.map +1 -1
  26. package/dist/components/job-graph/types.js.map +1 -1
  27. package/dist/components/step-list/step-list.d.ts +2 -2
  28. package/dist/components/step-list/step-list.d.ts.map +1 -1
  29. package/dist/components/step-list/step-list.js.map +1 -1
  30. package/dist/components/workflow-run-duration-label.js +1 -1
  31. package/dist/components/workflow-run-duration-label.js.map +1 -1
  32. package/dist/components/workflow-run-list/job-status-strip.d.ts +21 -0
  33. package/dist/components/workflow-run-list/job-status-strip.d.ts.map +1 -0
  34. package/dist/components/workflow-run-list/job-status-strip.js +185 -0
  35. package/dist/components/workflow-run-list/job-status-strip.js.map +1 -0
  36. package/dist/components/workflow-run-list/run-display.d.ts +27 -3
  37. package/dist/components/workflow-run-list/run-display.d.ts.map +1 -1
  38. package/dist/components/workflow-run-list/run-display.js +78 -7
  39. package/dist/components/workflow-run-list/run-display.js.map +1 -1
  40. package/dist/components/workflow-run-list/types.d.ts +23 -24
  41. package/dist/components/workflow-run-list/types.d.ts.map +1 -1
  42. package/dist/components/workflow-run-list/types.js.map +1 -1
  43. package/dist/components/workflow-run-list/workflow-run-filter-menu.d.ts +22 -0
  44. package/dist/components/workflow-run-list/workflow-run-filter-menu.d.ts.map +1 -0
  45. package/dist/components/workflow-run-list/workflow-run-filter-menu.js +68 -0
  46. package/dist/components/workflow-run-list/workflow-run-filter-menu.js.map +1 -0
  47. package/dist/components/workflow-run-list/workflow-run-filters.d.ts +18 -0
  48. package/dist/components/workflow-run-list/workflow-run-filters.d.ts.map +1 -0
  49. package/dist/components/workflow-run-list/workflow-run-filters.js +236 -0
  50. package/dist/components/workflow-run-list/workflow-run-filters.js.map +1 -0
  51. package/dist/components/workflow-run-list/workflow-run-list-content.d.ts +8 -4
  52. package/dist/components/workflow-run-list/workflow-run-list-content.d.ts.map +1 -1
  53. package/dist/components/workflow-run-list/workflow-run-list-content.js +40 -12
  54. package/dist/components/workflow-run-list/workflow-run-list-content.js.map +1 -1
  55. package/dist/components/workflow-run-list/workflow-run-list-states.d.ts +29 -2
  56. package/dist/components/workflow-run-list/workflow-run-list-states.d.ts.map +1 -1
  57. package/dist/components/workflow-run-list/workflow-run-list-states.js +103 -44
  58. package/dist/components/workflow-run-list/workflow-run-list-states.js.map +1 -1
  59. package/dist/components/workflow-run-list/workflow-run-list-view.d.ts +1 -1
  60. package/dist/components/workflow-run-list/workflow-run-list-view.d.ts.map +1 -1
  61. package/dist/components/workflow-run-list/workflow-run-list-view.js +60 -43
  62. package/dist/components/workflow-run-list/workflow-run-list-view.js.map +1 -1
  63. package/dist/components/workflow-run-list/workflow-run-list.d.ts +1 -1
  64. package/dist/components/workflow-run-list/workflow-run-list.d.ts.map +1 -1
  65. package/dist/components/workflow-run-list/workflow-run-list.js +20 -8
  66. package/dist/components/workflow-run-list/workflow-run-list.js.map +1 -1
  67. package/dist/components/workflow-run-list/workflow-run-row.d.ts +19 -9
  68. package/dist/components/workflow-run-list/workflow-run-row.d.ts.map +1 -1
  69. package/dist/components/workflow-run-list/workflow-run-row.js +179 -112
  70. package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
  71. package/dist/components/workflow-run-number-label.d.ts +15 -0
  72. package/dist/components/workflow-run-number-label.d.ts.map +1 -0
  73. package/dist/components/workflow-run-number-label.js +41 -0
  74. package/dist/components/workflow-run-number-label.js.map +1 -0
  75. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.d.ts +3 -3
  76. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.d.ts.map +1 -1
  77. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js +10 -9
  78. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js.map +1 -1
  79. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts +4 -9
  80. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
  81. package/dist/components/workflow-run-summary/workflow-run-summary.js +107 -74
  82. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
  83. package/dist/components/workflow-run-tabs/index.d.ts +6 -0
  84. package/dist/components/workflow-run-tabs/index.d.ts.map +1 -0
  85. package/dist/components/workflow-run-tabs/index.js +7 -0
  86. package/dist/components/workflow-run-tabs/index.js.map +1 -0
  87. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts +11 -0
  88. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts.map +1 -0
  89. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js +71 -0
  90. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js.map +1 -0
  91. package/dist/components/workflow-run-tabs/run-annotations-empty.d.ts +2 -0
  92. package/dist/components/workflow-run-tabs/run-annotations-empty.d.ts.map +1 -0
  93. package/dist/components/workflow-run-tabs/run-annotations-empty.js +11 -0
  94. package/dist/components/workflow-run-tabs/run-annotations-empty.js.map +1 -0
  95. package/dist/components/workflow-run-tabs/run-jobs-list.d.ts +18 -0
  96. package/dist/components/workflow-run-tabs/run-jobs-list.d.ts.map +1 -0
  97. package/dist/components/workflow-run-tabs/run-jobs-list.js +107 -0
  98. package/dist/components/workflow-run-tabs/run-jobs-list.js.map +1 -0
  99. package/dist/components/workflow-run-tabs/run-tab-count.d.ts +6 -0
  100. package/dist/components/workflow-run-tabs/run-tab-count.d.ts.map +1 -0
  101. package/dist/components/workflow-run-tabs/run-tab-count.js +27 -0
  102. package/dist/components/workflow-run-tabs/run-tab-count.js.map +1 -0
  103. package/dist/components/workflow-run-tabs/run-tab-strip.d.ts +8 -0
  104. package/dist/components/workflow-run-tabs/run-tab-strip.d.ts.map +1 -0
  105. package/dist/components/workflow-run-tabs/run-tab-strip.js +82 -0
  106. package/dist/components/workflow-run-tabs/run-tab-strip.js.map +1 -0
  107. package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts +2 -2
  108. package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts.map +1 -1
  109. package/dist/components/workflow-run-view/agent-config-failure-callout.js +4 -4
  110. package/dist/components/workflow-run-view/agent-config-failure-callout.js.map +1 -1
  111. package/dist/components/workflow-run-view/job-card.d.ts +2 -2
  112. package/dist/components/workflow-run-view/job-card.d.ts.map +1 -1
  113. package/dist/components/workflow-run-view/job-card.js +51 -32
  114. package/dist/components/workflow-run-view/job-card.js.map +1 -1
  115. package/dist/components/workflow-run-view/job-execution-switcher.d.ts +1 -1
  116. package/dist/components/workflow-run-view/job-execution-switcher.d.ts.map +1 -1
  117. package/dist/components/workflow-run-view/job-execution-switcher.js +20 -8
  118. package/dist/components/workflow-run-view/job-execution-switcher.js.map +1 -1
  119. package/dist/components/workflow-run-view/workflow-run-selection.d.ts.map +1 -1
  120. package/dist/components/workflow-run-view/workflow-run-selection.js +40 -1
  121. package/dist/components/workflow-run-view/workflow-run-selection.js.map +1 -1
  122. package/dist/components/workflow-run-view/workflow-run-states.d.ts +1 -0
  123. package/dist/components/workflow-run-view/workflow-run-states.d.ts.map +1 -1
  124. package/dist/components/workflow-run-view/workflow-run-states.js +10 -1
  125. package/dist/components/workflow-run-view/workflow-run-states.js.map +1 -1
  126. package/dist/components/workflow-run-view/workflow-run-view.d.ts +10 -5
  127. package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
  128. package/dist/components/workflow-run-view/workflow-run-view.js +136 -85
  129. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
  130. package/dist/components/workflow-source-panel/index.d.ts +1 -0
  131. package/dist/components/workflow-source-panel/index.d.ts.map +1 -1
  132. package/dist/components/workflow-source-panel/index.js +1 -0
  133. package/dist/components/workflow-source-panel/index.js.map +1 -1
  134. package/dist/components/workflow-source-panel/workflow-source-content.d.ts +12 -0
  135. package/dist/components/workflow-source-panel/workflow-source-content.d.ts.map +1 -0
  136. package/dist/components/workflow-source-panel/workflow-source-content.js +53 -0
  137. package/dist/components/workflow-source-panel/workflow-source-content.js.map +1 -0
  138. package/dist/components/workflow-source-panel/workflow-source-panel.d.ts +1 -1
  139. package/dist/components/workflow-source-panel/workflow-source-panel.d.ts.map +1 -1
  140. package/dist/components/workflow-source-panel/workflow-source-panel.js +7 -54
  141. package/dist/components/workflow-source-panel/workflow-source-panel.js.map +1 -1
  142. package/dist/components/workflow-status/status-visuals.d.ts +4 -4
  143. package/dist/components/workflow-status/status-visuals.d.ts.map +1 -1
  144. package/dist/components/workflow-status/status-visuals.js +7 -0
  145. package/dist/components/workflow-status/status-visuals.js.map +1 -1
  146. package/dist/components/workflow-status/workflow-status-icon.d.ts +3 -4
  147. package/dist/components/workflow-status/workflow-status-icon.d.ts.map +1 -1
  148. package/dist/components/workflow-status/workflow-status-icon.js +6 -4
  149. package/dist/components/workflow-status/workflow-status-icon.js.map +1 -1
  150. package/dist/core/entities/job-execution.d.ts +10 -0
  151. package/dist/core/entities/job-execution.d.ts.map +1 -1
  152. package/dist/core/entities/job-execution.js +11 -0
  153. package/dist/core/entities/job-execution.js.map +1 -1
  154. package/dist/core/entities/job.d.ts +4 -2
  155. package/dist/core/entities/job.d.ts.map +1 -1
  156. package/dist/core/entities/job.js +8 -1
  157. package/dist/core/entities/job.js.map +1 -1
  158. package/dist/core/entities/step.d.ts +1 -1
  159. package/dist/core/entities/step.d.ts.map +1 -1
  160. package/dist/core/entities/step.js +2 -0
  161. package/dist/core/entities/step.js.map +1 -1
  162. package/dist/core/entities/workflow-run.d.ts +52 -4
  163. package/dist/core/entities/workflow-run.d.ts.map +1 -1
  164. package/dist/core/entities/workflow-run.js +22 -3
  165. package/dist/core/entities/workflow-run.js.map +1 -1
  166. package/dist/core/workflow-run-tabs.d.ts +9 -0
  167. package/dist/core/workflow-run-tabs.d.ts.map +1 -0
  168. package/dist/core/workflow-run-tabs.js +3 -0
  169. package/dist/core/workflow-run-tabs.js.map +1 -0
  170. package/dist/core/workflow-run.d.ts +6 -6
  171. package/dist/core/workflow-run.d.ts.map +1 -1
  172. package/dist/core/workflow-run.js +3 -3
  173. package/dist/core/workflow-run.js.map +1 -1
  174. package/dist/feature.d.ts +7 -7
  175. package/dist/feature.js +5 -5
  176. package/dist/feature.js.map +1 -1
  177. package/dist/hooks/api/workflow-run-mapper.d.ts +4 -3
  178. package/dist/hooks/api/workflow-run-mapper.d.ts.map +1 -1
  179. package/dist/hooks/api/workflow-run-mapper.js +31 -3
  180. package/dist/hooks/api/workflow-run-mapper.js.map +1 -1
  181. package/dist/hooks/api/workflow-runs.d.ts +4 -4
  182. package/dist/hooks/api/workflow-runs.d.ts.map +1 -1
  183. package/dist/hooks/api/workflow-runs.js +12 -4
  184. package/dist/hooks/api/workflow-runs.js.map +1 -1
  185. package/dist/index.d.ts +4 -3
  186. package/dist/index.d.ts.map +1 -1
  187. package/dist/index.js +3 -2
  188. package/dist/index.js.map +1 -1
  189. package/dist/pages/project-workflows-page.js +46 -1
  190. package/dist/pages/project-workflows-page.js.map +1 -1
  191. package/dist/pages/workflow-run-detail-page.d.ts +11 -0
  192. package/dist/pages/workflow-run-detail-page.d.ts.map +1 -0
  193. package/dist/pages/workflow-run-detail-page.js +52 -0
  194. package/dist/pages/workflow-run-detail-page.js.map +1 -0
  195. package/dist/pages/workflow-run-list-page.d.ts +10 -0
  196. package/dist/pages/workflow-run-list-page.d.ts.map +1 -0
  197. package/dist/pages/workflow-run-list-page.js +43 -0
  198. package/dist/pages/workflow-run-list-page.js.map +1 -0
  199. package/dist/routes/inputs.d.ts +94 -5
  200. package/dist/routes/inputs.d.ts.map +1 -1
  201. package/dist/routes/inputs.js +199 -13
  202. package/dist/routes/inputs.js.map +1 -1
  203. package/dist/routes/project-route.d.ts +6 -0
  204. package/dist/routes/project-route.d.ts.map +1 -0
  205. package/dist/routes/project-route.js +25 -0
  206. package/dist/routes/project-route.js.map +1 -0
  207. package/dist/routes/run-detail.d.ts.map +1 -1
  208. package/dist/routes/run-detail.js +12 -7
  209. package/dist/routes/run-detail.js.map +1 -1
  210. package/dist/routes/runs.d.ts.map +1 -1
  211. package/dist/routes/runs.js +11 -6
  212. package/dist/routes/runs.js.map +1 -1
  213. package/dist/routes/workflows.d.ts.map +1 -1
  214. package/dist/routes/workflows.js +6 -5
  215. package/dist/routes/workflows.js.map +1 -1
  216. package/dist/tsconfig.test.tsbuildinfo +1 -1
  217. package/package.json +9 -9
  218. package/src/components/job-graph/job-duration-format.ts +11 -2
  219. package/src/components/job-graph/job-duration-label.tsx +5 -1
  220. package/src/components/job-graph/job-graph-content.tsx +7 -4
  221. package/src/components/job-graph/job-graph-view.test.tsx +19 -8
  222. package/src/components/job-graph/job-graph-view.tsx +6 -3
  223. package/src/components/job-graph/job-graph.stories.tsx +24 -20
  224. package/src/components/job-graph/job-node.stories.tsx +11 -6
  225. package/src/components/job-graph/job-node.test.tsx +82 -12
  226. package/src/components/job-graph/job-node.tsx +22 -15
  227. package/src/components/job-graph/types.ts +5 -1
  228. package/src/components/step-list/step-list.stories.tsx +2 -3
  229. package/src/components/step-list/step-list.tsx +3 -2
  230. package/src/components/workflow-run-duration-label.tsx +1 -1
  231. package/src/components/workflow-run-list/job-status-strip.tsx +189 -0
  232. package/src/components/workflow-run-list/run-display.test.ts +169 -30
  233. package/src/components/workflow-run-list/run-display.ts +116 -9
  234. package/src/components/workflow-run-list/types.ts +21 -17
  235. package/src/components/workflow-run-list/workflow-run-filter-menu.tsx +100 -0
  236. package/src/components/workflow-run-list/workflow-run-filters.tsx +243 -0
  237. package/src/components/workflow-run-list/workflow-run-list-content.tsx +44 -16
  238. package/src/components/workflow-run-list/workflow-run-list-states.tsx +128 -27
  239. package/src/components/workflow-run-list/workflow-run-list-view.test.tsx +441 -104
  240. package/src/components/workflow-run-list/workflow-run-list-view.tsx +73 -43
  241. package/src/components/workflow-run-list/workflow-run-list.stories.tsx +140 -20
  242. package/src/components/workflow-run-list/workflow-run-list.tsx +20 -10
  243. package/src/components/workflow-run-list/workflow-run-row.tsx +173 -106
  244. package/src/components/workflow-run-number-label.test.tsx +45 -0
  245. package/src/components/workflow-run-number-label.tsx +47 -0
  246. package/src/components/workflow-run-summary/workflow-run-attempt-switcher.test.tsx +7 -8
  247. package/src/components/workflow-run-summary/workflow-run-attempt-switcher.tsx +15 -14
  248. package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +29 -38
  249. package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +30 -24
  250. package/src/components/workflow-run-summary/workflow-run-summary.tsx +102 -77
  251. package/src/components/workflow-run-tabs/index.ts +5 -0
  252. package/src/components/workflow-run-tabs/run-annotation-summary-line.test.tsx +69 -0
  253. package/src/components/workflow-run-tabs/run-annotation-summary-line.tsx +97 -0
  254. package/src/components/workflow-run-tabs/run-annotations-empty.tsx +11 -0
  255. package/src/components/workflow-run-tabs/run-jobs-list.tsx +151 -0
  256. package/src/components/workflow-run-tabs/run-tab-count.tsx +28 -0
  257. package/src/components/workflow-run-tabs/run-tab-strip.stories.tsx +52 -0
  258. package/src/components/workflow-run-tabs/run-tab-strip.test.tsx +117 -0
  259. package/src/components/workflow-run-tabs/run-tab-strip.tsx +97 -0
  260. package/src/components/workflow-run-view/agent-config-failure-callout.stories.tsx +2 -3
  261. package/src/components/workflow-run-view/agent-config-failure-callout.tsx +4 -4
  262. package/src/components/workflow-run-view/job-card.stories.tsx +11 -10
  263. package/src/components/workflow-run-view/job-card.test.tsx +115 -0
  264. package/src/components/workflow-run-view/job-card.tsx +74 -35
  265. package/src/components/workflow-run-view/job-execution-switcher.tsx +31 -9
  266. package/src/components/workflow-run-view/workflow-run-selection.test.ts +48 -0
  267. package/src/components/workflow-run-view/workflow-run-selection.ts +48 -1
  268. package/src/components/workflow-run-view/workflow-run-states.tsx +12 -1
  269. package/src/components/workflow-run-view/workflow-run-view.test.tsx +152 -94
  270. package/src/components/workflow-run-view/workflow-run-view.tsx +219 -95
  271. package/src/components/workflow-source-panel/index.ts +1 -0
  272. package/src/components/workflow-source-panel/workflow-source-content.tsx +84 -0
  273. package/src/components/workflow-source-panel/workflow-source-panel.tsx +14 -81
  274. package/src/components/workflow-status/status-visuals.test.ts +9 -0
  275. package/src/components/workflow-status/status-visuals.ts +6 -4
  276. package/src/components/workflow-status/workflow-status-icon.stories.tsx +10 -2
  277. package/src/components/workflow-status/workflow-status-icon.test.tsx +18 -0
  278. package/src/components/workflow-status/workflow-status-icon.tsx +17 -24
  279. package/src/core/entities/job-execution.ts +22 -0
  280. package/src/core/entities/job-status.test.ts +104 -0
  281. package/src/core/entities/job.ts +18 -3
  282. package/src/core/entities/step.ts +4 -0
  283. package/src/core/entities/workflow-run.ts +76 -7
  284. package/src/core/workflow-run-tabs.ts +8 -0
  285. package/src/core/workflow-run.test.ts +24 -9
  286. package/src/core/workflow-run.ts +17 -2
  287. package/src/feature.ts +5 -5
  288. package/src/hooks/api/workflow-run-mapper.ts +30 -3
  289. package/src/hooks/api/workflow-runs.test.tsx +2 -0
  290. package/src/hooks/api/workflow-runs.ts +12 -7
  291. package/src/index.ts +7 -2
  292. package/src/page-harness-budget.test.ts +1 -1
  293. package/src/pages/project-workflows-page.test.tsx +59 -5
  294. package/src/pages/project-workflows-page.tsx +34 -1
  295. package/src/pages/workflow-run-detail-page.tsx +72 -0
  296. package/src/pages/workflow-run-list-page.tsx +65 -0
  297. package/src/pages/workflow-run-pages.test.tsx +823 -0
  298. package/src/routes/inputs.test.ts +240 -12
  299. package/src/routes/inputs.ts +215 -11
  300. package/src/routes/project-route.tsx +25 -0
  301. package/src/routes/run-detail.tsx +15 -8
  302. package/src/routes/runs.tsx +14 -7
  303. package/src/routes/workflows.tsx +5 -4
  304. package/src/storybook-config.test.ts +27 -0
  305. package/test/fixtures/workflow-run.ts +76 -7
  306. package/test/pages.tsx +18 -8
  307. package/test/render.tsx +8 -2
  308. package/tsconfig.build.tsbuildinfo +1 -1
  309. package/tsconfig.test.json +7 -1
  310. package/dist/components/workflow-run-list/workflow-run-list-header.d.ts +0 -10
  311. package/dist/components/workflow-run-list/workflow-run-list-header.d.ts.map +0 -1
  312. package/dist/components/workflow-run-list/workflow-run-list-header.js +0 -55
  313. package/dist/components/workflow-run-list/workflow-run-list-header.js.map +0 -1
  314. package/dist/pages/workflow-run-first-time-use.d.ts +0 -6
  315. package/dist/pages/workflow-run-first-time-use.d.ts.map +0 -1
  316. package/dist/pages/workflow-run-first-time-use.js +0 -17
  317. package/dist/pages/workflow-run-first-time-use.js.map +0 -1
  318. package/dist/pages/workflow-run-page.d.ts +0 -10
  319. package/dist/pages/workflow-run-page.d.ts.map +0 -1
  320. package/dist/pages/workflow-run-page.js +0 -90
  321. package/dist/pages/workflow-run-page.js.map +0 -1
  322. package/src/components/workflow-run-list/workflow-run-list-header.tsx +0 -53
  323. package/src/pages/workflow-run-first-time-use.tsx +0 -17
  324. package/src/pages/workflow-run-page.test.tsx +0 -519
  325. package/src/pages/workflow-run-page.tsx +0 -94
@@ -0,0 +1,115 @@
1
+ import {render, screen} from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import {workflowJob, workflowJobExecutionDto} from '#test/fixtures/workflow-run.js';
4
+ import {JobCard} from './job-card.js';
5
+ import {JobExecutionSwitcher} from './job-execution-switcher.js';
6
+
7
+ const FIRST_EXECUTION_ACCESSIBLE_NAME =
8
+ /Execution #1: Review PR #476, Failed, validation failed, duration 1m 55s/;
9
+ const SECOND_EXECUTION_MENU_ITEM = /Execution #2: Review PR #479/;
10
+
11
+ describe('JobCard execution names', () => {
12
+ test('shows only the resolved execution name', () => {
13
+ const job = workflowJob({
14
+ key: 'deploy',
15
+ name: 'Deploy application',
16
+ job_executions: [
17
+ workflowJobExecutionDto({
18
+ name: 'Deploy production',
19
+ sequence: 1,
20
+ }),
21
+ ],
22
+ });
23
+ const execution = job.jobExecutions[0];
24
+ if (!execution) throw new Error('Expected a job execution');
25
+
26
+ render(
27
+ <JobCard
28
+ job={job}
29
+ selectedJobExecution={execution}
30
+ selectedAttemptId={null}
31
+ onSelectedJobExecutionChange={undefined}
32
+ onSelectedAttemptChange={undefined}
33
+ />,
34
+ );
35
+
36
+ expect(screen.getByRole('heading', {name: 'Deploy production'})).toBeInTheDocument();
37
+ expect(screen.queryByText('Deploy application')).not.toBeInTheDocument();
38
+ expect(screen.queryByText('Execution #1')).not.toBeInTheDocument();
39
+ });
40
+
41
+ test('uses the static job name only when no execution exists', () => {
42
+ const job = workflowJob({
43
+ key: 'deploy',
44
+ name: 'Deploy application',
45
+ job_executions: [],
46
+ });
47
+
48
+ render(
49
+ <JobCard
50
+ job={job}
51
+ selectedJobExecution={undefined}
52
+ selectedAttemptId={null}
53
+ onSelectedJobExecutionChange={undefined}
54
+ onSelectedAttemptChange={undefined}
55
+ />,
56
+ );
57
+
58
+ expect(screen.getByRole('heading', {name: 'Deploy application'})).toBeInTheDocument();
59
+ expect(screen.getByText('Waiting for this job to start')).toBeInTheDocument();
60
+ });
61
+ });
62
+
63
+ describe('JobExecutionSwitcher execution names', () => {
64
+ test('uses each listening execution name while keeping its sequence and status', async () => {
65
+ const user = userEvent.setup();
66
+ const job = workflowJob({
67
+ key: 'reviews',
68
+ name: 'Process review',
69
+ mode: 'listening',
70
+ status: 'running',
71
+ listener_status: 'listening',
72
+ job_executions: [
73
+ workflowJobExecutionDto({
74
+ sequence: 1,
75
+ name: 'Review PR #476',
76
+ status: 'failed',
77
+ status_reason: 'validation failed',
78
+ queued_at: '2026-06-21T12:00:00.000Z',
79
+ started_at: '2026-06-21T12:00:05.000Z',
80
+ finished_at: '2026-06-21T12:02:00.000Z',
81
+ }),
82
+ workflowJobExecutionDto({
83
+ sequence: 2,
84
+ name: 'Review PR #479',
85
+ status: 'running',
86
+ }),
87
+ ],
88
+ });
89
+ const selected = job.jobExecutions[1];
90
+ if (!selected) throw new Error('Expected a selected job execution');
91
+
92
+ render(
93
+ <JobExecutionSwitcher
94
+ job={job}
95
+ selectedJobExecution={selected.id}
96
+ onSelectedJobExecutionChange={() => undefined}
97
+ variant="title"
98
+ />,
99
+ );
100
+
101
+ const trigger = screen.getByRole('button', {
102
+ name: 'Switch job execution, currently execution 2: Review PR #479',
103
+ });
104
+ expect(trigger).toBeInTheDocument();
105
+
106
+ await user.click(trigger);
107
+
108
+ expect(
109
+ await screen.findByRole('menuitem', {
110
+ name: FIRST_EXECUTION_ACCESSIBLE_NAME,
111
+ }),
112
+ ).toBeInTheDocument();
113
+ expect(screen.getByRole('menuitem', {name: SECOND_EXECUTION_MENU_ITEM})).toBeInTheDocument();
114
+ });
115
+ });
@@ -12,7 +12,11 @@ import {Fragment, useId, useRef, useState} from 'react';
12
12
  import {getWorkflowStatusVisual} from '#components/workflow-status/status-visuals.js';
13
13
  import {
14
14
  AGENT_CONFIG_ISSUES,
15
+ defaultJobExecution,
16
+ deriveJobDisplayStatus,
17
+ deriveJobExecutionDisplayStatus,
15
18
  type Job,
19
+ type JobDisplayStatus,
16
20
  type JobExecution,
17
21
  type JobExecutionTime,
18
22
  STEP_ERROR_REASONS,
@@ -30,7 +34,9 @@ import {formatJobExecutionTime, JobExecutionTimeText} from './job-execution-time
30
34
  import {StepAttemptLogPanel} from './step-attempt-log-panel.js';
31
35
 
32
36
  const STATUS_BADGE_LABEL_WIDTH_CH = Math.max(
33
- ...WORKFLOW_JOB_STATUSES.map((status) => getWorkflowStatusVisual(status).label.length),
37
+ ...[...WORKFLOW_JOB_STATUSES, 'listening' as const].map(
38
+ (status) => getWorkflowStatusVisual(status).label.length,
39
+ ),
34
40
  );
35
41
 
36
42
  interface LocalSelectedAttempt {
@@ -39,7 +45,7 @@ interface LocalSelectedAttempt {
39
45
  }
40
46
 
41
47
  export function JobCard({
42
- workspaceId,
48
+ workspaceSlug,
43
49
  job,
44
50
  selectedJobExecution,
45
51
  selectedAttemptId,
@@ -51,7 +57,7 @@ export function JobCard({
51
57
  focusedSourceStepId,
52
58
  onOpenStepSource,
53
59
  }: {
54
- workspaceId: string;
60
+ workspaceSlug?: string | undefined;
55
61
  job: Job;
56
62
  selectedJobExecution: JobExecution | undefined;
57
63
  selectedAttemptId: string | null | undefined;
@@ -71,7 +77,12 @@ export function JobCard({
71
77
  jobExecutionId: undefined,
72
78
  attemptId: null,
73
79
  });
74
- const selectedExecutionStatus = selectedJobExecution?.status ?? job.status;
80
+ const title = selectedJobExecution?.displayName ?? job.displayName;
81
+ const isNonDefaultExecutionSelected =
82
+ selectedJobExecution !== undefined && selectedJobExecution.id !== defaultJobExecution(job)?.id;
83
+ const selectedExecutionStatus = isNonDefaultExecutionSelected
84
+ ? deriveJobExecutionDisplayStatus(selectedJobExecution)
85
+ : deriveJobDisplayStatus(job);
75
86
  const effectiveSelectedAttemptId =
76
87
  selectedAttemptId === undefined &&
77
88
  localSelectedAttempt.jobExecutionId === selectedJobExecution?.id
@@ -93,7 +104,7 @@ export function JobCard({
93
104
  <CarriedOverStepPanel />
94
105
  ) : (
95
106
  <StepAttemptDetailPanel
96
- workspaceId={workspaceId}
107
+ workspaceSlug={workspaceSlug}
97
108
  step={step}
98
109
  stepId={stepId}
99
110
  attempt={attempt}
@@ -125,7 +136,7 @@ export function JobCard({
125
136
  onSelectedJobExecutionChange ? (
126
137
  <>
127
138
  <Text as="h2" id={titleId} size="sm" bold className="sr-only">
128
- {job.displayName}
139
+ {title}
129
140
  </Text>
130
141
  <div className="flex min-w-0 items-center gap-8">
131
142
  <JobStatusBadge status={selectedExecutionStatus} />
@@ -140,15 +151,17 @@ export function JobCard({
140
151
  ) : (
141
152
  <div className="flex min-w-0 items-center gap-8">
142
153
  <JobStatusBadge status={selectedExecutionStatus} />
143
- <Text
144
- as="h2"
145
- id={titleId}
146
- size="sm"
147
- bold
148
- className="min-w-0 truncate text-foreground-neutral-base"
149
- >
150
- {job.displayName}
151
- </Text>
154
+ <div className="flex min-w-0 items-center gap-8">
155
+ <Text
156
+ as="h2"
157
+ id={titleId}
158
+ size="sm"
159
+ bold
160
+ className="min-w-0 truncate text-foreground-neutral-base"
161
+ >
162
+ {title}
163
+ </Text>
164
+ </div>
152
165
  </div>
153
166
  )}
154
167
  {sourceAvailable && selectedSourceAction && sourcePanelId && onOpenStepSource ? (
@@ -196,7 +209,7 @@ export function JobCard({
196
209
  );
197
210
  }
198
211
 
199
- function JobStatusBadge({status}: {status: Job['status'] | JobExecution['status']}) {
212
+ function JobStatusBadge({status}: {status: JobDisplayStatus}) {
200
213
  const visual = getWorkflowStatusVisual(status);
201
214
 
202
215
  return (
@@ -252,7 +265,12 @@ function JobExecutionMetadata({execution}: {execution: JobExecution | undefined}
252
265
  ) : item.kind === 'trigger' ? (
253
266
  <JobExecutionTriggerMetadata execution={execution} />
254
267
  ) : (
255
- <JobExecutionMetadataItem icon={item.icon} kind={item.kind} time={item.time} />
268
+ <JobExecutionMetadataItem
269
+ icon={item.icon}
270
+ kind={item.kind}
271
+ time={item.time}
272
+ displayStatus={deriveJobExecutionDisplayStatus(execution)}
273
+ />
256
274
  )}
257
275
  </Fragment>
258
276
  ))}
@@ -309,14 +327,16 @@ function JobExecutionMetadataItem({
309
327
  icon,
310
328
  kind,
311
329
  time,
330
+ displayStatus,
312
331
  }: {
313
332
  icon: 'hourglassLine' | 'timerLine';
314
333
  kind: 'queue' | 'run';
315
334
  time: JobExecutionTime;
335
+ displayStatus: JobExecution['status'];
316
336
  }) {
317
337
  useTimeTick();
318
338
 
319
- const tooltip = `${jobExecutionTimeVerb(kind, time)} for ${formatJobExecutionTime(time)}`;
339
+ const tooltip = `${jobExecutionTimeVerb(kind, time, displayStatus)} for ${formatJobExecutionTime(time)}`;
320
340
 
321
341
  return (
322
342
  <Tooltip>
@@ -341,9 +361,15 @@ function JobExecutionMetadataItem({
341
361
  );
342
362
  }
343
363
 
344
- function jobExecutionTimeVerb(kind: 'queue' | 'run', time: JobExecutionTime): string {
345
- if (kind === 'queue') return time.state === 'live' ? 'Queuing' : 'Queued';
346
- return time.state === 'live' ? 'Running' : 'Ran';
364
+ function jobExecutionTimeVerb(
365
+ kind: 'queue' | 'run',
366
+ time: JobExecutionTime,
367
+ displayStatus: JobExecution['status'],
368
+ ): string {
369
+ if (kind === 'queue') return time.state === 'live' ? 'Queueing' : 'Queued';
370
+ if (time.state !== 'live') return 'Ran';
371
+ if (displayStatus === 'running') return 'Running';
372
+ return displayStatus === 'pending' ? 'Runner assigned' : 'Execution elapsed';
347
373
  }
348
374
 
349
375
  function MetadataSeparator() {
@@ -369,14 +395,14 @@ function selectedStepSourceAction(
369
395
  }
370
396
 
371
397
  function StepAttemptDetailPanel({
372
- workspaceId,
398
+ workspaceSlug,
373
399
  step,
374
400
  stepId,
375
401
  attempt,
376
402
  attemptError,
377
403
  attemptStatus,
378
404
  }: {
379
- workspaceId: string;
405
+ workspaceSlug?: string | undefined;
380
406
  step: Step;
381
407
  stepId: string;
382
408
  attempt: number;
@@ -389,7 +415,7 @@ function StepAttemptDetailPanel({
389
415
  <div className="flex min-w-0 flex-col gap-10">
390
416
  {isAgentConfigFailure(step, selectedAttemptError) ? (
391
417
  <AgentConfigFailureCallout
392
- workspaceId={workspaceId}
418
+ workspaceSlug={workspaceSlug}
393
419
  config={step.agentConfig}
394
420
  error={selectedAttemptError}
395
421
  />
@@ -432,7 +458,7 @@ function toSelectedAttemptError(
432
458
  signal: typeof error.signal === 'string' ? error.signal : undefined,
433
459
  reason: resolvedReason,
434
460
  agentConfigIssue,
435
- category: step.type === 'setup' ? 'setup' : 'user',
461
+ category: step.type === 'setup' || step.type === 'checkout' ? 'setup' : 'user',
436
462
  };
437
463
  }
438
464
 
@@ -449,35 +475,48 @@ function emptyStateForJob(job: Job, jobExecution: JobExecution): StepListEmptySt
449
475
  };
450
476
  }
451
477
 
452
- if (jobExecution.status === 'pending') {
478
+ const displayStatus =
479
+ job.mode === 'listening'
480
+ ? deriveJobExecutionDisplayStatus(jobExecution)
481
+ : deriveJobDisplayStatus(job);
482
+
483
+ if (jobExecution.status === 'running' && displayStatus === 'pending') {
484
+ return {
485
+ title: 'Runner preparing job',
486
+ description: 'A runner is assigned. Steps will appear here when work begins.',
487
+ status: displayStatus,
488
+ };
489
+ }
490
+
491
+ if (displayStatus === 'pending') {
453
492
  return {
454
493
  title: 'Waiting for this job to start',
455
494
  description: 'Steps will appear here once the job starts.',
456
- status: jobExecution.status,
495
+ status: displayStatus,
457
496
  };
458
497
  }
459
498
 
460
- if (jobExecution.status === 'running') {
499
+ if (displayStatus === 'running') {
461
500
  return {
462
501
  title: 'Waiting for the first step',
463
- description: 'This job is running, but no steps have started yet.',
464
- status: jobExecution.status,
502
+ description: 'The first running step will appear here shortly.',
503
+ status: displayStatus,
465
504
  };
466
505
  }
467
506
 
468
- if (jobExecution.status === 'cancelled') {
507
+ if (displayStatus === 'cancelled') {
469
508
  return {
470
509
  title: 'Cancelled before start',
471
510
  description: 'This job was cancelled before any step started.',
472
- status: jobExecution.status,
511
+ status: displayStatus,
473
512
  };
474
513
  }
475
514
 
476
- if (jobExecution.status === 'succeeded' || jobExecution.status === 'failed') {
515
+ if (displayStatus === 'succeeded' || displayStatus === 'failed') {
477
516
  return {
478
517
  title: 'No steps recorded',
479
518
  description: `Execution #${jobExecution.sequence} finished without recorded steps.`,
480
- status: jobExecution.status,
519
+ status: displayStatus,
481
520
  };
482
521
  }
483
522
 
@@ -497,7 +536,7 @@ function emptyStateForMissingExecution(job: Job): StepListEmptyState {
497
536
  return {
498
537
  title: 'Waiting for trigger events',
499
538
  description: 'Matching trigger events will create job executions here.',
500
- status: 'running',
539
+ status: deriveJobDisplayStatus(job),
501
540
  };
502
541
  }
503
542
 
@@ -9,9 +9,10 @@ import {Icon} from '@shipfox/react-ui/icon';
9
9
  import {Text} from '@shipfox/react-ui/typography';
10
10
  import {cn} from '@shipfox/react-ui/utils';
11
11
  import {useMemo} from 'react';
12
+ import {getWorkflowStatusVisual} from '#components/workflow-status/status-visuals.js';
12
13
  import {WorkflowStatusIcon} from '#components/workflow-status/workflow-status-icon.js';
13
- import type {Job, JobExecution} from '#core/workflow-run.js';
14
- import {JobExecutionTimeText} from './job-execution-time-text.js';
14
+ import {deriveJobExecutionDisplayStatus, type Job, type JobExecution} from '#core/workflow-run.js';
15
+ import {formatJobExecutionTime, JobExecutionTimeText} from './job-execution-time-text.js';
15
16
 
16
17
  export interface JobExecutionSwitcherProps {
17
18
  job: Job;
@@ -62,10 +63,10 @@ export function JobExecutionSwitcher({
62
63
  : 'min-h-28 gap-6 px-8 py-4 text-sm leading-20 text-foreground-neutral-subtle hover:bg-background-components-hover',
63
64
  className,
64
65
  )}
65
- aria-label={`Switch job execution, currently execution ${selected.sequence}`}
66
+ aria-label={`Switch job execution, currently execution ${selected.sequence}: ${selected.displayName}`}
66
67
  >
67
68
  {variant === 'title' ? (
68
- <TitleExecutionSummary job={job} execution={selected} />
69
+ <TitleExecutionSummary execution={selected} />
69
70
  ) : (
70
71
  <ExecutionSummary execution={selected} />
71
72
  )}
@@ -87,14 +88,19 @@ export function JobExecutionSwitcher({
87
88
  <button
88
89
  type="button"
89
90
  aria-current={isSelected ? 'true' : undefined}
91
+ aria-label={executionAccessibleLabel(jobExecution)}
90
92
  className="w-full text-left"
91
93
  >
92
- <WorkflowStatusIcon status={jobExecution.status} size={14} tooltip={false} />
94
+ <WorkflowStatusIcon
95
+ status={deriveJobExecutionDisplayStatus(jobExecution)}
96
+ size={14}
97
+ tooltip={false}
98
+ />
93
99
  <span className="font-code text-xs leading-20 text-foreground-neutral-base tabular-nums">
94
100
  #{jobExecution.sequence}
95
101
  </span>
96
102
  <span className="min-w-0 truncate text-xs leading-20 text-foreground-neutral-base">
97
- {job.displayName}
103
+ {jobExecution.displayName}
98
104
  </span>
99
105
  {!isSelected && jobExecution.statusReason ? (
100
106
  <span className="min-w-0 flex-1 truncate text-xs leading-20 text-foreground-neutral-muted">
@@ -119,23 +125,39 @@ export function JobExecutionSwitcher({
119
125
  );
120
126
  }
121
127
 
122
- function TitleExecutionSummary({job, execution}: {job: Job; execution: JobExecution}) {
128
+ function executionAccessibleLabel(execution: JobExecution): string {
129
+ const status = getWorkflowStatusVisual(deriveJobExecutionDisplayStatus(execution));
130
+ return [
131
+ `Execution #${execution.sequence}: ${execution.displayName}`,
132
+ status.label,
133
+ execution.statusReason ?? undefined,
134
+ execution.displayDuration
135
+ ? `duration ${formatJobExecutionTime(execution.displayDuration)}`
136
+ : undefined,
137
+ ]
138
+ .filter((part): part is string => Boolean(part))
139
+ .join(', ');
140
+ }
141
+
142
+ function TitleExecutionSummary({execution}: {execution: JobExecution}) {
123
143
  return (
124
144
  <span className="flex min-w-0 items-center gap-8">
125
145
  <Text as="span" size="sm" bold className="shrink-0 text-foreground-neutral-base">
126
146
  #{execution.sequence}
127
147
  </Text>
128
148
  <Text as="span" size="sm" bold className="min-w-0 truncate text-foreground-neutral-base">
129
- {job.displayName}
149
+ {execution.displayName}
130
150
  </Text>
131
151
  </span>
132
152
  );
133
153
  }
134
154
 
135
155
  function ExecutionSummary({execution}: {execution: JobExecution}) {
156
+ const displayStatus = deriveJobExecutionDisplayStatus(execution);
157
+
136
158
  return (
137
159
  <span className="flex min-w-0 items-center gap-6">
138
- <WorkflowStatusIcon status={execution.status} size={14} tooltip={false} />
160
+ <WorkflowStatusIcon status={displayStatus} size={14} tooltip={false} />
139
161
  <span className="shrink-0 font-code text-xs leading-20 text-foreground-neutral-base tabular-nums">
140
162
  Execution #{execution.sequence}
141
163
  </span>
@@ -82,6 +82,29 @@ describe('resolveWorkflowRunSelection', () => {
82
82
  expect(resolved.selectedAttemptId).toBeNull();
83
83
  });
84
84
 
85
+ test('finds a job from a legacy job execution selection', () => {
86
+ const execution = workflowJobExecutionDto({id: 'execution-deploy', sequence: 1});
87
+ const run = workflowRunDetail({
88
+ jobs: [
89
+ workflowJobDto({id: 'job-build', name: 'build'}),
90
+ workflowJobDto({
91
+ id: 'job-deploy',
92
+ name: 'deploy',
93
+ position: 1,
94
+ job_executions: [execution],
95
+ }),
96
+ ],
97
+ });
98
+
99
+ const resolved = resolveWorkflowRunSelection({
100
+ run,
101
+ selection: {jobExecutionId: 'execution-deploy'},
102
+ });
103
+
104
+ expect(resolved.job?.id).toBe('job-deploy');
105
+ expect(resolved.jobExecution?.id).toBe('execution-deploy');
106
+ });
107
+
85
108
  test('falls back from an invalid job execution id to the running execution', () => {
86
109
  const run = workflowRunDetail({
87
110
  jobs: [
@@ -156,6 +179,31 @@ describe('resolveWorkflowRunSelection', () => {
156
179
  expect(resolved.selectedAttemptId).toBe('attempt-2');
157
180
  });
158
181
 
182
+ test('finds the step owner from a legacy step attempt selection', () => {
183
+ const attempt = workflowStepAttemptDto({id: 'attempt-deploy', step_id: 'step-deploy'});
184
+ const step = workflowStepDto({
185
+ id: 'step-deploy',
186
+ current_attempt: 1,
187
+ attempts: [attempt],
188
+ });
189
+ const run = workflowRunDetail({
190
+ jobs: [
191
+ workflowJobDto({id: 'job-build', name: 'build'}),
192
+ workflowJobDto({id: 'job-deploy', name: 'deploy', position: 1, steps: [step]}),
193
+ ],
194
+ });
195
+
196
+ const resolved = resolveWorkflowRunSelection({
197
+ run,
198
+ selection: {stepAttemptId: 'attempt-deploy'},
199
+ });
200
+
201
+ expect(resolved.job?.id).toBe('job-deploy');
202
+ expect(resolved.step?.id).toBe('step-deploy');
203
+ expect(resolved.attempt?.id).toBe('attempt-deploy');
204
+ expect(resolved.selectedAttemptId).toBe('attempt-deploy');
205
+ });
206
+
159
207
  test('lets a valid step override a mismatched job id', () => {
160
208
  const step = workflowStepDto({
161
209
  id: 'step-deploy',
@@ -25,6 +25,8 @@ export function resolveWorkflowRunSelection({
25
25
  }): ResolvedWorkflowRunSelection {
26
26
  const jobById = new Map(run.jobs.map((job) => [job.id, job]));
27
27
  const stepMatch = findStep(run, selection.stepId);
28
+ const jobExecutionMatch = findJobExecution(run, selection.jobExecutionId);
29
+ const stepAttemptMatch = findStepAttempt(run, selection.stepAttemptId);
28
30
 
29
31
  if (stepMatch) {
30
32
  const attempt = resolveStepAttempt(stepMatch.step, selection.stepAttemptId);
@@ -37,7 +39,20 @@ export function resolveWorkflowRunSelection({
37
39
  };
38
40
  }
39
41
 
40
- const job = (selection.jobId ? jobById.get(selection.jobId) : undefined) ?? run.jobs.at(0);
42
+ if (stepAttemptMatch) {
43
+ return {
44
+ job: stepAttemptMatch.job,
45
+ jobExecution: stepAttemptMatch.jobExecution,
46
+ step: stepAttemptMatch.step,
47
+ attempt: stepAttemptMatch.attempt,
48
+ selectedAttemptId: stepAttemptMatch.attempt.id,
49
+ };
50
+ }
51
+
52
+ const job =
53
+ (selection.jobId ? jobById.get(selection.jobId) : undefined) ??
54
+ jobExecutionMatch?.job ??
55
+ run.jobs.at(0);
41
56
  const jobExecution = job ? resolveJobExecution(job, selection.jobExecutionId) : undefined;
42
57
 
43
58
  return {
@@ -49,6 +64,38 @@ export function resolveWorkflowRunSelection({
49
64
  };
50
65
  }
51
66
 
67
+ function findJobExecution(
68
+ run: WorkflowRunDetail,
69
+ jobExecutionId: string | undefined,
70
+ ): {job: Job; jobExecution: JobExecution} | undefined {
71
+ if (!jobExecutionId) return undefined;
72
+
73
+ for (const job of run.jobs) {
74
+ const jobExecution = job.jobExecutions.find((candidate) => candidate.id === jobExecutionId);
75
+ if (jobExecution) return {job, jobExecution};
76
+ }
77
+
78
+ return undefined;
79
+ }
80
+
81
+ function findStepAttempt(
82
+ run: WorkflowRunDetail,
83
+ stepAttemptId: string | undefined,
84
+ ): {job: Job; jobExecution: JobExecution; step: Step; attempt: StepAttempt} | undefined {
85
+ if (!stepAttemptId) return undefined;
86
+
87
+ for (const job of run.jobs) {
88
+ for (const jobExecution of job.jobExecutions) {
89
+ for (const step of jobExecution.steps) {
90
+ const attempt = step.attempts.find((candidate) => candidate.id === stepAttemptId);
91
+ if (attempt) return {job, jobExecution, step, attempt};
92
+ }
93
+ }
94
+ }
95
+
96
+ return undefined;
97
+ }
98
+
52
99
  function findStep(
53
100
  run: WorkflowRunDetail,
54
101
  stepId: string | undefined,
@@ -9,7 +9,7 @@ export function WorkflowRunSkeleton() {
9
9
  return (
10
10
  <section
11
11
  aria-label="Loading workflow run"
12
- className="border-b border-border-neutral-base bg-background-subtle-base px-16 py-12"
12
+ className="bg-background-neutral-background px-16 py-12"
13
13
  >
14
14
  <div className="flex min-w-0 flex-wrap items-center gap-x-12 gap-y-8">
15
15
  <div className="flex min-w-0 items-center gap-8">
@@ -31,6 +31,17 @@ export function WorkflowRunSkeleton() {
31
31
  );
32
32
  }
33
33
 
34
+ export function WorkflowRunContentSkeleton() {
35
+ return (
36
+ <section
37
+ aria-label="Loading workflow run content"
38
+ className="min-h-160 rounded-8 border border-border-neutral-base bg-background-components-base p-16"
39
+ >
40
+ <Skeleton className="h-160 w-full rounded-6" />
41
+ </section>
42
+ );
43
+ }
44
+
34
45
  export function WorkflowRunNotFound() {
35
46
  return (
36
47
  <EmptyState