@shipfox/client-workflows 0.2.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 (327) hide show
  1. package/.storybook/main.ts +1 -0
  2. package/.storybook/preview.css +10 -0
  3. package/.storybook/preview.tsx +97 -0
  4. package/.swcrc +42 -0
  5. package/.turbo/turbo-build.log +2 -0
  6. package/.turbo/turbo-type$colon$emit.log +1 -0
  7. package/.turbo/turbo-type.log +1 -0
  8. package/CHANGELOG.md +162 -0
  9. package/LICENSE +21 -0
  10. package/dist/components/identifier/identifier.d.ts +6 -0
  11. package/dist/components/identifier/identifier.d.ts.map +1 -0
  12. package/dist/components/identifier/identifier.js +144 -0
  13. package/dist/components/identifier/identifier.js.map +1 -0
  14. package/dist/components/identifier/index.d.ts +2 -0
  15. package/dist/components/identifier/index.d.ts.map +1 -0
  16. package/dist/components/identifier/index.js +3 -0
  17. package/dist/components/identifier/index.js.map +1 -0
  18. package/dist/components/job-graph/graph-model.d.ts +27 -0
  19. package/dist/components/job-graph/graph-model.d.ts.map +1 -0
  20. package/dist/components/job-graph/graph-model.js +107 -0
  21. package/dist/components/job-graph/graph-model.js.map +1 -0
  22. package/dist/components/job-graph/index.d.ts +3 -0
  23. package/dist/components/job-graph/index.d.ts.map +1 -0
  24. package/dist/components/job-graph/index.js +3 -0
  25. package/dist/components/job-graph/index.js.map +1 -0
  26. package/dist/components/job-graph/job-duration-format.d.ts +4 -0
  27. package/dist/components/job-graph/job-duration-format.d.ts.map +1 -0
  28. package/dist/components/job-graph/job-duration-format.js +41 -0
  29. package/dist/components/job-graph/job-duration-format.js.map +1 -0
  30. package/dist/components/job-graph/job-duration-label.d.ts +5 -0
  31. package/dist/components/job-graph/job-duration-label.d.ts.map +1 -0
  32. package/dist/components/job-graph/job-duration-label.js +33 -0
  33. package/dist/components/job-graph/job-duration-label.js.map +1 -0
  34. package/dist/components/job-graph/job-graph-content.d.ts +9 -0
  35. package/dist/components/job-graph/job-graph-content.d.ts.map +1 -0
  36. package/dist/components/job-graph/job-graph-content.js +181 -0
  37. package/dist/components/job-graph/job-graph-content.js.map +1 -0
  38. package/dist/components/job-graph/job-graph-view.d.ts +11 -0
  39. package/dist/components/job-graph/job-graph-view.d.ts.map +1 -0
  40. package/dist/components/job-graph/job-graph-view.js +24 -0
  41. package/dist/components/job-graph/job-graph-view.js.map +1 -0
  42. package/dist/components/job-graph/job-graph.d.ts +3 -0
  43. package/dist/components/job-graph/job-graph.d.ts.map +1 -0
  44. package/dist/components/job-graph/job-graph.js +21 -0
  45. package/dist/components/job-graph/job-graph.js.map +1 -0
  46. package/dist/components/job-graph/job-node.d.ts +16 -0
  47. package/dist/components/job-graph/job-node.d.ts.map +1 -0
  48. package/dist/components/job-graph/job-node.js +196 -0
  49. package/dist/components/job-graph/job-node.js.map +1 -0
  50. package/dist/components/job-graph/types.d.ts +9 -0
  51. package/dist/components/job-graph/types.d.ts.map +1 -0
  52. package/dist/components/job-graph/types.js +3 -0
  53. package/dist/components/job-graph/types.js.map +1 -0
  54. package/dist/components/step-list/index.d.ts +3 -0
  55. package/dist/components/step-list/index.d.ts.map +1 -0
  56. package/dist/components/step-list/index.js +3 -0
  57. package/dist/components/step-list/index.js.map +1 -0
  58. package/dist/components/step-list/step-list-model.d.ts +36 -0
  59. package/dist/components/step-list/step-list-model.d.ts.map +1 -0
  60. package/dist/components/step-list/step-list-model.js +135 -0
  61. package/dist/components/step-list/step-list-model.js.map +1 -0
  62. package/dist/components/step-list/step-list.d.ts +32 -0
  63. package/dist/components/step-list/step-list.d.ts.map +1 -0
  64. package/dist/components/step-list/step-list.js +409 -0
  65. package/dist/components/step-list/step-list.js.map +1 -0
  66. package/dist/components/workflow-run-duration-label.d.ts +8 -0
  67. package/dist/components/workflow-run-duration-label.d.ts.map +1 -0
  68. package/dist/components/workflow-run-duration-label.js +84 -0
  69. package/dist/components/workflow-run-duration-label.js.map +1 -0
  70. package/dist/components/workflow-run-list/index.d.ts +2 -0
  71. package/dist/components/workflow-run-list/index.d.ts.map +1 -0
  72. package/dist/components/workflow-run-list/index.js +3 -0
  73. package/dist/components/workflow-run-list/index.js.map +1 -0
  74. package/dist/components/workflow-run-list/run-display.d.ts +5 -0
  75. package/dist/components/workflow-run-list/run-display.d.ts.map +1 -0
  76. package/dist/components/workflow-run-list/run-display.js +19 -0
  77. package/dist/components/workflow-run-list/run-display.js.map +1 -0
  78. package/dist/components/workflow-run-list/types.d.ts +21 -0
  79. package/dist/components/workflow-run-list/types.d.ts.map +1 -0
  80. package/dist/components/workflow-run-list/types.js +3 -0
  81. package/dist/components/workflow-run-list/types.js.map +1 -0
  82. package/dist/components/workflow-run-list/workflow-run-list-content.d.ts +14 -0
  83. package/dist/components/workflow-run-list/workflow-run-list-content.d.ts.map +1 -0
  84. package/dist/components/workflow-run-list/workflow-run-list-content.js +37 -0
  85. package/dist/components/workflow-run-list/workflow-run-list-content.js.map +1 -0
  86. package/dist/components/workflow-run-list/workflow-run-list-header.d.ts +10 -0
  87. package/dist/components/workflow-run-list/workflow-run-list-header.d.ts.map +1 -0
  88. package/dist/components/workflow-run-list/workflow-run-list-header.js +55 -0
  89. package/dist/components/workflow-run-list/workflow-run-list-header.js.map +1 -0
  90. package/dist/components/workflow-run-list/workflow-run-list-states.d.ts +15 -0
  91. package/dist/components/workflow-run-list/workflow-run-list-states.d.ts.map +1 -0
  92. package/dist/components/workflow-run-list/workflow-run-list-states.js +103 -0
  93. package/dist/components/workflow-run-list/workflow-run-list-states.js.map +1 -0
  94. package/dist/components/workflow-run-list/workflow-run-list-view.d.ts +3 -0
  95. package/dist/components/workflow-run-list/workflow-run-list-view.d.ts.map +1 -0
  96. package/dist/components/workflow-run-list/workflow-run-list-view.js +46 -0
  97. package/dist/components/workflow-run-list/workflow-run-list-view.js.map +1 -0
  98. package/dist/components/workflow-run-list/workflow-run-list.d.ts +3 -0
  99. package/dist/components/workflow-run-list/workflow-run-list.d.ts.map +1 -0
  100. package/dist/components/workflow-run-list/workflow-run-list.js +20 -0
  101. package/dist/components/workflow-run-list/workflow-run-list.js.map +1 -0
  102. package/dist/components/workflow-run-list/workflow-run-row.d.ts +14 -0
  103. package/dist/components/workflow-run-list/workflow-run-row.d.ts.map +1 -0
  104. package/dist/components/workflow-run-list/workflow-run-row.js +172 -0
  105. package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -0
  106. package/dist/components/workflow-run-summary/index.d.ts +2 -0
  107. package/dist/components/workflow-run-summary/index.d.ts.map +1 -0
  108. package/dist/components/workflow-run-summary/index.js +3 -0
  109. package/dist/components/workflow-run-summary/index.js.map +1 -0
  110. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.d.ts +9 -0
  111. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.d.ts.map +1 -0
  112. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js +138 -0
  113. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js.map +1 -0
  114. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts +20 -0
  115. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -0
  116. package/dist/components/workflow-run-summary/workflow-run-summary.js +252 -0
  117. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -0
  118. package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts +7 -0
  119. package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts.map +1 -0
  120. package/dist/components/workflow-run-view/agent-config-failure-callout.js +84 -0
  121. package/dist/components/workflow-run-view/agent-config-failure-callout.js.map +1 -0
  122. package/dist/components/workflow-run-view/index.d.ts +3 -0
  123. package/dist/components/workflow-run-view/index.d.ts.map +1 -0
  124. package/dist/components/workflow-run-view/index.js +4 -0
  125. package/dist/components/workflow-run-view/index.js.map +1 -0
  126. package/dist/components/workflow-run-view/job-card.d.ts +17 -0
  127. package/dist/components/workflow-run-view/job-card.d.ts.map +1 -0
  128. package/dist/components/workflow-run-view/job-card.js +476 -0
  129. package/dist/components/workflow-run-view/job-card.js.map +1 -0
  130. package/dist/components/workflow-run-view/job-execution-switcher.d.ts +10 -0
  131. package/dist/components/workflow-run-view/job-execution-switcher.d.ts.map +1 -0
  132. package/dist/components/workflow-run-view/job-execution-switcher.js +171 -0
  133. package/dist/components/workflow-run-view/job-execution-switcher.js.map +1 -0
  134. package/dist/components/workflow-run-view/job-execution-time-text.d.ts +6 -0
  135. package/dist/components/workflow-run-view/job-execution-time-text.d.ts.map +1 -0
  136. package/dist/components/workflow-run-view/job-execution-time-text.js +24 -0
  137. package/dist/components/workflow-run-view/job-execution-time-text.js.map +1 -0
  138. package/dist/components/workflow-run-view/step-attempt-log-panel.d.ts +9 -0
  139. package/dist/components/workflow-run-view/step-attempt-log-panel.d.ts.map +1 -0
  140. package/dist/components/workflow-run-view/step-attempt-log-panel.js +152 -0
  141. package/dist/components/workflow-run-view/step-attempt-log-panel.js.map +1 -0
  142. package/dist/components/workflow-run-view/workflow-run-selection.d.ts +14 -0
  143. package/dist/components/workflow-run-view/workflow-run-selection.d.ts.map +1 -0
  144. package/dist/components/workflow-run-view/workflow-run-selection.js +56 -0
  145. package/dist/components/workflow-run-view/workflow-run-selection.js.map +1 -0
  146. package/dist/components/workflow-run-view/workflow-run-states.d.ts +12 -0
  147. package/dist/components/workflow-run-view/workflow-run-states.d.ts.map +1 -0
  148. package/dist/components/workflow-run-view/workflow-run-states.js +91 -0
  149. package/dist/components/workflow-run-view/workflow-run-states.js.map +1 -0
  150. package/dist/components/workflow-run-view/workflow-run-view.d.ts +15 -0
  151. package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -0
  152. package/dist/components/workflow-run-view/workflow-run-view.js +285 -0
  153. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -0
  154. package/dist/components/workflow-source-panel/index.d.ts +2 -0
  155. package/dist/components/workflow-source-panel/index.d.ts.map +1 -0
  156. package/dist/components/workflow-source-panel/index.js +3 -0
  157. package/dist/components/workflow-source-panel/index.js.map +1 -0
  158. package/dist/components/workflow-source-panel/workflow-source-panel.d.ts +13 -0
  159. package/dist/components/workflow-source-panel/workflow-source-panel.d.ts.map +1 -0
  160. package/dist/components/workflow-source-panel/workflow-source-panel.js +91 -0
  161. package/dist/components/workflow-source-panel/workflow-source-panel.js.map +1 -0
  162. package/dist/components/workflow-status/status-visuals.d.ts +12 -0
  163. package/dist/components/workflow-status/status-visuals.d.ts.map +1 -0
  164. package/dist/components/workflow-status/status-visuals.js +54 -0
  165. package/dist/components/workflow-status/status-visuals.js.map +1 -0
  166. package/dist/components/workflow-status/workflow-status-icon.d.ts +22 -0
  167. package/dist/components/workflow-status/workflow-status-icon.d.ts.map +1 -0
  168. package/dist/components/workflow-status/workflow-status-icon.js +100 -0
  169. package/dist/components/workflow-status/workflow-status-icon.js.map +1 -0
  170. package/dist/core/entities/job-execution.d.ts +69 -0
  171. package/dist/core/entities/job-execution.d.ts.map +1 -0
  172. package/dist/core/entities/job-execution.js +83 -0
  173. package/dist/core/entities/job-execution.js.map +1 -0
  174. package/dist/core/entities/job.d.ts +56 -0
  175. package/dist/core/entities/job.d.ts.map +1 -0
  176. package/dist/core/entities/job.js +71 -0
  177. package/dist/core/entities/job.js.map +1 -0
  178. package/dist/core/entities/step-attempt.d.ts +49 -0
  179. package/dist/core/entities/step-attempt.d.ts.map +1 -0
  180. package/dist/core/entities/step-attempt.js +48 -0
  181. package/dist/core/entities/step-attempt.js.map +1 -0
  182. package/dist/core/entities/step.d.ts +41 -0
  183. package/dist/core/entities/step.d.ts.map +1 -0
  184. package/dist/core/entities/step.js +51 -0
  185. package/dist/core/entities/step.js.map +1 -0
  186. package/dist/core/entities/workflow-run-attempt.d.ts +44 -0
  187. package/dist/core/entities/workflow-run-attempt.d.ts.map +1 -0
  188. package/dist/core/entities/workflow-run-attempt.js +51 -0
  189. package/dist/core/entities/workflow-run-attempt.js.map +1 -0
  190. package/dist/core/entities/workflow-run.d.ts +61 -0
  191. package/dist/core/entities/workflow-run.d.ts.map +1 -0
  192. package/dist/core/entities/workflow-run.js +104 -0
  193. package/dist/core/entities/workflow-run.js.map +1 -0
  194. package/dist/core/workflow-run-url-state.d.ts +13 -0
  195. package/dist/core/workflow-run-url-state.d.ts.map +1 -0
  196. package/dist/core/workflow-run-url-state.js +43 -0
  197. package/dist/core/workflow-run-url-state.js.map +1 -0
  198. package/dist/core/workflow-run.d.ts +13 -0
  199. package/dist/core/workflow-run.d.ts.map +1 -0
  200. package/dist/core/workflow-run.js +8 -0
  201. package/dist/core/workflow-run.js.map +1 -0
  202. package/dist/env.d.js +2 -0
  203. package/dist/env.d.js.map +1 -0
  204. package/dist/feature.d.ts +20 -0
  205. package/dist/feature.d.ts.map +1 -0
  206. package/dist/feature.js +27 -0
  207. package/dist/feature.js.map +1 -0
  208. package/dist/hooks/api/workflow-runs.d.ts +109 -0
  209. package/dist/hooks/api/workflow-runs.d.ts.map +1 -0
  210. package/dist/hooks/api/workflow-runs.js +394 -0
  211. package/dist/hooks/api/workflow-runs.js.map +1 -0
  212. package/dist/index.d.ts +5 -0
  213. package/dist/index.d.ts.map +1 -0
  214. package/dist/index.js +5 -0
  215. package/dist/index.js.map +1 -0
  216. package/dist/pages/workflow-run-first-time-use.d.ts +6 -0
  217. package/dist/pages/workflow-run-first-time-use.d.ts.map +1 -0
  218. package/dist/pages/workflow-run-first-time-use.js +17 -0
  219. package/dist/pages/workflow-run-first-time-use.js.map +1 -0
  220. package/dist/pages/workflow-run-page.d.ts +8 -0
  221. package/dist/pages/workflow-run-page.d.ts.map +1 -0
  222. package/dist/pages/workflow-run-page.js +83 -0
  223. package/dist/pages/workflow-run-page.js.map +1 -0
  224. package/dist/routes/run-detail.d.ts +8 -0
  225. package/dist/routes/run-detail.d.ts.map +1 -0
  226. package/dist/routes/run-detail.js +21 -0
  227. package/dist/routes/run-detail.js.map +1 -0
  228. package/dist/routes/runs.d.ts +8 -0
  229. package/dist/routes/runs.d.ts.map +1 -0
  230. package/dist/routes/runs.js +20 -0
  231. package/dist/routes/runs.js.map +1 -0
  232. package/dist/tsconfig.test.tsbuildinfo +1 -0
  233. package/package.json +114 -0
  234. package/src/components/identifier/identifier.tsx +178 -0
  235. package/src/components/identifier/index.ts +1 -0
  236. package/src/components/job-graph/graph-model.test.ts +189 -0
  237. package/src/components/job-graph/graph-model.ts +178 -0
  238. package/src/components/job-graph/index.ts +2 -0
  239. package/src/components/job-graph/job-duration-format.ts +54 -0
  240. package/src/components/job-graph/job-duration-label.tsx +29 -0
  241. package/src/components/job-graph/job-graph-content.tsx +225 -0
  242. package/src/components/job-graph/job-graph-view.test.tsx +298 -0
  243. package/src/components/job-graph/job-graph-view.tsx +45 -0
  244. package/src/components/job-graph/job-graph.stories.tsx +182 -0
  245. package/src/components/job-graph/job-graph.tsx +24 -0
  246. package/src/components/job-graph/job-node.stories.tsx +364 -0
  247. package/src/components/job-graph/job-node.test.tsx +347 -0
  248. package/src/components/job-graph/job-node.tsx +202 -0
  249. package/src/components/job-graph/types.ts +9 -0
  250. package/src/components/step-list/index.ts +6 -0
  251. package/src/components/step-list/step-list-model.test.ts +369 -0
  252. package/src/components/step-list/step-list-model.ts +205 -0
  253. package/src/components/step-list/step-list.stories.tsx +419 -0
  254. package/src/components/step-list/step-list.test.tsx +566 -0
  255. package/src/components/step-list/step-list.tsx +494 -0
  256. package/src/components/workflow-run-duration-label.tsx +118 -0
  257. package/src/components/workflow-run-list/index.ts +1 -0
  258. package/src/components/workflow-run-list/run-display.test.ts +53 -0
  259. package/src/components/workflow-run-list/run-display.ts +22 -0
  260. package/src/components/workflow-run-list/types.ts +22 -0
  261. package/src/components/workflow-run-list/workflow-run-list-content.tsx +58 -0
  262. package/src/components/workflow-run-list/workflow-run-list-header.tsx +53 -0
  263. package/src/components/workflow-run-list/workflow-run-list-states.tsx +86 -0
  264. package/src/components/workflow-run-list/workflow-run-list-view.test.tsx +178 -0
  265. package/src/components/workflow-run-list/workflow-run-list-view.tsx +57 -0
  266. package/src/components/workflow-run-list/workflow-run-list.stories.tsx +111 -0
  267. package/src/components/workflow-run-list/workflow-run-list.tsx +29 -0
  268. package/src/components/workflow-run-list/workflow-run-row.tsx +176 -0
  269. package/src/components/workflow-run-summary/index.ts +1 -0
  270. package/src/components/workflow-run-summary/workflow-run-attempt-switcher.test.tsx +230 -0
  271. package/src/components/workflow-run-summary/workflow-run-attempt-switcher.tsx +146 -0
  272. package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +414 -0
  273. package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +354 -0
  274. package/src/components/workflow-run-summary/workflow-run-summary.tsx +295 -0
  275. package/src/components/workflow-run-view/agent-config-failure-callout.stories.tsx +140 -0
  276. package/src/components/workflow-run-view/agent-config-failure-callout.tsx +93 -0
  277. package/src/components/workflow-run-view/index.ts +2 -0
  278. package/src/components/workflow-run-view/job-card.stories.tsx +788 -0
  279. package/src/components/workflow-run-view/job-card.tsx +566 -0
  280. package/src/components/workflow-run-view/job-execution-switcher.tsx +170 -0
  281. package/src/components/workflow-run-view/job-execution-time-text.tsx +34 -0
  282. package/src/components/workflow-run-view/step-attempt-log-panel.test.tsx +290 -0
  283. package/src/components/workflow-run-view/step-attempt-log-panel.tsx +152 -0
  284. package/src/components/workflow-run-view/workflow-run-selection.test.ts +280 -0
  285. package/src/components/workflow-run-view/workflow-run-selection.ts +89 -0
  286. package/src/components/workflow-run-view/workflow-run-states.tsx +70 -0
  287. package/src/components/workflow-run-view/workflow-run-view.test.tsx +1214 -0
  288. package/src/components/workflow-run-view/workflow-run-view.tsx +341 -0
  289. package/src/components/workflow-source-panel/index.ts +1 -0
  290. package/src/components/workflow-source-panel/workflow-source-panel.stories.tsx +110 -0
  291. package/src/components/workflow-source-panel/workflow-source-panel.test.tsx +96 -0
  292. package/src/components/workflow-source-panel/workflow-source-panel.tsx +136 -0
  293. package/src/components/workflow-status/status-visuals.test.ts +38 -0
  294. package/src/components/workflow-status/status-visuals.ts +36 -0
  295. package/src/components/workflow-status/workflow-status-icon.stories.tsx +90 -0
  296. package/src/components/workflow-status/workflow-status-icon.tsx +135 -0
  297. package/src/core/entities/job-execution.ts +139 -0
  298. package/src/core/entities/job.ts +149 -0
  299. package/src/core/entities/step-attempt.ts +89 -0
  300. package/src/core/entities/step.ts +105 -0
  301. package/src/core/entities/workflow-run-attempt.ts +87 -0
  302. package/src/core/entities/workflow-run.ts +183 -0
  303. package/src/core/workflow-run-url-state.ts +61 -0
  304. package/src/core/workflow-run.test.ts +640 -0
  305. package/src/core/workflow-run.ts +66 -0
  306. package/src/env.d.ts +8 -0
  307. package/src/feature.ts +26 -0
  308. package/src/hooks/api/workflow-runs.test.tsx +410 -0
  309. package/src/hooks/api/workflow-runs.ts +485 -0
  310. package/src/index.ts +45 -0
  311. package/src/page-harness-budget.test.ts +33 -0
  312. package/src/pages/workflow-run-first-time-use.tsx +17 -0
  313. package/src/pages/workflow-run-page.test.tsx +517 -0
  314. package/src/pages/workflow-run-page.tsx +89 -0
  315. package/src/routes/run-detail.tsx +15 -0
  316. package/src/routes/runs.tsx +11 -0
  317. package/test/fixtures/logs.ts +13 -0
  318. package/test/fixtures/workflow-run.ts +311 -0
  319. package/test/pages.tsx +79 -0
  320. package/test/render.tsx +40 -0
  321. package/test/setup.ts +3 -0
  322. package/tsconfig.build.json +9 -0
  323. package/tsconfig.build.tsbuildinfo +1 -0
  324. package/tsconfig.json +3 -0
  325. package/tsconfig.test.json +8 -0
  326. package/vercel.json +8 -0
  327. package/vitest.config.ts +70 -0
@@ -0,0 +1,788 @@
1
+ import type {
2
+ WorkflowExecutionEventDto,
3
+ WorkflowRunJobDetailDto,
4
+ WorkflowRunJobExecutionDetailDto,
5
+ WorkflowRunStepDetailDto,
6
+ } from '@shipfox/api-workflows-dto';
7
+ import {Text} from '@shipfox/react-ui/typography';
8
+ import type {Meta, StoryObj} from '@storybook/react';
9
+ import {type ReactNode, useState} from 'react';
10
+ import {screen, userEvent, within} from 'storybook/test';
11
+ import {type Job, resolveJobExecution} from '#core/workflow-run.js';
12
+ import {
13
+ workflowJob,
14
+ workflowJobExecutionDto,
15
+ workflowStepAttemptDto,
16
+ workflowStepDto,
17
+ } from '#test/fixtures/workflow-run.js';
18
+ import type {StepExpandedContext} from '../step-list/index.js';
19
+ import {JobCard} from './job-card.js';
20
+
21
+ const WORKSPACE_ID = '44444444-4444-4444-8444-444444444444';
22
+ const SWITCH_EXECUTION_PATTERN = /Switch job execution/;
23
+ const EXECUTION_FIVE_PATTERN = /#5/;
24
+
25
+ const QUEUED_AT = '2026-06-26T11:54:00.000Z';
26
+ const STARTED_AT = '2026-06-26T11:57:46.000Z';
27
+ const FINISHED_AT = '2026-06-26T12:00:00.000Z';
28
+ const LISTENING_BASE_AT = Date.parse('2026-06-26T10:00:00.000Z');
29
+
30
+ const meta = {
31
+ title: 'Workflows/JobCard',
32
+ component: JobCard,
33
+ parameters: {
34
+ layout: 'centered',
35
+ },
36
+ decorators: [
37
+ (Story) => (
38
+ <div className="w-760 bg-background-neutral-base p-16">
39
+ <Story />
40
+ </div>
41
+ ),
42
+ ],
43
+ args: {
44
+ workspaceId: WORKSPACE_ID,
45
+ job: singleExecutionJob(),
46
+ selectedJobExecution: singleExecutionJob().jobExecutions[0],
47
+ selectedAttemptId: null,
48
+ onSelectedJobExecutionChange: undefined,
49
+ onSelectedAttemptChange: undefined,
50
+ renderExpandedStep: StepDetailPlaceholder,
51
+ },
52
+ } satisfies Meta<typeof JobCard>;
53
+
54
+ export default meta;
55
+ type Story = StoryObj<typeof meta>;
56
+ type JobCardStoryContext = Parameters<NonNullable<Story['play']>>[0];
57
+
58
+ async function openJobExecutionSwitcher(ctx: JobCardStoryContext) {
59
+ const canvas = within(ctx.canvasElement);
60
+
61
+ await userEvent.click(await canvas.findByRole('button', {name: SWITCH_EXECUTION_PATTERN}));
62
+ await screen.findByRole('menu');
63
+ await screen.findByRole('menuitem', {name: EXECUTION_FIVE_PATTERN});
64
+ }
65
+
66
+ export const Playground: Story = {
67
+ render: () => <JobCardStory job={singleExecutionJob()} />,
68
+ };
69
+
70
+ export const States: Story = {
71
+ render: () => (
72
+ <div className="flex flex-col gap-16">
73
+ <StorySection label="single execution">
74
+ <JobCardStory job={singleExecutionJob()} />
75
+ </StorySection>
76
+ <StorySection label="display name fallback">
77
+ <JobCardStory job={keyFallbackJob()} />
78
+ </StorySection>
79
+ <StorySection label="queueing execution">
80
+ <JobCardStory job={queueingExecutionJob()} initialJobExecutionId="exec-queueing" />
81
+ </StorySection>
82
+ <StorySection label="failed then succeeded">
83
+ <JobCardStory job={failedThenSucceededJob()} initialJobExecutionId="exec-2" />
84
+ </StorySection>
85
+ <StorySection label="running retry">
86
+ <JobCardStory job={runningRetryJob()} initialJobExecutionId="exec-2" />
87
+ </StorySection>
88
+ </div>
89
+ ),
90
+ };
91
+
92
+ export const EmptyStates: Story = {
93
+ render: () => (
94
+ <div className="flex flex-col gap-16">
95
+ <StorySection label="pending, no execution">
96
+ <JobCardStory job={pendingNoExecutionJob()} />
97
+ </StorySection>
98
+ <StorySection label="listening, waiting for trigger events">
99
+ <JobCardStory job={listeningJobNoExecutions()} />
100
+ </StorySection>
101
+ <StorySection label="listener resolved, no executions">
102
+ <JobCardStory job={listeningJobResolvedNoExecutions()} />
103
+ </StorySection>
104
+ <StorySection label="skipped, no execution">
105
+ <JobCardStory job={zeroExecutionJob()} />
106
+ </StorySection>
107
+ <StorySection label="cancelled, no execution">
108
+ <JobCardStory job={cancelledNoExecutionJob()} />
109
+ </StorySection>
110
+ <StorySection label="finished, missing execution record">
111
+ <JobCardStory job={finishedWithoutExecutionJob()} />
112
+ </StorySection>
113
+ <StorySection label="finished, no steps">
114
+ <JobCardStory job={finishedWithoutStepsJob()} initialJobExecutionId="exec-empty" />
115
+ </StorySection>
116
+ <StorySection label="carried over">
117
+ <JobCardStory job={carriedOverJob()} />
118
+ </StorySection>
119
+ </div>
120
+ ),
121
+ };
122
+
123
+ export const ListeningProgression: Story = {
124
+ render: () => (
125
+ <div className="flex flex-col gap-16">
126
+ <StorySection label="pending, no executions">
127
+ <JobCardStory job={listeningJobPending()} />
128
+ </StorySection>
129
+ <StorySection label="running, 0 executions">
130
+ <JobCardStory job={listeningJobNoExecutions()} />
131
+ </StorySection>
132
+ <StorySection label="running, 1 running execution">
133
+ <JobCardStory job={listeningJobOneExecution()} initialJobExecutionId="exec-listen-one-1" />
134
+ </StorySection>
135
+ <StorySection label="running, mixed executions">
136
+ <JobCardStory job={listeningJobMixedExecutions()} initialJobExecutionId="exec-listen-6" />
137
+ </StorySection>
138
+ <StorySection label="failed, resolved">
139
+ <JobCardStory job={listeningJobResolved()} initialJobExecutionId="exec-listen-resolved-4" />
140
+ </StorySection>
141
+ </div>
142
+ ),
143
+ };
144
+
145
+ export const ManyExecutions: Story = {
146
+ render: () => (
147
+ <div className="flex flex-col gap-16">
148
+ <StorySection label="running, 64 executions, short name">
149
+ <JobCardStory
150
+ job={manyExecutionsListeningJob('gate', 'job-many-short')}
151
+ initialJobExecutionId="exec-many-job-many-short-64"
152
+ />
153
+ </StorySection>
154
+ <StorySection label="running, 64 executions, medium name">
155
+ <JobCardStory
156
+ job={manyExecutionsListeningJob('release-gates', 'job-many-medium')}
157
+ initialJobExecutionId="exec-many-job-many-medium-64"
158
+ />
159
+ </StorySection>
160
+ <StorySection label="running, 64 executions, long name">
161
+ <JobCardStory
162
+ job={manyExecutionsListeningJob(
163
+ 'release-production-multi-region-with-canary-validation',
164
+ 'job-many-long',
165
+ )}
166
+ initialJobExecutionId="exec-many-job-many-long-64"
167
+ />
168
+ </StorySection>
169
+ </div>
170
+ ),
171
+ };
172
+
173
+ export const SourceAction: Story = {
174
+ render: () => (
175
+ <JobCardStory
176
+ job={sourceActionJob()}
177
+ initialJobExecutionId="exec-source-action"
178
+ initialAttemptId="attempt-checkout-exec-source-action"
179
+ sourceAvailable
180
+ sourcePanelId="workflow-source-panel-story"
181
+ />
182
+ ),
183
+ };
184
+
185
+ export const Content: Story = {
186
+ decorators: [
187
+ (Story) => (
188
+ <div className="w-440 bg-background-neutral-base p-16">
189
+ <Story />
190
+ </div>
191
+ ),
192
+ ],
193
+ render: () => (
194
+ <div className="flex flex-col gap-16">
195
+ <StorySection label="regular long name">
196
+ <JobCardStory job={longNameManyExecutionsJob()} initialJobExecutionId="exec-6" />
197
+ </StorySection>
198
+ <StorySection label="listening long name">
199
+ <JobCardStory job={longNameListeningJob()} initialJobExecutionId="exec-long-listening-3" />
200
+ </StorySection>
201
+ </div>
202
+ ),
203
+ };
204
+
205
+ export const TestListeningSwitcherOpen: Story = {
206
+ render: () => (
207
+ <JobCardStory job={listeningJobMixedExecutions()} initialJobExecutionId="exec-listen-6" />
208
+ ),
209
+ play: openJobExecutionSwitcher,
210
+ };
211
+
212
+ function JobCardStory({
213
+ job,
214
+ initialJobExecutionId,
215
+ initialAttemptId = null,
216
+ sourceAvailable,
217
+ sourcePanelId,
218
+ }: {
219
+ job: Job;
220
+ initialJobExecutionId?: string | undefined;
221
+ initialAttemptId?: string | null | undefined;
222
+ sourceAvailable?: boolean | undefined;
223
+ sourcePanelId?: string | undefined;
224
+ }) {
225
+ const [selectedJobExecutionId, setSelectedJobExecutionId] = useState(initialJobExecutionId);
226
+ const [selectedAttemptId, setSelectedAttemptId] = useState<string | null>(initialAttemptId);
227
+ const selectedJobExecution = resolveJobExecution(job, selectedJobExecutionId);
228
+
229
+ function selectJobExecution(jobExecutionId: string | undefined) {
230
+ setSelectedJobExecutionId(jobExecutionId);
231
+ setSelectedAttemptId(null);
232
+ }
233
+
234
+ return (
235
+ <JobCard
236
+ workspaceId={WORKSPACE_ID}
237
+ job={job}
238
+ selectedJobExecution={selectedJobExecution}
239
+ selectedAttemptId={selectedAttemptId}
240
+ onSelectedJobExecutionChange={selectJobExecution}
241
+ onSelectedAttemptChange={(attemptId) => setSelectedAttemptId(attemptId ?? null)}
242
+ renderExpandedStep={StepDetailPlaceholder}
243
+ sourceAvailable={sourceAvailable}
244
+ sourcePanelId={sourcePanelId}
245
+ focusedSourceStepId={null}
246
+ onOpenStepSource={() => undefined}
247
+ />
248
+ );
249
+ }
250
+
251
+ function StorySection({label, children}: {label: string; children: ReactNode}) {
252
+ return (
253
+ <div className="flex min-w-0 flex-col gap-8">
254
+ <Text size="xs" className="font-code text-foreground-neutral-muted">
255
+ {label}
256
+ </Text>
257
+ {children}
258
+ </div>
259
+ );
260
+ }
261
+
262
+ function StepDetailPlaceholder({attempt, attemptStatus}: StepExpandedContext) {
263
+ return (
264
+ <div className="rounded-6 border border-border-neutral-base bg-background-components-base px-12 py-10">
265
+ <Text size="xs" className="font-code text-foreground-neutral-muted">
266
+ attempt #{attempt} - {attemptStatus}
267
+ </Text>
268
+ <Text size="sm" className="text-foreground-neutral-subtle">
269
+ Step detail content renders here in the run viewer.
270
+ </Text>
271
+ </div>
272
+ );
273
+ }
274
+
275
+ function singleExecutionJob(): Job {
276
+ return makeJob({
277
+ id: 'job-build',
278
+ name: 'build',
279
+ status: 'succeeded',
280
+ job_executions: [
281
+ makeExecution({
282
+ id: 'exec-1',
283
+ job_id: 'job-build',
284
+ sequence: 1,
285
+ status: 'succeeded',
286
+ queued_at: QUEUED_AT,
287
+ started_at: STARTED_AT,
288
+ finished_at: FINISHED_AT,
289
+ steps: [
290
+ makeStep('checkout', 'succeeded', 0, 'exec-1'),
291
+ makeStep('install', 'succeeded', 1, 'exec-1'),
292
+ makeStep('test', 'succeeded', 2, 'exec-1'),
293
+ ],
294
+ }),
295
+ ],
296
+ });
297
+ }
298
+
299
+ function keyFallbackJob(): Job {
300
+ return makeJob({
301
+ id: 'job-key-fallback',
302
+ key: 'release-production',
303
+ name: null,
304
+ status: 'running',
305
+ job_executions: [
306
+ makeExecution({
307
+ id: 'exec-key-fallback',
308
+ job_id: 'job-key-fallback',
309
+ sequence: 1,
310
+ status: 'running',
311
+ queued_at: QUEUED_AT,
312
+ started_at: STARTED_AT,
313
+ steps: [
314
+ makeStep('package', 'succeeded', 0, 'exec-key-fallback'),
315
+ makeStep('deploy', 'running', 1, 'exec-key-fallback'),
316
+ ],
317
+ }),
318
+ ],
319
+ });
320
+ }
321
+
322
+ function queueingExecutionJob(): Job {
323
+ return makeJob({
324
+ id: 'job-queueing',
325
+ name: 'allocate-runner',
326
+ status: 'pending',
327
+ job_executions: [
328
+ makeExecution({
329
+ id: 'exec-queueing',
330
+ job_id: 'job-queueing',
331
+ sequence: 1,
332
+ status: 'pending',
333
+ queued_at: QUEUED_AT,
334
+ started_at: null,
335
+ finished_at: null,
336
+ steps: [],
337
+ }),
338
+ ],
339
+ });
340
+ }
341
+
342
+ function failedThenSucceededJob(): Job {
343
+ return makeJob({
344
+ id: 'job-deploy',
345
+ name: 'deploy',
346
+ status: 'succeeded',
347
+ job_executions: [
348
+ makeExecution({
349
+ id: 'exec-1',
350
+ job_id: 'job-deploy',
351
+ sequence: 1,
352
+ status: 'failed',
353
+ status_reason: 'step_failed',
354
+ queued_at: '2026-06-26T11:46:00.000Z',
355
+ started_at: '2026-06-26T11:48:00.000Z',
356
+ finished_at: '2026-06-26T11:50:30.000Z',
357
+ steps: [
358
+ makeStep('deploy', 'failed', 0, 'exec-1', {
359
+ attempts: [
360
+ workflowStepAttemptDto({
361
+ id: 'attempt-deploy-1',
362
+ step_id: 'step-deploy-1',
363
+ status: 'failed',
364
+ exit_code: 1,
365
+ error: {message: 'Deployment failed'},
366
+ }),
367
+ ],
368
+ }),
369
+ ],
370
+ }),
371
+ makeExecution({
372
+ id: 'exec-2',
373
+ job_id: 'job-deploy',
374
+ sequence: 2,
375
+ status: 'succeeded',
376
+ queued_at: QUEUED_AT,
377
+ started_at: STARTED_AT,
378
+ finished_at: FINISHED_AT,
379
+ steps: [
380
+ makeStep('deploy', 'succeeded', 0, 'exec-2'),
381
+ makeStep('smoke-test', 'succeeded', 1, 'exec-2'),
382
+ ],
383
+ }),
384
+ ],
385
+ });
386
+ }
387
+
388
+ function runningRetryJob(): Job {
389
+ return makeJob({
390
+ id: 'job-release',
391
+ name: 'release-production',
392
+ status: 'running',
393
+ job_executions: [
394
+ makeExecution({
395
+ id: 'exec-1',
396
+ job_id: 'job-release',
397
+ sequence: 1,
398
+ status: 'failed',
399
+ status_reason: 'step_failed',
400
+ queued_at: '2026-06-26T11:46:00.000Z',
401
+ started_at: '2026-06-26T11:48:00.000Z',
402
+ finished_at: '2026-06-26T11:52:00.000Z',
403
+ steps: [makeStep('package', 'failed', 0, 'exec-1')],
404
+ }),
405
+ makeExecution({
406
+ id: 'exec-2',
407
+ job_id: 'job-release',
408
+ sequence: 2,
409
+ status: 'running',
410
+ queued_at: QUEUED_AT,
411
+ started_at: STARTED_AT,
412
+ steps: [
413
+ makeStep('package', 'succeeded', 0, 'exec-2'),
414
+ makeStep('publish', 'running', 1, 'exec-2', {
415
+ current_attempt: 1,
416
+ attempts: [
417
+ workflowStepAttemptDto({
418
+ id: 'attempt-publish-running',
419
+ step_id: 'step-publish-exec-2',
420
+ status: 'running',
421
+ exit_code: null,
422
+ finished_at: null,
423
+ }),
424
+ ],
425
+ }),
426
+ makeStep('notify', 'pending', 2, 'exec-2', {attempts: []}),
427
+ ],
428
+ }),
429
+ ],
430
+ });
431
+ }
432
+
433
+ function sourceActionJob(): Job {
434
+ return makeJob({
435
+ id: 'job-source-action',
436
+ name: 'build',
437
+ status: 'succeeded',
438
+ job_executions: [
439
+ makeExecution({
440
+ id: 'exec-source-action',
441
+ job_id: 'job-source-action',
442
+ sequence: 1,
443
+ status: 'succeeded',
444
+ queued_at: QUEUED_AT,
445
+ started_at: STARTED_AT,
446
+ finished_at: FINISHED_AT,
447
+ steps: [
448
+ makeStep('checkout', 'succeeded', 0, 'exec-source-action', {
449
+ source_location: {start_line: 12, end_line: 16},
450
+ }),
451
+ makeStep('install', 'succeeded', 1, 'exec-source-action'),
452
+ makeStep('test', 'succeeded', 2, 'exec-source-action'),
453
+ ],
454
+ }),
455
+ ],
456
+ });
457
+ }
458
+
459
+ function pendingNoExecutionJob(): Job {
460
+ return makeJob({
461
+ id: 'job-pending-empty',
462
+ name: 'wait-for-runner',
463
+ status: 'pending',
464
+ job_executions: [],
465
+ });
466
+ }
467
+
468
+ function zeroExecutionJob(): Job {
469
+ return makeJob({
470
+ id: 'job-skipped',
471
+ name: 'deploy-preview',
472
+ status: 'skipped',
473
+ status_reason: 'dependency_not_completed',
474
+ dependencies: ['build'],
475
+ job_executions: [],
476
+ });
477
+ }
478
+
479
+ function cancelledNoExecutionJob(): Job {
480
+ return makeJob({
481
+ id: 'job-cancelled-empty',
482
+ name: 'deploy-production',
483
+ status: 'cancelled',
484
+ status_reason: 'run_cancelled',
485
+ job_executions: [],
486
+ });
487
+ }
488
+
489
+ function finishedWithoutStepsJob(): Job {
490
+ return makeJob({
491
+ id: 'job-empty-steps',
492
+ name: 'noop',
493
+ status: 'succeeded',
494
+ job_executions: [
495
+ makeExecution({
496
+ id: 'exec-empty',
497
+ job_id: 'job-empty-steps',
498
+ sequence: 1,
499
+ status: 'succeeded',
500
+ queued_at: QUEUED_AT,
501
+ started_at: STARTED_AT,
502
+ finished_at: FINISHED_AT,
503
+ steps: [],
504
+ }),
505
+ ],
506
+ });
507
+ }
508
+
509
+ function finishedWithoutExecutionJob(): Job {
510
+ return makeJob({
511
+ id: 'job-missing-execution',
512
+ name: 'publish-artifacts',
513
+ status: 'succeeded',
514
+ job_executions: [],
515
+ });
516
+ }
517
+
518
+ function carriedOverJob(): Job {
519
+ return makeJob({
520
+ id: 'job-test',
521
+ name: 'test',
522
+ status: 'succeeded',
523
+ carried_over: true,
524
+ job_executions: [],
525
+ });
526
+ }
527
+
528
+ function listeningJobPending(): Job {
529
+ return makeJob({
530
+ id: 'job-listening-pending',
531
+ name: 'release-gates',
532
+ mode: 'listening',
533
+ status: 'pending',
534
+ listener_status: 'inactive',
535
+ job_executions: [],
536
+ });
537
+ }
538
+
539
+ function listeningJobNoExecutions(): Job {
540
+ return makeJob({
541
+ id: 'job-listening-empty',
542
+ key: 'deploy-window',
543
+ name: null,
544
+ mode: 'listening',
545
+ status: 'running',
546
+ listener_status: 'listening',
547
+ job_executions: [],
548
+ });
549
+ }
550
+
551
+ function listeningJobResolvedNoExecutions(): Job {
552
+ return makeJob({
553
+ id: 'job-listening-resolved-empty',
554
+ name: 'release-gates',
555
+ mode: 'listening',
556
+ status: 'succeeded',
557
+ listener_status: 'resolved',
558
+ resolution_reason: 'max_executions',
559
+ job_executions: [],
560
+ });
561
+ }
562
+
563
+ function listeningJobOneExecution(): Job {
564
+ return makeJob({
565
+ id: 'job-listening-one',
566
+ name: 'release-gates',
567
+ mode: 'listening',
568
+ status: 'running',
569
+ listener_status: 'listening',
570
+ job_executions: [makeListeningExecution(1, 'running', 'job-listening-one', 'exec-listen-one')],
571
+ });
572
+ }
573
+
574
+ function listeningJobMixedExecutions(): Job {
575
+ return makeJob({
576
+ id: 'job-listening-mixed',
577
+ name: 'release-gates',
578
+ mode: 'listening',
579
+ status: 'running',
580
+ listener_status: 'listening',
581
+ job_executions: [
582
+ ...Array.from({length: 2}, (_, index) =>
583
+ makeListeningExecution(index + 1, 'running', 'job-listening-mixed'),
584
+ ),
585
+ ...Array.from({length: 3}, (_, index) =>
586
+ makeListeningExecution(index + 3, 'succeeded', 'job-listening-mixed'),
587
+ ),
588
+ makeListeningExecution(6, 'failed', 'job-listening-mixed', 'exec-listen', {
589
+ trigger_events: [
590
+ makeTriggerEvent(6, 'deployment_status'),
591
+ makeTriggerEvent(7, 'check_run'),
592
+ ],
593
+ }),
594
+ ],
595
+ });
596
+ }
597
+
598
+ function listeningJobResolved(): Job {
599
+ return makeJob({
600
+ id: 'job-listening-resolved',
601
+ name: 'release-production-gates',
602
+ mode: 'listening',
603
+ status: 'failed',
604
+ listener_status: 'resolved',
605
+ resolution_reason: 'max_executions',
606
+ job_executions: Array.from({length: 4}, (_, index) =>
607
+ makeListeningExecution(
608
+ index + 1,
609
+ index === 3 ? 'failed' : 'succeeded',
610
+ 'job-listening-resolved',
611
+ 'exec-listen-resolved',
612
+ ),
613
+ ),
614
+ });
615
+ }
616
+
617
+ function manyExecutionsListeningJob(name: string, id: string): Job {
618
+ return makeJob({
619
+ id,
620
+ name,
621
+ mode: 'listening',
622
+ status: 'running',
623
+ listener_status: 'listening',
624
+ job_executions: Array.from({length: 64}, (_, index) => {
625
+ const sequence = index + 1;
626
+ const status = manyExecutionStatus(sequence);
627
+ return makeListeningExecution(sequence, status, id, `exec-many-${id}`, {
628
+ name: variedExecutionName(sequence),
629
+ });
630
+ }),
631
+ });
632
+ }
633
+
634
+ function longNameManyExecutionsJob(): Job {
635
+ return makeJob({
636
+ id: 'job-long',
637
+ name: 'release-production-multi-region-with-canary-validation-and-post-deploy-observability',
638
+ status: 'running',
639
+ job_executions: Array.from({length: 6}, (_, index) => {
640
+ const sequence = index + 1;
641
+ const status: WorkflowRunJobExecutionDetailDto['status'] =
642
+ sequence < 6 ? 'failed' : 'running';
643
+ return makeExecution({
644
+ id: `exec-${sequence}`,
645
+ job_id: 'job-long',
646
+ sequence,
647
+ status,
648
+ status_reason: status === 'failed' ? 'step_failed' : null,
649
+ queued_at: `2026-06-26T11:${String(36 + index * 3).padStart(2, '0')}:00.000Z`,
650
+ started_at: `2026-06-26T11:${String(38 + index * 3).padStart(2, '0')}:00.000Z`,
651
+ finished_at:
652
+ status === 'failed'
653
+ ? `2026-06-26T11:${String(40 + index * 3).padStart(2, '0')}:00.000Z`
654
+ : null,
655
+ steps: [
656
+ makeStep(
657
+ 'run-production-canary-smoke-tests-with-cross-region-checkout-payment-and-notification-validation',
658
+ status,
659
+ 0,
660
+ `exec-${sequence}`,
661
+ ),
662
+ ],
663
+ });
664
+ }),
665
+ });
666
+ }
667
+
668
+ function longNameListeningJob(): Job {
669
+ return makeJob({
670
+ id: 'job-long-listening',
671
+ name: 'release-production-multi-region-with-canary-validation-and-post-deploy-observability',
672
+ mode: 'listening',
673
+ status: 'running',
674
+ listener_status: 'listening',
675
+ job_executions: Array.from({length: 3}, (_, index) =>
676
+ makeListeningExecution(
677
+ index + 1,
678
+ index === 2 ? 'running' : 'succeeded',
679
+ 'job-long-listening',
680
+ 'exec-long-listening',
681
+ ),
682
+ ),
683
+ });
684
+ }
685
+
686
+ function makeListeningExecution(
687
+ sequence: number,
688
+ status: WorkflowRunJobExecutionDetailDto['status'],
689
+ jobId: string,
690
+ idPrefix = 'exec-listen',
691
+ overrides: Partial<WorkflowRunJobExecutionDetailDto> = {},
692
+ ): WorkflowRunJobExecutionDetailDto {
693
+ return makeExecution({
694
+ id: `${idPrefix}-${sequence}`,
695
+ job_id: jobId,
696
+ sequence,
697
+ name: `event-${sequence}`,
698
+ status,
699
+ status_reason: status === 'failed' ? 'step_failed' : null,
700
+ queued_at: listeningTimestamp(sequence, 0),
701
+ started_at: listeningTimestamp(sequence, 2),
702
+ finished_at: status === 'running' ? null : listeningTimestamp(sequence, 3),
703
+ trigger_events: [makeTriggerEvent(sequence, variedExecutionName(sequence))],
704
+ steps: [makeStep('handle-event', status, 0, `${idPrefix}-${sequence}`)],
705
+ ...overrides,
706
+ });
707
+ }
708
+
709
+ function makeTriggerEvent(
710
+ sequence: number,
711
+ event = `event-${sequence}`,
712
+ ): WorkflowExecutionEventDto {
713
+ return {
714
+ source: 'github',
715
+ event,
716
+ delivery_id: `delivery-${sequence}`,
717
+ received_at: listeningTimestamp(sequence, 1),
718
+ data: {sequence},
719
+ };
720
+ }
721
+
722
+ function manyExecutionStatus(sequence: number): WorkflowRunJobExecutionDetailDto['status'] {
723
+ if (sequence === 64) return 'running';
724
+ if (sequence % 11 === 0) return 'failed';
725
+ if (sequence % 7 === 0) return 'cancelled';
726
+ return 'succeeded';
727
+ }
728
+
729
+ function variedExecutionName(sequence: number): string {
730
+ const names = [
731
+ 'push',
732
+ 'gate-opened',
733
+ 'deployment-status-received',
734
+ 'manual-approval-received-from-release-coordinator',
735
+ 'production-canary-validation-completed-for-eu-west-and-us-east',
736
+ ];
737
+
738
+ return names[(sequence - 1) % names.length] ?? 'event';
739
+ }
740
+
741
+ function listeningTimestamp(sequence: number, offsetMinutes: number): string {
742
+ return new Date(LISTENING_BASE_AT + (sequence + offsetMinutes) * 60_000).toISOString();
743
+ }
744
+
745
+ function makeJob(overrides: Partial<WorkflowRunJobDetailDto>): Job {
746
+ return workflowJob(overrides);
747
+ }
748
+
749
+ function makeExecution(
750
+ overrides: Partial<WorkflowRunJobExecutionDetailDto> & {
751
+ id: string;
752
+ job_id: string;
753
+ sequence: number;
754
+ },
755
+ ): WorkflowRunJobExecutionDetailDto {
756
+ return workflowJobExecutionDto(overrides);
757
+ }
758
+
759
+ function makeStep(
760
+ name: string,
761
+ status: string,
762
+ position: number,
763
+ jobExecutionId: string,
764
+ overrides: Partial<WorkflowRunStepDetailDto> = {},
765
+ ): WorkflowRunStepDetailDto {
766
+ const stepId = `step-${name}-${jobExecutionId}`;
767
+ return workflowStepDto({
768
+ id: stepId,
769
+ job_execution_id: jobExecutionId,
770
+ name,
771
+ position,
772
+ status,
773
+ attempts:
774
+ overrides.attempts ??
775
+ (status === 'pending'
776
+ ? []
777
+ : [
778
+ workflowStepAttemptDto({
779
+ id: `attempt-${name}-${jobExecutionId}`,
780
+ step_id: stepId,
781
+ status,
782
+ exit_code: status === 'failed' ? 1 : null,
783
+ finished_at: status === 'running' ? null : FINISHED_AT,
784
+ }),
785
+ ]),
786
+ ...overrides,
787
+ });
788
+ }