@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,517 @@
1
+ import type {
2
+ WorkflowRunDetailResponseDto,
3
+ WorkflowRunResponseDto,
4
+ } from '@shipfox/api-workflows-dto';
5
+ import {configureApiClient} from '@shipfox/client-api';
6
+ import {act, screen, waitFor} from '@testing-library/react';
7
+ import userEvent from '@testing-library/user-event';
8
+ import {inlineLogBody, outputLine} from '#test/fixtures/logs.js';
9
+ import {
10
+ workflowJobDto,
11
+ workflowJobExecutionDto,
12
+ workflowRunDetailDto,
13
+ workflowRunDto,
14
+ workflowStepAttemptDto,
15
+ workflowStepDto,
16
+ } from '#test/fixtures/workflow-run.js';
17
+ import {jsonResponse, PROJECT_TEST_WID, renderProjectPage} from '#test/pages.js';
18
+ import {WorkflowRunPage} from './workflow-run-page.js';
19
+
20
+ const PROJECT_ID = '44444444-4444-4444-8444-444444444444';
21
+ const DEFINITION_ID = '55555555-5555-4555-8555-555555555555';
22
+ const RUN_ID = '66666666-6666-4666-8666-666666666666';
23
+ const SECOND_RUN_ID = '66666666-6666-4666-8666-000000000002';
24
+ const BUILD_JOB_ID = '77777777-7777-4777-8777-777777777777';
25
+ const BUILD_STEP_ID = '99999999-9999-4999-8999-000000000000';
26
+ const BUILD_ATTEMPT_ID = 'aaaaaaaa-aaaa-4aaa-8aaa-000000000000';
27
+ const DEPLOY_JOB_ID = '88888888-8888-4888-8888-888888888888';
28
+ const DEPLOY_STEP_ID = '99999999-9999-4999-8999-999999999999';
29
+ const DEPLOY_RETRY_STEP_ID = '99999999-9999-4999-8999-000000000003';
30
+ const DEPLOY_ATTEMPT_ONE_ID = 'aaaaaaaa-aaaa-4aaa-8aaa-000000000001';
31
+ const DEPLOY_ATTEMPT_TWO_ID = 'aaaaaaaa-aaaa-4aaa-8aaa-000000000002';
32
+ const DEPLOY_EXECUTION_ONE_ID = 'bbbbbbbb-bbbb-4bbb-8bbb-000000000001';
33
+ const DEPLOY_EXECUTION_TWO_ID = 'bbbbbbbb-bbbb-4bbb-8bbb-000000000002';
34
+ const SMOKE_WEB_RE = /smoke-web/u;
35
+ const EXECUTION_ONE_MENU_ITEM_PATTERN = /#1/;
36
+ const RUN_DETAIL_PATH_RE = /^\/workflows\/runs\/([^/]+)$/u;
37
+ const RUN_OVERRIDES = {
38
+ id: RUN_ID,
39
+ project_id: PROJECT_ID,
40
+ definition_id: DEFINITION_ID,
41
+ trigger_payload: {source: 'manual', event: 'fire'},
42
+ created_at: '2026-05-07T01:01:00.000Z',
43
+ updated_at: '2026-05-07T01:02:00.000Z',
44
+ } satisfies Partial<WorkflowRunResponseDto>;
45
+ const SECOND_RUN_OVERRIDES = {
46
+ ...RUN_OVERRIDES,
47
+ id: SECOND_RUN_ID,
48
+ name: 'smoke-web',
49
+ } satisfies Partial<WorkflowRunResponseDto>;
50
+
51
+ describe('WorkflowRunPage', () => {
52
+ test('keeps the runs list mounted and only skeletons the run view until a run is selected', async () => {
53
+ configureApiClient({fetchImpl: vi.fn(() => new Promise<Response>(() => undefined))});
54
+
55
+ renderRunsPath();
56
+
57
+ // The run view has nothing to show until a run is selected, so it skeletons...
58
+ expect(await screen.findByLabelText('Loading workflow run')).toBeInTheDocument();
59
+ // ...but the runs list itself stays mounted; it is never replaced by a page skeleton.
60
+ expect(screen.getByLabelText('Workflow runs')).toBeInTheDocument();
61
+ });
62
+
63
+ test('redirects to the most recent run when opened without a workflow run id', async () => {
64
+ configureApiClient({fetchImpl: createRunsListFetch()});
65
+
66
+ const {router} = renderRunsPath(
67
+ `?job=${DEPLOY_JOB_ID}&step=${DEPLOY_STEP_ID}&stepAttempt=${DEPLOY_ATTEMPT_TWO_ID}&runAttempt=1`,
68
+ );
69
+
70
+ // Landing on /runs with runs present redirects to the newest run, so its row becomes the
71
+ // selected (current) row in the rail even though the opened URL carried no workflow run id.
72
+ const selectedRow = await screen.findByRole('link', {current: 'page'});
73
+ expect(selectedRow).toHaveTextContent('deploy-web');
74
+ expect(currentSearch(router).job).toBeUndefined();
75
+ expect(currentSearch(router).step).toBeUndefined();
76
+ expect(currentSearch(router).stepAttempt).toBeUndefined();
77
+ expect(currentSearch(router).runAttempt).toBeUndefined();
78
+ });
79
+
80
+ test('shows the first-time-use surface when the project has no runs', async () => {
81
+ configureApiClient({fetchImpl: createEmptyRunsFetch()});
82
+
83
+ renderRunsPath();
84
+
85
+ expect(await screen.findByText('No workflow runs yet')).toBeInTheDocument();
86
+ // The rail and the perpetual detail skeleton give way to the onboarding surface entirely.
87
+ expect(screen.queryByLabelText('Workflow runs')).not.toBeInTheDocument();
88
+ expect(screen.queryByLabelText('Loading workflow run')).not.toBeInTheDocument();
89
+ });
90
+
91
+ test('restores a deep-linked job and exact attempt after data loads', async () => {
92
+ configureApiClient({fetchImpl: createRunDetailFetch()});
93
+
94
+ renderRunPath(
95
+ `?job=${BUILD_JOB_ID}&step=${DEPLOY_STEP_ID}&stepAttempt=${DEPLOY_ATTEMPT_TWO_ID}`,
96
+ );
97
+
98
+ const deployJob = await screen.findByRole('button', {name: 'deploy, Running'});
99
+ const deployAttempt = await screen.findByRole('button', {
100
+ name: 'deploy, Running, attempt 2',
101
+ });
102
+
103
+ expect(deployJob).toHaveAttribute('aria-pressed', 'true');
104
+ expect(deployAttempt).toHaveAttribute('aria-expanded', 'true');
105
+ expect(await screen.findByText('attempt two log')).toBeInTheDocument();
106
+ });
107
+
108
+ test('selecting a job writes job search state and clears stale step state', async () => {
109
+ const user = userEvent.setup();
110
+ configureApiClient({fetchImpl: createRunDetailFetch()});
111
+ const {router} = renderRunPath(
112
+ `?step=${DEPLOY_STEP_ID}&stepAttempt=${DEPLOY_ATTEMPT_TWO_ID}&runAttempt=1`,
113
+ );
114
+
115
+ await user.click(await screen.findByRole('button', {name: 'build, Succeeded'}));
116
+
117
+ await waitFor(() => {
118
+ expect(currentSearch(router)).toMatchObject({job: BUILD_JOB_ID});
119
+ });
120
+ expect(currentSearch(router).step).toBeUndefined();
121
+ expect(currentSearch(router).stepAttempt).toBeUndefined();
122
+ expect(screen.getByRole('button', {name: 'checkout, Succeeded, attempt 1'})).toHaveAttribute(
123
+ 'aria-expanded',
124
+ 'false',
125
+ );
126
+ });
127
+
128
+ test('selecting an attempt writes job, step, and attempt search state', async () => {
129
+ const user = userEvent.setup();
130
+ configureApiClient({fetchImpl: createRunDetailFetch()});
131
+ const {router} = renderRunPath(`?job=${DEPLOY_JOB_ID}`);
132
+
133
+ await user.click(await screen.findByRole('button', {name: 'deploy, Running, attempt 2'}));
134
+
135
+ await waitFor(() => {
136
+ expect(currentSearch(router)).toMatchObject({
137
+ job: DEPLOY_JOB_ID,
138
+ step: DEPLOY_STEP_ID,
139
+ stepAttempt: DEPLOY_ATTEMPT_TWO_ID,
140
+ });
141
+ });
142
+ });
143
+
144
+ test('selecting a listening execution writes job execution search state and scopes step selection', async () => {
145
+ const user = userEvent.setup();
146
+ configureApiClient({
147
+ fetchImpl: createRunDetailFetch({details: {[RUN_ID]: retryRunDetailDto()}}),
148
+ });
149
+ const {router} = renderRunPath(`?job=${DEPLOY_JOB_ID}`);
150
+
151
+ await user.click(
152
+ await screen.findByRole('button', {
153
+ name: 'Switch job execution, currently execution 2',
154
+ }),
155
+ );
156
+ await user.click(screen.getByRole('menuitem', {name: EXECUTION_ONE_MENU_ITEM_PATTERN}));
157
+
158
+ await waitFor(() => {
159
+ expect(currentSearch(router)).toMatchObject({
160
+ job: DEPLOY_JOB_ID,
161
+ jobExecution: DEPLOY_EXECUTION_ONE_ID,
162
+ });
163
+ });
164
+ expect(screen.getByRole('button', {name: 'deploy, Failed, attempt 1'})).toBeInTheDocument();
165
+
166
+ await user.click(screen.getByRole('button', {name: 'deploy, Failed, attempt 1'}));
167
+
168
+ await waitFor(() => {
169
+ expect(currentSearch(router)).toMatchObject({
170
+ job: DEPLOY_JOB_ID,
171
+ jobExecution: DEPLOY_EXECUTION_ONE_ID,
172
+ step: DEPLOY_STEP_ID,
173
+ stepAttempt: DEPLOY_ATTEMPT_ONE_ID,
174
+ });
175
+ });
176
+ expect(await screen.findByText('attempt one log')).toBeInTheDocument();
177
+ });
178
+
179
+ test('collapsing an attempt removes step and attempt while preserving job', async () => {
180
+ const user = userEvent.setup();
181
+ configureApiClient({fetchImpl: createRunDetailFetch()});
182
+ const {router} = renderRunPath(`?step=${DEPLOY_STEP_ID}&stepAttempt=${DEPLOY_ATTEMPT_TWO_ID}`);
183
+
184
+ const deployAttempt = await screen.findByRole('button', {
185
+ name: 'deploy, Running, attempt 2',
186
+ });
187
+ await user.click(deployAttempt);
188
+
189
+ await waitFor(() => {
190
+ expect(currentSearch(router)).toMatchObject({job: DEPLOY_JOB_ID});
191
+ });
192
+ expect(currentSearch(router).step).toBeUndefined();
193
+ expect(currentSearch(router).stepAttempt).toBeUndefined();
194
+ expect(deployAttempt).toHaveAttribute('aria-expanded', 'false');
195
+ });
196
+
197
+ test('back and forward navigation restores prior selections', async () => {
198
+ const user = userEvent.setup();
199
+ configureApiClient({fetchImpl: createRunDetailFetch()});
200
+ const {router} = renderRunPath(`?job=${DEPLOY_JOB_ID}`);
201
+
202
+ await user.click(await screen.findByRole('button', {name: 'deploy, Running, attempt 2'}));
203
+ await waitFor(() => {
204
+ expect(currentSearch(router).stepAttempt).toBe(DEPLOY_ATTEMPT_TWO_ID);
205
+ });
206
+
207
+ await act(() => {
208
+ router.history.back();
209
+ });
210
+ await waitFor(() => {
211
+ expect(currentSearch(router)).toMatchObject({job: DEPLOY_JOB_ID});
212
+ });
213
+ expect(currentSearch(router).step).toBeUndefined();
214
+ expect(currentSearch(router).stepAttempt).toBeUndefined();
215
+ expect(screen.getByRole('button', {name: 'deploy, Running, attempt 2'})).toHaveAttribute(
216
+ 'aria-expanded',
217
+ 'false',
218
+ );
219
+
220
+ await act(() => {
221
+ router.history.forward();
222
+ });
223
+ await waitFor(() => {
224
+ expect(currentSearch(router).stepAttempt).toBe(DEPLOY_ATTEMPT_TWO_ID);
225
+ });
226
+ expect(screen.getByRole('button', {name: 'deploy, Running, attempt 2'})).toHaveAttribute(
227
+ 'aria-expanded',
228
+ 'true',
229
+ );
230
+ });
231
+
232
+ test('run rail links clear job, step, and attempt when switching runs', async () => {
233
+ const user = userEvent.setup();
234
+ configureApiClient({
235
+ fetchImpl: createRunDetailFetch({
236
+ runs: [workflowRunDto(RUN_OVERRIDES), workflowRunDto(SECOND_RUN_OVERRIDES)],
237
+ details: {
238
+ [RUN_ID]: defaultRunDetailDto(),
239
+ [SECOND_RUN_ID]: workflowRunDetailDto({...SECOND_RUN_OVERRIDES, jobs: []}),
240
+ },
241
+ }),
242
+ });
243
+ const {router} = renderRunPath(`?step=${DEPLOY_STEP_ID}&stepAttempt=${DEPLOY_ATTEMPT_TWO_ID}`);
244
+
245
+ await user.click(await screen.findByRole('link', {name: SMOKE_WEB_RE}));
246
+
247
+ await waitFor(() => {
248
+ expect(router.state.location.pathname).toContain(SECOND_RUN_ID);
249
+ });
250
+ expect(currentSearch(router).job).toBeUndefined();
251
+ expect(currentSearch(router).step).toBeUndefined();
252
+ expect(currentSearch(router).stepAttempt).toBeUndefined();
253
+ expect(currentSearch(router).runAttempt).toBeUndefined();
254
+ });
255
+ });
256
+
257
+ function renderRunsPath(search = '') {
258
+ return renderProjectPage(
259
+ `/workspaces/${PROJECT_TEST_WID}/projects/${PROJECT_ID}/runs${search}`,
260
+ ({workflowRunId}) => (
261
+ <WorkflowRunPage
262
+ workspaceId={PROJECT_TEST_WID}
263
+ projectId={PROJECT_ID}
264
+ workflowRunId={workflowRunId}
265
+ />
266
+ ),
267
+ );
268
+ }
269
+
270
+ function renderRunPath(search = '') {
271
+ return renderProjectPage(
272
+ `/workspaces/${PROJECT_TEST_WID}/projects/${PROJECT_ID}/runs/${RUN_ID}${search}`,
273
+ ({workflowRunId}) => (
274
+ <WorkflowRunPage
275
+ workspaceId={PROJECT_TEST_WID}
276
+ projectId={PROJECT_ID}
277
+ workflowRunId={workflowRunId}
278
+ />
279
+ ),
280
+ );
281
+ }
282
+
283
+ function createRunsListFetch() {
284
+ return vi.fn((input: RequestInfo | URL) => {
285
+ const url = new URL(requestInputUrl(input));
286
+
287
+ if (url.pathname === '/workflows/runs') {
288
+ return Promise.resolve(
289
+ jsonResponse({
290
+ runs: [workflowRunDto(RUN_OVERRIDES)],
291
+ next_cursor: null,
292
+ filtered_total_count: 1,
293
+ }),
294
+ );
295
+ }
296
+ if (url.pathname === `/workflows/runs/${RUN_ID}`) {
297
+ return Promise.resolve(jsonResponse(workflowRunDetailDto({...RUN_OVERRIDES, jobs: []})));
298
+ }
299
+
300
+ return Promise.resolve(jsonResponse({code: 'not-found'}, {status: 404}));
301
+ });
302
+ }
303
+
304
+ function createRunDetailFetch({
305
+ runs = [workflowRunDto(RUN_OVERRIDES)],
306
+ details = {[RUN_ID]: defaultRunDetailDto()},
307
+ }: {
308
+ runs?: WorkflowRunResponseDto[];
309
+ details?: Record<string, WorkflowRunDetailResponseDto>;
310
+ } = {}) {
311
+ return vi.fn((input: RequestInfo | URL) => {
312
+ const url = new URL(requestInputUrl(input));
313
+
314
+ if (url.pathname === '/workflows/runs') {
315
+ return Promise.resolve(
316
+ jsonResponse({runs, next_cursor: null, filtered_total_count: runs.length}),
317
+ );
318
+ }
319
+
320
+ const runMatch = url.pathname.match(RUN_DETAIL_PATH_RE);
321
+ if (runMatch?.[1] && details[runMatch[1]]) {
322
+ return Promise.resolve(jsonResponse(details[runMatch[1]]));
323
+ }
324
+
325
+ if (url.pathname === `/steps/${DEPLOY_STEP_ID}/attempts/1/logs`) {
326
+ return Promise.resolve(jsonResponse(inlineLogBody(outputLine('attempt one log\n'), 1)));
327
+ }
328
+ if (url.pathname === `/steps/${DEPLOY_STEP_ID}/attempts/2/logs`) {
329
+ return Promise.resolve(jsonResponse(inlineLogBody(outputLine('attempt two log\n'), 1)));
330
+ }
331
+ if (url.pathname === `/steps/${DEPLOY_RETRY_STEP_ID}/attempts/1/logs`) {
332
+ return Promise.resolve(jsonResponse(inlineLogBody(outputLine('retry attempt log\n'), 1)));
333
+ }
334
+ if (url.pathname === `/steps/${BUILD_STEP_ID}/attempts/1/logs`) {
335
+ return Promise.resolve(jsonResponse(inlineLogBody(outputLine('build log\n'), 1)));
336
+ }
337
+
338
+ return Promise.resolve(jsonResponse({code: 'not-found'}, {status: 404}));
339
+ });
340
+ }
341
+
342
+ function createEmptyRunsFetch() {
343
+ return vi.fn((input: RequestInfo | URL) => {
344
+ const url = new URL(requestInputUrl(input));
345
+
346
+ if (url.pathname === '/workflows/runs') {
347
+ return Promise.resolve(jsonResponse({runs: [], next_cursor: null, filtered_total_count: 0}));
348
+ }
349
+
350
+ return Promise.resolve(jsonResponse({code: 'not-found'}, {status: 404}));
351
+ });
352
+ }
353
+
354
+ function requestInputUrl(input: RequestInfo | URL) {
355
+ if (input instanceof Request) return input.url;
356
+ return String(input);
357
+ }
358
+
359
+ function defaultRunDetailDto(
360
+ overrides: Partial<WorkflowRunDetailResponseDto> = {},
361
+ ): WorkflowRunDetailResponseDto {
362
+ return workflowRunDetailDto({
363
+ ...RUN_OVERRIDES,
364
+ jobs: [
365
+ workflowJobDto({
366
+ id: BUILD_JOB_ID,
367
+ run_attempt_id: RUN_ID,
368
+ name: 'build',
369
+ status: 'succeeded',
370
+ steps: [
371
+ workflowStepDto({
372
+ id: BUILD_STEP_ID,
373
+ key: 'checkout',
374
+ name: 'checkout',
375
+ status: 'succeeded',
376
+ current_attempt: 1,
377
+ attempts: [
378
+ workflowStepAttemptDto({
379
+ id: BUILD_ATTEMPT_ID,
380
+ step_id: BUILD_STEP_ID,
381
+ status: 'succeeded',
382
+ }),
383
+ ],
384
+ }),
385
+ ],
386
+ }),
387
+ workflowJobDto({
388
+ id: DEPLOY_JOB_ID,
389
+ run_attempt_id: RUN_ID,
390
+ name: 'deploy',
391
+ status: 'running',
392
+ position: 1,
393
+ dependencies: ['build'],
394
+ steps: [
395
+ workflowStepDto({
396
+ id: DEPLOY_STEP_ID,
397
+ key: 'deploy',
398
+ name: 'deploy',
399
+ status: 'running',
400
+ current_attempt: 2,
401
+ attempts: [
402
+ workflowStepAttemptDto({
403
+ id: DEPLOY_ATTEMPT_ONE_ID,
404
+ step_id: DEPLOY_STEP_ID,
405
+ attempt: 1,
406
+ execution_order: 1,
407
+ status: 'failed',
408
+ exit_code: 1,
409
+ }),
410
+ workflowStepAttemptDto({
411
+ id: DEPLOY_ATTEMPT_TWO_ID,
412
+ step_id: DEPLOY_STEP_ID,
413
+ attempt: 2,
414
+ execution_order: 2,
415
+ status: 'running',
416
+ exit_code: null,
417
+ finished_at: null,
418
+ }),
419
+ ],
420
+ }),
421
+ ],
422
+ }),
423
+ ],
424
+ ...overrides,
425
+ });
426
+ }
427
+
428
+ function retryRunDetailDto(): WorkflowRunDetailResponseDto {
429
+ return defaultRunDetailDto({
430
+ jobs: [
431
+ workflowJobDto({
432
+ id: BUILD_JOB_ID,
433
+ run_attempt_id: RUN_ID,
434
+ name: 'build',
435
+ status: 'succeeded',
436
+ steps: [
437
+ workflowStepDto({
438
+ id: BUILD_STEP_ID,
439
+ name: 'checkout',
440
+ status: 'succeeded',
441
+ attempts: [
442
+ workflowStepAttemptDto({
443
+ id: BUILD_ATTEMPT_ID,
444
+ step_id: BUILD_STEP_ID,
445
+ status: 'succeeded',
446
+ }),
447
+ ],
448
+ }),
449
+ ],
450
+ }),
451
+ workflowJobDto({
452
+ id: DEPLOY_JOB_ID,
453
+ run_attempt_id: RUN_ID,
454
+ name: 'deploy',
455
+ mode: 'listening',
456
+ status: 'running',
457
+ listener_status: 'listening',
458
+ position: 1,
459
+ dependencies: ['build'],
460
+ job_executions: [
461
+ workflowJobExecutionDto({
462
+ id: DEPLOY_EXECUTION_ONE_ID,
463
+ job_id: DEPLOY_JOB_ID,
464
+ sequence: 1,
465
+ name: 'deploy',
466
+ status: 'failed',
467
+ started_at: '2026-05-07T01:01:00.000Z',
468
+ finished_at: '2026-05-07T01:02:00.000Z',
469
+ steps: [
470
+ workflowStepDto({
471
+ id: DEPLOY_STEP_ID,
472
+ name: 'deploy',
473
+ status: 'failed',
474
+ attempts: [
475
+ workflowStepAttemptDto({
476
+ id: DEPLOY_ATTEMPT_ONE_ID,
477
+ step_id: DEPLOY_STEP_ID,
478
+ status: 'failed',
479
+ exit_code: 1,
480
+ }),
481
+ ],
482
+ }),
483
+ ],
484
+ }),
485
+ workflowJobExecutionDto({
486
+ id: DEPLOY_EXECUTION_TWO_ID,
487
+ job_id: DEPLOY_JOB_ID,
488
+ sequence: 2,
489
+ name: 'deploy',
490
+ status: 'running',
491
+ started_at: '2026-05-07T01:03:00.000Z',
492
+ steps: [
493
+ workflowStepDto({
494
+ id: DEPLOY_RETRY_STEP_ID,
495
+ name: 'deploy retry',
496
+ status: 'running',
497
+ attempts: [
498
+ workflowStepAttemptDto({
499
+ id: DEPLOY_ATTEMPT_TWO_ID,
500
+ step_id: DEPLOY_RETRY_STEP_ID,
501
+ status: 'running',
502
+ exit_code: null,
503
+ finished_at: null,
504
+ }),
505
+ ],
506
+ }),
507
+ ],
508
+ }),
509
+ ],
510
+ }),
511
+ ],
512
+ });
513
+ }
514
+
515
+ function currentSearch({state}: ReturnType<typeof renderRunPath>['router']) {
516
+ return state.location.search as Record<string, unknown>;
517
+ }
@@ -0,0 +1,89 @@
1
+ import {useNavigate, useSearch} from '@tanstack/react-router';
2
+ import {useCallback, useEffect} from 'react';
3
+ import {WorkflowRunList} from '#components/workflow-run-list/workflow-run-list.js';
4
+ import {WorkflowRunView} from '#components/workflow-run-view/index.js';
5
+ import {
6
+ type WorkflowRunSelectionInput,
7
+ withoutWorkflowRunSelectionSearch,
8
+ withWorkflowRunSelectionSearch,
9
+ workflowRunSelectionFromSearch,
10
+ } from '#core/workflow-run-url-state.js';
11
+ import {useWorkflowRunsInfiniteQuery} from '#hooks/api/workflow-runs.js';
12
+ import {WorkflowRunFirstTimeUse} from './workflow-run-first-time-use.js';
13
+
14
+ interface WorkflowRunPageProps {
15
+ workspaceId: string;
16
+ projectId: string;
17
+ workflowRunId?: string | undefined;
18
+ }
19
+
20
+ /**
21
+ * Resolve which surface the runs path should show from a single read of the runs list:
22
+ * - when opened without a workflow run id, point the URL at the most recent run so the detail pane is
23
+ * never empty (navigation happens in an effect since it mutates history);
24
+ * - report `hasNoRuns` once the list has loaded with zero runs, so a brand-new project lands
25
+ * on the first-time-use surface instead of an empty rail and a perpetual detail skeleton.
26
+ */
27
+ function useWorkflowRunPageTarget(
28
+ workspaceId: string,
29
+ projectId: string,
30
+ workflowRunId: string | undefined,
31
+ ) {
32
+ const navigate = useNavigate();
33
+ const {data, isPending} = useWorkflowRunsInfiniteQuery(projectId, {});
34
+ const firstWorkflowRunId = data?.pages[0]?.runs[0]?.id;
35
+ // Gate on data presence, not `!isError`: a transient refetch error after a prior success
36
+ // keeps `data`, so the redirect (and the no-runs surface) still resolve from it instead of
37
+ // stalling on the rail while active-run polling hits a blip.
38
+ const isLoaded = !isPending && data !== undefined;
39
+
40
+ useEffect(() => {
41
+ if (workflowRunId || !isLoaded || !firstWorkflowRunId) return;
42
+ navigate({
43
+ to: '/workspaces/$wid/projects/$pid/runs/$workflowRunId',
44
+ params: {wid: workspaceId, pid: projectId, workflowRunId: firstWorkflowRunId},
45
+ search: ((previous: Record<string, unknown>) =>
46
+ withoutWorkflowRunSelectionSearch(previous)) as never,
47
+ replace: true,
48
+ });
49
+ }, [navigate, workspaceId, projectId, workflowRunId, isLoaded, firstWorkflowRunId]);
50
+
51
+ return {hasNoRuns: isLoaded && firstWorkflowRunId === undefined};
52
+ }
53
+
54
+ export function WorkflowRunPage({workspaceId, projectId, workflowRunId}: WorkflowRunPageProps) {
55
+ const {hasNoRuns} = useWorkflowRunPageTarget(workspaceId, projectId, workflowRunId);
56
+ const navigate = useNavigate();
57
+ const search = useSearch({strict: false}) as Record<string, unknown>;
58
+ const selection = workflowRunSelectionFromSearch(search);
59
+ const onSelectionChange = useCallback(
60
+ (nextSelection: WorkflowRunSelectionInput) => {
61
+ navigate({
62
+ search: ((previous: Record<string, unknown>) =>
63
+ withWorkflowRunSelectionSearch(previous, nextSelection)) as never,
64
+ });
65
+ },
66
+ [navigate],
67
+ );
68
+
69
+ if (!workflowRunId && hasNoRuns) {
70
+ return <WorkflowRunFirstTimeUse />;
71
+ }
72
+
73
+ return (
74
+ <div className="flex min-h-0 flex-1 overflow-hidden">
75
+ <WorkflowRunList
76
+ workspaceId={workspaceId}
77
+ projectId={projectId}
78
+ selectedWorkflowRunId={workflowRunId}
79
+ />
80
+ <WorkflowRunView
81
+ workspaceId={workspaceId}
82
+ projectId={projectId}
83
+ workflowRunId={workflowRunId}
84
+ selection={selection}
85
+ onSelectionChange={onSelectionChange}
86
+ />
87
+ </div>
88
+ );
89
+ }
@@ -0,0 +1,15 @@
1
+ import {defineRoute} from '@shipfox/client-shell/runtime';
2
+ import {useParams} from '@tanstack/react-router';
3
+ import {WorkflowRunPage} from '#pages/workflow-run-page.js';
4
+
5
+ export default defineRoute({
6
+ staticData: {layout: 'full-bleed'},
7
+ component: () => {
8
+ const {wid, pid, workflowRunId} = useParams({strict: false}) as {
9
+ wid: string;
10
+ pid: string;
11
+ workflowRunId: string;
12
+ };
13
+ return <WorkflowRunPage workspaceId={wid} projectId={pid} workflowRunId={workflowRunId} />;
14
+ },
15
+ });
@@ -0,0 +1,11 @@
1
+ import {defineRoute} from '@shipfox/client-shell/runtime';
2
+ import {useParams} from '@tanstack/react-router';
3
+ import {WorkflowRunPage} from '#pages/workflow-run-page.js';
4
+
5
+ export default defineRoute({
6
+ staticData: {layout: 'full-bleed'},
7
+ component: () => {
8
+ const {wid, pid} = useParams({strict: false}) as {wid: string; pid: string};
9
+ return <WorkflowRunPage workspaceId={wid} projectId={pid} />;
10
+ },
11
+ });
@@ -0,0 +1,13 @@
1
+ export const outputLine = (data: string, ts = 1): string =>
2
+ `${JSON.stringify({v: 1, ts, type: 'output', stream: 'stdout', data})}\n`;
3
+
4
+ export function inlineLogBody(ndjson: string, nextCursor: number) {
5
+ return {
6
+ mode: 'inline' as const,
7
+ ndjson,
8
+ next_cursor: nextCursor,
9
+ has_more: false,
10
+ state: 'closed' as const,
11
+ truncated: false,
12
+ };
13
+ }