@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,566 @@
1
+ import {agentConfigIssueSchema, stepErrorReasonSchema} from '@shipfox/api-workflows-dto';
2
+ import {TriggerSourceIcon} from '@shipfox/client-triggers';
3
+ import {Badge} from '@shipfox/react-ui/badge';
4
+ import {Button} from '@shipfox/react-ui/button';
5
+ import {EmptyState} from '@shipfox/react-ui/empty-state';
6
+ import {Icon} from '@shipfox/react-ui/icon';
7
+ import {RelativeTime} from '@shipfox/react-ui/relative-time';
8
+ import {useTimeTick} from '@shipfox/react-ui/time-ticker';
9
+ import {Tooltip, TooltipContent, TooltipTrigger} from '@shipfox/react-ui/tooltip';
10
+ import {Text} from '@shipfox/react-ui/typography';
11
+ import type {ReactNode} from 'react';
12
+ import {Fragment, useId, useRef, useState} from 'react';
13
+ import {getWorkflowStatusVisual} from '#components/workflow-status/status-visuals.js';
14
+ import {
15
+ type Job,
16
+ type JobExecution,
17
+ type JobExecutionTime,
18
+ type Step,
19
+ type StepError,
20
+ type StepSourceLocation,
21
+ WORKFLOW_JOB_STATUSES,
22
+ workflowRunTriggerDisplayLabel,
23
+ workflowRunTriggerLabel,
24
+ } from '#core/workflow-run.js';
25
+ import {type StepExpandedContext, StepList, type StepListEmptyState} from '../step-list/index.js';
26
+ import {AgentConfigFailureCallout} from './agent-config-failure-callout.js';
27
+ import {JobExecutionSwitcher} from './job-execution-switcher.js';
28
+ import {formatJobExecutionTime, JobExecutionTimeText} from './job-execution-time-text.js';
29
+ import {StepAttemptLogPanel} from './step-attempt-log-panel.js';
30
+
31
+ const STATUS_BADGE_LABEL_WIDTH_CH = Math.max(
32
+ ...WORKFLOW_JOB_STATUSES.map((status) => getWorkflowStatusVisual(status).label.length),
33
+ );
34
+
35
+ interface LocalSelectedAttempt {
36
+ jobExecutionId: string | undefined;
37
+ attemptId: string | null;
38
+ }
39
+
40
+ export function JobCard({
41
+ workspaceId,
42
+ job,
43
+ selectedJobExecution,
44
+ selectedAttemptId,
45
+ onSelectedJobExecutionChange,
46
+ onSelectedAttemptChange,
47
+ renderExpandedStep,
48
+ sourcePanelId,
49
+ sourceAvailable,
50
+ focusedSourceStepId,
51
+ onOpenStepSource,
52
+ }: {
53
+ workspaceId: string;
54
+ job: Job;
55
+ selectedJobExecution: JobExecution | undefined;
56
+ selectedAttemptId: string | null | undefined;
57
+ onSelectedJobExecutionChange: ((jobExecutionId: string | undefined) => void) | undefined;
58
+ onSelectedAttemptChange: ((attemptId: string | undefined) => void) | undefined;
59
+ renderExpandedStep?: ((context: StepExpandedContext) => ReactNode) | undefined;
60
+ sourcePanelId?: string | undefined;
61
+ sourceAvailable?: boolean | undefined;
62
+ focusedSourceStepId?: string | null | undefined;
63
+ onOpenStepSource?:
64
+ | ((stepId: string, location: StepSourceLocation, trigger: HTMLButtonElement | null) => void)
65
+ | undefined;
66
+ }) {
67
+ const titleId = useId();
68
+ const sourceButtonRef = useRef<HTMLButtonElement>(null);
69
+ const [localSelectedAttempt, setLocalSelectedAttempt] = useState<LocalSelectedAttempt>({
70
+ jobExecutionId: undefined,
71
+ attemptId: null,
72
+ });
73
+ const selectedExecutionStatus = selectedJobExecution?.status ?? job.status;
74
+ const effectiveSelectedAttemptId =
75
+ selectedAttemptId === undefined &&
76
+ localSelectedAttempt.jobExecutionId === selectedJobExecution?.id
77
+ ? localSelectedAttempt.attemptId
78
+ : selectedAttemptId;
79
+ const selectedSourceAction = selectedStepSourceAction(
80
+ selectedJobExecution,
81
+ effectiveSelectedAttemptId,
82
+ );
83
+ const defaultRenderExpandedStep = ({
84
+ step,
85
+ stepId,
86
+ attempt,
87
+ attemptError,
88
+ attemptStatus,
89
+ carriedOver,
90
+ }: StepExpandedContext) =>
91
+ carriedOver ? (
92
+ <CarriedOverStepPanel />
93
+ ) : (
94
+ <StepAttemptDetailPanel
95
+ workspaceId={workspaceId}
96
+ step={step}
97
+ stepId={stepId}
98
+ attempt={attempt}
99
+ attemptError={attemptError}
100
+ attemptStatus={attemptStatus}
101
+ />
102
+ );
103
+
104
+ function selectAttempt(attemptId: string | undefined) {
105
+ if (selectedAttemptId === undefined) {
106
+ setLocalSelectedAttempt({
107
+ jobExecutionId: selectedJobExecution?.id,
108
+ attemptId: attemptId ?? null,
109
+ });
110
+ }
111
+
112
+ onSelectedAttemptChange?.(attemptId);
113
+ }
114
+
115
+ return (
116
+ <section
117
+ aria-labelledby={titleId}
118
+ className="flex min-h-0 flex-col rounded-8 border border-border-neutral-base bg-background-components-base"
119
+ >
120
+ <div className="grid min-h-52 min-w-0 grid-cols-1 gap-y-4 px-16 py-12">
121
+ <div className="flex min-w-0 flex-wrap items-center justify-between gap-10">
122
+ {job.mode === 'listening' &&
123
+ job.jobExecutions.length > 0 &&
124
+ onSelectedJobExecutionChange ? (
125
+ <>
126
+ <Text as="h2" id={titleId} size="sm" bold className="sr-only">
127
+ {job.displayName}
128
+ </Text>
129
+ <div className="flex min-w-0 items-center gap-8">
130
+ <JobStatusBadge status={selectedExecutionStatus} />
131
+ <JobExecutionSwitcher
132
+ job={job}
133
+ selectedJobExecution={selectedJobExecution?.id ?? null}
134
+ onSelectedJobExecutionChange={onSelectedJobExecutionChange}
135
+ variant="title"
136
+ />
137
+ </div>
138
+ </>
139
+ ) : (
140
+ <div className="flex min-w-0 items-center gap-8">
141
+ <JobStatusBadge status={selectedExecutionStatus} />
142
+ <Text
143
+ as="h2"
144
+ id={titleId}
145
+ size="sm"
146
+ bold
147
+ className="min-w-0 truncate text-foreground-neutral-base"
148
+ >
149
+ {job.displayName}
150
+ </Text>
151
+ </div>
152
+ )}
153
+ {sourceAvailable && selectedSourceAction && sourcePanelId && onOpenStepSource ? (
154
+ <div className="flex min-w-max items-center gap-6">
155
+ <Button
156
+ ref={sourceButtonRef}
157
+ type="button"
158
+ variant="secondary"
159
+ size="xs"
160
+ aria-controls={sourcePanelId}
161
+ aria-expanded={focusedSourceStepId === selectedSourceAction.stepId}
162
+ onClick={() =>
163
+ onOpenStepSource(
164
+ selectedSourceAction.stepId,
165
+ selectedSourceAction.location,
166
+ sourceButtonRef.current,
167
+ )
168
+ }
169
+ >
170
+ View source
171
+ </Button>
172
+ </div>
173
+ ) : null}
174
+ </div>
175
+ <JobExecutionMetadata execution={selectedJobExecution} />
176
+ </div>
177
+ <div className="min-h-0 border-t border-border-neutral-strong">
178
+ {selectedJobExecution ? (
179
+ <StepList
180
+ job={job}
181
+ jobExecution={selectedJobExecution}
182
+ selectedAttemptId={selectedAttemptId}
183
+ onSelectedAttemptChange={selectAttempt}
184
+ autoSelectActiveAttempt
185
+ emptyState={emptyStateForJob(job, selectedJobExecution)}
186
+ showHeader={false}
187
+ className="rounded-none border-0 bg-transparent"
188
+ renderExpandedStep={renderExpandedStep ?? defaultRenderExpandedStep}
189
+ />
190
+ ) : (
191
+ <JobExecutionEmptyState job={job} />
192
+ )}
193
+ </div>
194
+ </section>
195
+ );
196
+ }
197
+
198
+ function JobStatusBadge({status}: {status: Job['status'] | JobExecution['status']}) {
199
+ const visual = getWorkflowStatusVisual(status);
200
+
201
+ return (
202
+ <Badge variant={visual.badge} size="xs">
203
+ <span className="text-center" style={{width: `${STATUS_BADGE_LABEL_WIDTH_CH}ch`}}>
204
+ {visual.label}
205
+ </span>
206
+ </Badge>
207
+ );
208
+ }
209
+
210
+ function JobExecutionEmptyState({job}: {job: Job}) {
211
+ const emptyState = emptyStateForMissingExecution(job);
212
+
213
+ return (
214
+ <EmptyState
215
+ className="min-h-120 px-16 py-20"
216
+ icon="componentLine"
217
+ title={emptyState.title}
218
+ description={emptyState.description}
219
+ variant="compact"
220
+ />
221
+ );
222
+ }
223
+
224
+ function JobExecutionMetadata({execution}: {execution: JobExecution | undefined}) {
225
+ if (!execution) return null;
226
+
227
+ const queueTime = execution.queueTime;
228
+ const runTime = execution.runTime;
229
+ const executionStartedAt = execution.startedAt ?? execution.queuedAt ?? execution.createdAt;
230
+ const items = [
231
+ execution.triggerEvents.length > 0 ? {key: 'trigger', kind: 'trigger' as const} : null,
232
+ {key: 'started', kind: 'started' as const, value: executionStartedAt},
233
+ queueTime
234
+ ? {key: 'queue', icon: 'hourglassLine' as const, kind: 'queue' as const, time: queueTime}
235
+ : null,
236
+ runTime ? {key: 'run', icon: 'timerLine' as const, kind: 'run' as const, time: runTime} : null,
237
+ ].filter((item) => item !== null);
238
+
239
+ if (items.length === 0) return null;
240
+
241
+ return (
242
+ <div className="flex min-w-0 items-center gap-12 overflow-hidden text-foreground-neutral-muted">
243
+ {items.map((item, index) => (
244
+ <Fragment key={item.key}>
245
+ {index > 0 ? <MetadataSeparator /> : null}
246
+ {item.kind === 'started' ? (
247
+ <RelativeTime
248
+ value={item.value}
249
+ className="shrink-0 whitespace-nowrap text-xs leading-20 text-foreground-neutral-muted"
250
+ />
251
+ ) : item.kind === 'trigger' ? (
252
+ <JobExecutionTriggerMetadata execution={execution} />
253
+ ) : (
254
+ <JobExecutionMetadataItem icon={item.icon} kind={item.kind} time={item.time} />
255
+ )}
256
+ </Fragment>
257
+ ))}
258
+ </div>
259
+ );
260
+ }
261
+
262
+ function JobExecutionTriggerMetadata({execution}: {execution: JobExecution}) {
263
+ const triggerEvent = execution.triggerEvents[0];
264
+ if (!triggerEvent) return null;
265
+
266
+ const triggerLabel = workflowRunTriggerLabel({
267
+ triggerSource: triggerEvent.source,
268
+ triggerEvent: triggerEvent.event,
269
+ });
270
+ const triggerDisplayLabel = workflowRunTriggerDisplayLabel({
271
+ triggerSource: triggerEvent.source,
272
+ triggerEvent: triggerEvent.event,
273
+ });
274
+ const triggerCount = execution.triggerEvents.length;
275
+ const countLabel = triggerCount > 1 ? ` (${triggerCount})` : '';
276
+ const tooltip = triggerCount > 1 ? `${triggerLabel} (${triggerCount} events)` : triggerLabel;
277
+
278
+ return (
279
+ <Tooltip>
280
+ <TooltipTrigger asChild>
281
+ <button
282
+ type="button"
283
+ aria-label={tooltip}
284
+ className="inline-flex min-w-0 max-w-full cursor-default items-center gap-4 rounded-4 border-0 bg-transparent p-0 text-left text-foreground-neutral-muted outline-none focus-visible:shadow-border-interactive-with-active"
285
+ >
286
+ <TriggerSourceIcon
287
+ provider={triggerEvent.source}
288
+ source={triggerEvent.source}
289
+ aria-hidden="true"
290
+ className="size-12 shrink-0"
291
+ />
292
+ <Text as="span" size="xs" className="min-w-0 truncate">
293
+ {triggerDisplayLabel}
294
+ {countLabel}
295
+ </Text>
296
+ </button>
297
+ </TooltipTrigger>
298
+ <TooltipContent>
299
+ <Text as="span" size="xs" className="block max-w-[360px] break-words">
300
+ {tooltip}
301
+ </Text>
302
+ </TooltipContent>
303
+ </Tooltip>
304
+ );
305
+ }
306
+
307
+ function JobExecutionMetadataItem({
308
+ icon,
309
+ kind,
310
+ time,
311
+ }: {
312
+ icon: 'hourglassLine' | 'timerLine';
313
+ kind: 'queue' | 'run';
314
+ time: JobExecutionTime;
315
+ }) {
316
+ useTimeTick();
317
+
318
+ const tooltip = `${jobExecutionTimeVerb(kind, time)} for ${formatJobExecutionTime(time)}`;
319
+
320
+ return (
321
+ <Tooltip>
322
+ <TooltipTrigger asChild>
323
+ <button
324
+ type="button"
325
+ aria-label={tooltip}
326
+ className="inline-flex min-w-0 cursor-default items-center gap-4 rounded-4 border-0 bg-transparent p-0 text-foreground-neutral-muted outline-none focus-visible:shadow-border-interactive-with-active"
327
+ >
328
+ <Icon name={icon} className="size-12 shrink-0" aria-hidden="true" />
329
+ <span className="shrink-0 text-xs leading-20 text-foreground-neutral-muted tabular-nums">
330
+ <JobExecutionTimeText time={time} />
331
+ </span>
332
+ </button>
333
+ </TooltipTrigger>
334
+ <TooltipContent>
335
+ <Text as="span" size="xs">
336
+ {tooltip}
337
+ </Text>
338
+ </TooltipContent>
339
+ </Tooltip>
340
+ );
341
+ }
342
+
343
+ function jobExecutionTimeVerb(kind: 'queue' | 'run', time: JobExecutionTime): string {
344
+ if (kind === 'queue') return time.state === 'live' ? 'Queuing' : 'Queued';
345
+ return time.state === 'live' ? 'Running' : 'Ran';
346
+ }
347
+
348
+ function MetadataSeparator() {
349
+ return <span aria-hidden="true" className="h-12 w-px shrink-0 bg-border-neutral-strong" />;
350
+ }
351
+
352
+ function selectedStepSourceAction(
353
+ jobExecution: JobExecution | undefined,
354
+ selectedAttemptId: string | null | undefined,
355
+ ): {stepId: string; location: StepSourceLocation} | null {
356
+ if (!jobExecution || !selectedAttemptId) return null;
357
+
358
+ for (const step of jobExecution.steps) {
359
+ const selected =
360
+ selectedAttemptId === `carried-over:${step.id}` ||
361
+ step.attempts.some((attempt) => attempt.id === selectedAttemptId);
362
+ if (selected && step.sourceLocation) {
363
+ return {stepId: step.id, location: step.sourceLocation};
364
+ }
365
+ }
366
+
367
+ return null;
368
+ }
369
+
370
+ function StepAttemptDetailPanel({
371
+ workspaceId,
372
+ step,
373
+ stepId,
374
+ attempt,
375
+ attemptError,
376
+ attemptStatus,
377
+ }: {
378
+ workspaceId: string;
379
+ step: Step;
380
+ stepId: string;
381
+ attempt: number;
382
+ attemptError: Record<string, unknown> | null;
383
+ attemptStatus: string;
384
+ }) {
385
+ const selectedAttemptError = toSelectedAttemptError(step, attemptError) ?? step.error;
386
+
387
+ return (
388
+ <div className="flex min-w-0 flex-col gap-10">
389
+ {isAgentConfigFailure(step, selectedAttemptError) ? (
390
+ <AgentConfigFailureCallout
391
+ workspaceId={workspaceId}
392
+ config={null}
393
+ error={selectedAttemptError}
394
+ />
395
+ ) : null}
396
+ <StepAttemptLogPanel stepId={stepId} attempt={attempt} attemptStatus={attemptStatus} />
397
+ </div>
398
+ );
399
+ }
400
+
401
+ function toSelectedAttemptError(
402
+ step: Step,
403
+ error: Record<string, unknown> | null,
404
+ ): StepError | null {
405
+ if (error === null) return null;
406
+
407
+ const reason = stepErrorReasonSchema.safeParse(error.reason);
408
+ const agentConfigIssue = agentConfigIssueSchema.safeParse(
409
+ error.agentConfigIssue ?? error.agent_config_issue,
410
+ );
411
+ const exitCode = error.exitCode ?? error.exit_code;
412
+ const parsedReason = reason.success
413
+ ? reason.data
414
+ : agentConfigIssue.success
415
+ ? 'agent_config_invalid'
416
+ : undefined;
417
+
418
+ if (parsedReason === undefined) return null;
419
+
420
+ return {
421
+ message: typeof error.message === 'string' ? error.message : '',
422
+ exitCode: exitCode === null || typeof exitCode === 'number' ? exitCode : null,
423
+ signal: typeof error.signal === 'string' ? error.signal : undefined,
424
+ reason: parsedReason,
425
+ agentConfigIssue: agentConfigIssue.success ? agentConfigIssue.data : undefined,
426
+ category: step.type === 'setup' ? 'setup' : 'user',
427
+ };
428
+ }
429
+
430
+ function isAgentConfigFailure(step: Step, error: StepError | null): boolean {
431
+ return step.type === 'agent' && error?.reason === 'agent_config_invalid';
432
+ }
433
+
434
+ function emptyStateForJob(job: Job, jobExecution: JobExecution): StepListEmptyState | undefined {
435
+ if (job.carriedOver) {
436
+ return {
437
+ title: 'Carried over from a previous attempt',
438
+ description: 'This job did not execute in this run.',
439
+ status: 'succeeded',
440
+ };
441
+ }
442
+
443
+ if (jobExecution.status === 'pending') {
444
+ return {
445
+ title: 'Waiting for this job to start',
446
+ description: 'Steps will appear here once the job starts.',
447
+ status: jobExecution.status,
448
+ };
449
+ }
450
+
451
+ if (jobExecution.status === 'running') {
452
+ return {
453
+ title: 'Waiting for the first step',
454
+ description: 'This job is running, but no steps have started yet.',
455
+ status: jobExecution.status,
456
+ };
457
+ }
458
+
459
+ if (jobExecution.status === 'cancelled') {
460
+ return {
461
+ title: 'Cancelled before start',
462
+ description: 'This job was cancelled before any step started.',
463
+ status: jobExecution.status,
464
+ };
465
+ }
466
+
467
+ if (jobExecution.status === 'succeeded' || jobExecution.status === 'failed') {
468
+ return {
469
+ title: 'No steps recorded',
470
+ description: `Execution #${jobExecution.sequence} finished without recorded steps.`,
471
+ status: jobExecution.status,
472
+ };
473
+ }
474
+
475
+ return undefined;
476
+ }
477
+
478
+ function emptyStateForMissingExecution(job: Job): StepListEmptyState {
479
+ if (job.carriedOver) {
480
+ return {
481
+ title: 'Carried over from a previous attempt',
482
+ description: 'This job did not execute in this run.',
483
+ status: 'succeeded',
484
+ };
485
+ }
486
+
487
+ if (job.mode === 'listening' && job.listenerStatus === 'listening') {
488
+ return {
489
+ title: 'Waiting for trigger events',
490
+ description: 'Matching trigger events will create job executions here.',
491
+ status: 'running',
492
+ };
493
+ }
494
+
495
+ if (job.mode === 'listening' && job.listenerStatus === 'resolved') {
496
+ return {
497
+ title: 'Listener resolved without executions',
498
+ description: 'No matching trigger event created a job execution before the listener stopped.',
499
+ status: job.status,
500
+ };
501
+ }
502
+
503
+ if (job.status === 'pending') {
504
+ return {
505
+ title: 'Waiting for this job to start',
506
+ description: 'No execution has been created for this job yet.',
507
+ status: 'pending',
508
+ };
509
+ }
510
+
511
+ if (job.status === 'skipped') {
512
+ return {
513
+ title: 'This job was skipped',
514
+ description: skippedJobDescription(job.statusReason),
515
+ status: 'skipped',
516
+ };
517
+ }
518
+
519
+ if (job.status === 'cancelled') {
520
+ return {
521
+ title: 'Cancelled before start',
522
+ description: 'This job was cancelled before an execution was created.',
523
+ status: 'cancelled',
524
+ };
525
+ }
526
+
527
+ return {
528
+ title: 'Execution details unavailable',
529
+ description: 'This job finished, but no job execution record is available.',
530
+ status: job.status,
531
+ };
532
+ }
533
+
534
+ function CarriedOverStepPanel() {
535
+ return (
536
+ <EmptyState
537
+ className="min-h-120 rounded-8 border border-border-neutral-base bg-background-components-base px-16 py-20"
538
+ icon="componentLine"
539
+ title="Carried over from a previous attempt"
540
+ description="Not executed in this run."
541
+ variant="compact"
542
+ />
543
+ );
544
+ }
545
+
546
+ function skippedJobDescription(reason: Job['statusReason']): string {
547
+ switch (reason) {
548
+ case 'dependency_not_completed':
549
+ return 'A required job did not complete, so this job was skipped.';
550
+ case 'default_gate_rejected':
551
+ return 'A required job did not succeed, so this job was skipped.';
552
+ case 'condition_false':
553
+ case 'condition_rejected':
554
+ return 'The job condition did not match, so this job was skipped.';
555
+ case 'condition_errored':
556
+ return 'The job condition could not be evaluated, so this job was skipped.';
557
+ case 'user_cancelled':
558
+ case 'run_cancelled':
559
+ case 'timed_out':
560
+ case 'runner_lost':
561
+ case 'step_failed':
562
+ case 'unknown':
563
+ case null:
564
+ return 'This job did not start.';
565
+ }
566
+ }