@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,22 @@
1
+ import type {QueryLoadErrorQuery} from '@shipfox/client-ui';
2
+ import type {WorkflowRunListItem} from '#core/workflow-run.js';
3
+
4
+ export type WorkflowRunListStatusFilter = 'all' | 'failed' | 'running';
5
+
6
+ export interface WorkflowRunListProps {
7
+ workspaceId: string;
8
+ projectId: string;
9
+ selectedWorkflowRunId?: string | undefined;
10
+ className?: string | undefined;
11
+ }
12
+
13
+ export type WorkflowRunListQuery = QueryLoadErrorQuery & {isPending: boolean};
14
+
15
+ export interface WorkflowRunListViewProps {
16
+ runs: WorkflowRunListItem[];
17
+ query: WorkflowRunListQuery;
18
+ workspaceId: string;
19
+ projectId: string;
20
+ selectedWorkflowRunId?: string | undefined;
21
+ className?: string | undefined;
22
+ }
@@ -0,0 +1,58 @@
1
+ import {QueryLoadError} from '@shipfox/client-ui';
2
+ import type {WorkflowRunListItem} from '#core/workflow-run.js';
3
+ import type {WorkflowRunListQuery} from './types.js';
4
+ import {
5
+ WorkflowRunListEmpty,
6
+ WorkflowRunListNoMatches,
7
+ WorkflowRunListSkeleton,
8
+ WorkflowRunListStaleError,
9
+ } from './workflow-run-list-states.js';
10
+ import {WorkflowRunRowList} from './workflow-run-row.js';
11
+
12
+ interface WorkflowRunListContentProps {
13
+ query: WorkflowRunListQuery;
14
+ totalRuns: number;
15
+ runs: WorkflowRunListItem[];
16
+ workspaceId: string;
17
+ projectId: string;
18
+ selectedWorkflowRunId?: string | undefined;
19
+ onClearFilters: () => void;
20
+ }
21
+
22
+ export function WorkflowRunListContent({
23
+ query,
24
+ totalRuns,
25
+ runs,
26
+ workspaceId,
27
+ projectId,
28
+ selectedWorkflowRunId,
29
+ onClearFilters,
30
+ }: WorkflowRunListContentProps) {
31
+ const {isPending, isError} = query;
32
+ // A refetch that fails after a prior success keeps the rows on screen behind a slim
33
+ // banner. QueryLoadError owns the inverse case (errored before anything loaded) and
34
+ // self-gates to nothing here once data exists.
35
+ const refreshFailed = isError && query.data !== undefined;
36
+
37
+ return (
38
+ <div className="min-h-0 flex-1 overflow-y-auto">
39
+ {isPending ? <WorkflowRunListSkeleton /> : null}
40
+ {!isPending ? (
41
+ <QueryLoadError query={query} subject="workflow runs" icon="pulseLine" />
42
+ ) : null}
43
+ {!isPending && refreshFailed ? <WorkflowRunListStaleError query={query} /> : null}
44
+ {!isPending && !isError && totalRuns === 0 ? <WorkflowRunListEmpty /> : null}
45
+ {!isPending && totalRuns > 0 && runs.length === 0 ? (
46
+ <WorkflowRunListNoMatches onClear={onClearFilters} />
47
+ ) : null}
48
+ {!isPending && runs.length > 0 ? (
49
+ <WorkflowRunRowList
50
+ runs={runs}
51
+ workspaceId={workspaceId}
52
+ projectId={projectId}
53
+ selectedWorkflowRunId={selectedWorkflowRunId}
54
+ />
55
+ ) : null}
56
+ </div>
57
+ );
58
+ }
@@ -0,0 +1,53 @@
1
+ import {Button} from '@shipfox/react-ui/button';
2
+ import {Icon} from '@shipfox/react-ui/icon';
3
+ import {Input} from '@shipfox/react-ui/input';
4
+ import type {WorkflowRunListStatusFilter} from './types.js';
5
+
6
+ const STATUS_FILTERS: Array<{value: WorkflowRunListStatusFilter; label: string}> = [
7
+ {value: 'all', label: 'All'},
8
+ {value: 'failed', label: 'Failed'},
9
+ {value: 'running', label: 'Running'},
10
+ ];
11
+
12
+ interface WorkflowRunListHeaderProps {
13
+ query: string;
14
+ onQueryChange: (value: string) => void;
15
+ statusFilter: WorkflowRunListStatusFilter;
16
+ onStatusFilterChange: (value: WorkflowRunListStatusFilter) => void;
17
+ }
18
+
19
+ export function WorkflowRunListHeader({
20
+ query,
21
+ onQueryChange,
22
+ statusFilter,
23
+ onStatusFilterChange,
24
+ }: WorkflowRunListHeaderProps) {
25
+ return (
26
+ <div className="flex flex-col gap-8 border-b border-border-neutral-base px-12 py-12">
27
+ <Input
28
+ value={query}
29
+ onChange={(event) => onQueryChange(event.target.value)}
30
+ placeholder="Run id or trigger..."
31
+ aria-label="Search runs"
32
+ size="small"
33
+ iconLeft={<Icon name="searchLine" className="size-14 text-foreground-neutral-muted" />}
34
+ />
35
+
36
+ <fieldset className="flex items-center gap-8">
37
+ <legend className="sr-only">Run status filter</legend>
38
+ {STATUS_FILTERS.map((filter) => (
39
+ <Button
40
+ key={filter.value}
41
+ type="button"
42
+ size="2xs"
43
+ variant={statusFilter === filter.value ? 'primary' : 'transparent'}
44
+ aria-pressed={statusFilter === filter.value}
45
+ onClick={() => onStatusFilterChange(filter.value)}
46
+ >
47
+ {filter.label}
48
+ </Button>
49
+ ))}
50
+ </fieldset>
51
+ </div>
52
+ );
53
+ }
@@ -0,0 +1,86 @@
1
+ import type {QueryLoadErrorQuery} from '@shipfox/client-ui';
2
+ import {Button} from '@shipfox/react-ui/button';
3
+ import {Callout} from '@shipfox/react-ui/callout';
4
+ import {EmptyState} from '@shipfox/react-ui/empty-state';
5
+ import {Skeleton} from '@shipfox/react-ui/skeleton';
6
+ import {Text} from '@shipfox/react-ui/typography';
7
+
8
+ export function WorkflowRunListSkeleton() {
9
+ return (
10
+ <div className="flex flex-col gap-4 p-8" role="status" aria-label="Loading runs">
11
+ {Array.from({length: 6}).map((_, index) => (
12
+ <div
13
+ // biome-ignore lint/suspicious/noArrayIndexKey: skeleton row, stable position
14
+ key={index}
15
+ className="flex flex-col gap-3 rounded-8 border border-transparent px-10 py-7"
16
+ >
17
+ <div className="flex h-20 items-center gap-7">
18
+ <Skeleton className="size-6 shrink-0 rounded-full" />
19
+ <Skeleton className="h-12 w-1/3" />
20
+ </div>
21
+ <div className="flex h-20 items-center gap-6 pl-13">
22
+ <Skeleton className="h-12 flex-1" />
23
+ <Skeleton className="h-12 w-40" />
24
+ </div>
25
+ </div>
26
+ ))}
27
+ </div>
28
+ );
29
+ }
30
+
31
+ /**
32
+ * Slim non-blocking banner shown when a background refetch fails after a prior success,
33
+ * so the already-loaded rows stay on screen with the retry inline rather than being
34
+ * replaced by a full-placeholder error.
35
+ */
36
+ export function WorkflowRunListStaleError({query}: {query: QueryLoadErrorQuery}) {
37
+ return (
38
+ <div className="border-b border-border-neutral-base p-8">
39
+ <Callout role="alert" type="error">
40
+ <div className="flex items-center justify-between gap-8">
41
+ <Text size="xs">Could not refresh workflow runs.</Text>
42
+ <Button
43
+ type="button"
44
+ size="2xs"
45
+ variant="secondary"
46
+ isLoading={query.isFetching}
47
+ onClick={() => {
48
+ void query.refetch();
49
+ }}
50
+ >
51
+ Retry
52
+ </Button>
53
+ </div>
54
+ </Callout>
55
+ </div>
56
+ );
57
+ }
58
+
59
+ export function WorkflowRunListEmpty() {
60
+ return (
61
+ <div className="p-16">
62
+ <EmptyState
63
+ icon="pulseLine"
64
+ title="No runs yet"
65
+ description="Runs from this project's workflows will appear here as soon as one is launched."
66
+ />
67
+ </div>
68
+ );
69
+ }
70
+
71
+ export function WorkflowRunListNoMatches({onClear}: {onClear: () => void}) {
72
+ return (
73
+ <div className="p-16">
74
+ <EmptyState
75
+ icon="filterOffLine"
76
+ title="No matching runs"
77
+ description="No runs match your current search or status filter."
78
+ action={
79
+ <Button type="button" size="sm" variant="secondary" onClick={onClear}>
80
+ Clear filters
81
+ </Button>
82
+ }
83
+ />
84
+ </div>
85
+ );
86
+ }
@@ -0,0 +1,178 @@
1
+ import {screen} from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import type {WorkflowRunListItem, WorkflowRunStatus} from '#core/workflow-run.js';
4
+ import {workflowRunListItem} from '#test/fixtures/workflow-run.js';
5
+ import {renderWithRouter} from '#test/render.js';
6
+ import type {WorkflowRunListQuery} from './types.js';
7
+ import {WorkflowRunListView} from './workflow-run-list-view.js';
8
+
9
+ const WORKSPACE_ID = '11111111-1111-4111-8111-111111111111';
10
+ const PROJECT_ID = '44444444-4444-4444-8444-444444444444';
11
+
12
+ function loadedQuery(): WorkflowRunListQuery {
13
+ return {
14
+ isPending: false,
15
+ isError: false,
16
+ isFetching: false,
17
+ data: {pages: [], pageParams: []},
18
+ error: null,
19
+ refetch: () => undefined,
20
+ };
21
+ }
22
+
23
+ describe('WorkflowRunListView', () => {
24
+ afterEach(() => {
25
+ vi.useRealTimers();
26
+ vi.restoreAllMocks();
27
+ });
28
+
29
+ test('narrows the list to the selected status filter', async () => {
30
+ const user = userEvent.setup();
31
+ renderListView([
32
+ run('running', 'deploy-web'),
33
+ run('failed', 'integration-tests'),
34
+ run('succeeded', 'build-image'),
35
+ ]);
36
+
37
+ await user.click(await screen.findByRole('button', {name: 'Failed'}));
38
+
39
+ expect(screen.getByText('integration-tests')).toBeInTheDocument();
40
+ expect(screen.queryByText('deploy-web')).not.toBeInTheDocument();
41
+ expect(screen.queryByText('build-image')).not.toBeInTheDocument();
42
+ expect(screen.getByRole('button', {name: 'Failed'})).toHaveAttribute('aria-pressed', 'true');
43
+ });
44
+
45
+ test('the Running filter keeps in-progress pending runs alongside running ones', async () => {
46
+ const user = userEvent.setup();
47
+ renderListView([
48
+ run('running', 'deploy-web'),
49
+ run('pending', 'queued-build'),
50
+ run('succeeded', 'build-image'),
51
+ ]);
52
+
53
+ await user.click(await screen.findByRole('button', {name: 'Running'}));
54
+
55
+ expect(screen.getByText('deploy-web')).toBeInTheDocument();
56
+ expect(screen.getByText('queued-build')).toBeInTheDocument();
57
+ expect(screen.queryByText('build-image')).not.toBeInTheDocument();
58
+ });
59
+
60
+ test('renders optimistic temp runs without a navigable link', async () => {
61
+ renderListView([run('pending', 'queued-build', 'temp-1234'), run('running', 'deploy-web')]);
62
+
63
+ // The canonical run is a link to its detail page; the optimistic temp run is shown but
64
+ // not yet navigable (its detail page does not exist until the canonical row replaces it).
65
+ const links = await screen.findAllByRole('link');
66
+ expect(links.some((link) => link.textContent?.includes('deploy-web'))).toBe(true);
67
+ expect(links.some((link) => link.textContent?.includes('queued-build'))).toBe(false);
68
+ expect(screen.getByText('queued-build')).toBeInTheDocument();
69
+ });
70
+
71
+ test('shows a finished run duration in the row metadata', async () => {
72
+ renderListView([
73
+ run('succeeded', 'build-image', 'run-build-image', {
74
+ started_at: '2026-05-07T01:00:00.000Z',
75
+ finished_at: '2026-05-07T01:02:14.000Z',
76
+ }),
77
+ ]);
78
+
79
+ const duration = await screen.findByText('2m 14s');
80
+ expect(duration).toBeInTheDocument();
81
+ expect(duration).toHaveAttribute('aria-label', 'ran 2m 14s');
82
+ expect(
83
+ screen.getByRole('link', {
84
+ name: (name) => name.includes('build-image') && name.includes('ran 2m 14s'),
85
+ }),
86
+ ).toBeInTheDocument();
87
+ });
88
+
89
+ test('shows a live running run duration in the row metadata', async () => {
90
+ vi.spyOn(Date, 'now').mockReturnValue(Date.parse('2026-05-07T01:02:14.000Z'));
91
+
92
+ renderListView([
93
+ run('running', 'deploy-web', 'run-deploy-web', {
94
+ started_at: '2026-05-07T01:00:00.000Z',
95
+ finished_at: null,
96
+ }),
97
+ ]);
98
+
99
+ const duration = await screen.findByText('2m 14s');
100
+ expect(duration).toBeInTheDocument();
101
+ expect(duration).toHaveAttribute('aria-label', 'running 2m 14s');
102
+ expect(
103
+ screen.getByRole('link', {
104
+ name: (name) => name.includes('deploy-web') && name.includes('running 2m 14s'),
105
+ }),
106
+ ).toBeInTheDocument();
107
+ });
108
+
109
+ test('scopes the trigger tooltip to the trigger label', async () => {
110
+ const user = userEvent.setup();
111
+ renderListView([run('failed', 'integration-tests')]);
112
+
113
+ await user.hover(
114
+ await screen.findByRole('link', {
115
+ name: (name) =>
116
+ name.includes('integration-tests') && name.includes('github') && name.includes('push'),
117
+ }),
118
+ );
119
+
120
+ expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
121
+
122
+ await user.hover(screen.getByText('push'));
123
+
124
+ expect(await screen.findByRole('tooltip')).toHaveTextContent('github_acme · push');
125
+ });
126
+
127
+ test('restores every row after the filters are cleared', async () => {
128
+ const user = userEvent.setup();
129
+ renderListView([
130
+ run('running', 'deploy-web'),
131
+ run('failed', 'integration-tests'),
132
+ run('succeeded', 'build-image'),
133
+ ]);
134
+ await user.type(await screen.findByLabelText('Search runs'), 'no-such-run');
135
+
136
+ expect(screen.getByText('No matching runs')).toBeInTheDocument();
137
+
138
+ await user.click(await screen.findByRole('button', {name: 'Clear filters'}));
139
+
140
+ expect(screen.getByText('deploy-web')).toBeInTheDocument();
141
+ expect(screen.getByText('integration-tests')).toBeInTheDocument();
142
+ expect(screen.getByText('build-image')).toBeInTheDocument();
143
+ expect(screen.getByLabelText('Search runs')).toHaveValue('');
144
+ });
145
+ });
146
+
147
+ function renderListView(runs: WorkflowRunListItem[]) {
148
+ // Row links need router context; the query and data stay injected by props.
149
+ renderWithRouter(
150
+ <WorkflowRunListView
151
+ runs={runs}
152
+ query={loadedQuery()}
153
+ workspaceId={WORKSPACE_ID}
154
+ projectId={PROJECT_ID}
155
+ />,
156
+ );
157
+ }
158
+
159
+ function run(
160
+ status: WorkflowRunStatus,
161
+ name: string,
162
+ id = `run-${name}`,
163
+ overrides: NonNullable<Parameters<typeof workflowRunListItem>[0]> = {},
164
+ ): WorkflowRunListItem {
165
+ return workflowRunListItem({
166
+ id,
167
+ project_id: PROJECT_ID,
168
+ definition_id: '55555555-5555-4555-8555-555555555555',
169
+ name,
170
+ status,
171
+ trigger_provider: 'github',
172
+ trigger_source: 'github_acme',
173
+ trigger_event: 'push',
174
+ created_at: '2026-05-07T01:01:00.000Z',
175
+ updated_at: '2026-05-07T01:02:00.000Z',
176
+ ...overrides,
177
+ });
178
+ }
@@ -0,0 +1,57 @@
1
+ import {TimeTickerProvider} from '@shipfox/react-ui/time-ticker';
2
+ import {cn} from '@shipfox/react-ui/utils';
3
+ import {useState} from 'react';
4
+ import {runMatchesSearch, runMatchesStatusFilter} from './run-display.js';
5
+ import type {WorkflowRunListStatusFilter, WorkflowRunListViewProps} from './types.js';
6
+ import {WorkflowRunListContent} from './workflow-run-list-content.js';
7
+ import {WorkflowRunListHeader} from './workflow-run-list-header.js';
8
+
9
+ export function WorkflowRunListView({
10
+ runs,
11
+ query,
12
+ workspaceId,
13
+ projectId,
14
+ selectedWorkflowRunId,
15
+ className,
16
+ }: WorkflowRunListViewProps) {
17
+ const [search, setSearch] = useState('');
18
+ const [statusFilter, setStatusFilter] = useState<WorkflowRunListStatusFilter>('all');
19
+
20
+ const filteredRuns = runs.filter((run) => {
21
+ if (!runMatchesStatusFilter(run.status, statusFilter)) return false;
22
+ return runMatchesSearch(run, search);
23
+ });
24
+
25
+ function handleClearFilters() {
26
+ setSearch('');
27
+ setStatusFilter('all');
28
+ }
29
+
30
+ return (
31
+ <TimeTickerProvider intervalMs={1000} reducedMotionIntervalMs={10_000}>
32
+ <aside
33
+ className={cn(
34
+ 'flex w-304 shrink-0 flex-col border-r border-border-neutral-base bg-background-subtle-base',
35
+ className,
36
+ )}
37
+ aria-label="Workflow runs"
38
+ >
39
+ <WorkflowRunListHeader
40
+ query={search}
41
+ onQueryChange={setSearch}
42
+ statusFilter={statusFilter}
43
+ onStatusFilterChange={setStatusFilter}
44
+ />
45
+ <WorkflowRunListContent
46
+ query={query}
47
+ totalRuns={runs.length}
48
+ runs={filteredRuns}
49
+ workspaceId={workspaceId}
50
+ projectId={projectId}
51
+ selectedWorkflowRunId={selectedWorkflowRunId}
52
+ onClearFilters={handleClearFilters}
53
+ />
54
+ </aside>
55
+ </TimeTickerProvider>
56
+ );
57
+ }
@@ -0,0 +1,111 @@
1
+ import {Code} from '@shipfox/react-ui/typography';
2
+ import type {Meta, StoryObj} from '@storybook/react';
3
+ import type {ReactNode} from 'react';
4
+ import {expect, userEvent, within} from 'storybook/test';
5
+ import type {WorkflowRunListItem, WorkflowRunStatus} from '#core/workflow-run.js';
6
+ import {sequencedWorkflowRunListItem} from '#test/fixtures/workflow-run.js';
7
+ import type {WorkflowRunListQuery} from './types.js';
8
+ import {WorkflowRunListView} from './workflow-run-list-view.js';
9
+
10
+ // Stand-in for the react-query result the view reads. `data !== undefined` is the
11
+ // "loaded at least once" signal that splits a fresh load error from a stale refresh.
12
+ function makeQuery(overrides: Partial<WorkflowRunListQuery> = {}): WorkflowRunListQuery {
13
+ return {
14
+ isPending: false,
15
+ isError: false,
16
+ isFetching: false,
17
+ data: {pages: [], pageParams: []},
18
+ error: null,
19
+ refetch: () => undefined,
20
+ ...overrides,
21
+ };
22
+ }
23
+
24
+ function makeRun(status: WorkflowRunStatus, name: string, minutesAgo: number): WorkflowRunListItem {
25
+ return sequencedWorkflowRunListItem(status, name, minutesAgo);
26
+ }
27
+
28
+ const SAMPLE_RUNS: WorkflowRunListItem[] = [
29
+ makeRun('running', 'deploy-web', 1),
30
+ makeRun('failed', 'integration-tests', 4),
31
+ makeRun('succeeded', 'build-image', 12),
32
+ makeRun('cancelled', 'lint-and-type', 38),
33
+ makeRun('pending', 'release-prod', 0),
34
+ makeRun('succeeded', 'release-production-multi-region-with-canary-and-smoke-tests', 95),
35
+ ];
36
+
37
+ // One story on the full rail. The data states (loading / empty / errors / runs) are
38
+ // driven by args; search, status filter, "clear filters" and "no matches" are live in
39
+ // the rendered toolbar since the view owns that state. The decorator gives the rail a
40
+ // real height so `flex-1` content scrolls the way it does in the app shell.
41
+ const meta = {
42
+ title: 'Workflows/WorkflowRunList',
43
+ component: WorkflowRunListView,
44
+ parameters: {layout: 'centered'},
45
+ decorators: [
46
+ (Story) => (
47
+ <div className="flex h-600">
48
+ <Story />
49
+ </div>
50
+ ),
51
+ ],
52
+ args: {
53
+ runs: SAMPLE_RUNS,
54
+ query: makeQuery(),
55
+ workspaceId: 'ws-demo',
56
+ projectId: 'proj-demo',
57
+ },
58
+ } satisfies Meta<typeof WorkflowRunListView>;
59
+
60
+ export default meta;
61
+ type Story = StoryObj<typeof meta>;
62
+
63
+ export const Playground: Story = {};
64
+
65
+ export const Selected: Story = {
66
+ args: {selectedWorkflowRunId: SAMPLE_RUNS[1].id},
67
+ };
68
+
69
+ export const DataStates: Story = {
70
+ render: (args) => (
71
+ <div className="grid grid-cols-2 gap-24">
72
+ <StateExample label="Loading">
73
+ <WorkflowRunListView {...args} query={makeQuery({isPending: true, data: undefined})} />
74
+ </StateExample>
75
+ <StateExample label="Empty">
76
+ <WorkflowRunListView {...args} runs={[]} />
77
+ </StateExample>
78
+ <StateExample label="Load error">
79
+ <WorkflowRunListView
80
+ {...args}
81
+ runs={[]}
82
+ query={makeQuery({isError: true, data: undefined})}
83
+ />
84
+ </StateExample>
85
+ <StateExample label="Stale error">
86
+ <WorkflowRunListView {...args} query={makeQuery({isError: true})} />
87
+ </StateExample>
88
+ </div>
89
+ ),
90
+ };
91
+
92
+ export const TestNoMatches: Story = {
93
+ play: async ({canvasElement}) => {
94
+ const canvas = within(canvasElement);
95
+ await userEvent.type(canvas.getByLabelText('Search runs'), 'no-such-run');
96
+ await expect(await canvas.findByText('No matching runs')).toBeInTheDocument();
97
+ },
98
+ };
99
+
100
+ function StateExample({label, children}: {label: string; children: ReactNode}) {
101
+ return (
102
+ <div className="flex w-304 flex-col gap-8">
103
+ <Code variant="label" className="text-foreground-neutral-subtle">
104
+ {label}
105
+ </Code>
106
+ <div className="flex h-560 rounded-8 border border-border-neutral-base bg-background-neutral-base">
107
+ {children}
108
+ </div>
109
+ </div>
110
+ );
111
+ }
@@ -0,0 +1,29 @@
1
+ import {useMemo} from 'react';
2
+ import type {WorkflowRunListItem} from '#core/workflow-run.js';
3
+ import {useWorkflowRunsInfiniteQuery} from '#hooks/api/workflow-runs.js';
4
+ import type {WorkflowRunListProps} from './types.js';
5
+ import {WorkflowRunListView} from './workflow-run-list-view.js';
6
+
7
+ export function WorkflowRunList({
8
+ workspaceId,
9
+ projectId,
10
+ selectedWorkflowRunId,
11
+ className,
12
+ }: WorkflowRunListProps) {
13
+ const query = useWorkflowRunsInfiniteQuery(projectId, {});
14
+ const runs = useMemo<WorkflowRunListItem[]>(
15
+ () => query.data?.pages.flatMap((page) => page.runs) ?? [],
16
+ [query.data],
17
+ );
18
+
19
+ return (
20
+ <WorkflowRunListView
21
+ runs={runs}
22
+ query={query}
23
+ workspaceId={workspaceId}
24
+ projectId={projectId}
25
+ selectedWorkflowRunId={selectedWorkflowRunId}
26
+ className={className}
27
+ />
28
+ );
29
+ }