@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,341 @@
1
+ import type {WorkflowRunRerunModeDto} from '@shipfox/api-workflows-dto';
2
+ import {ApiError} from '@shipfox/client-api';
3
+ import {QueryLoadError} from '@shipfox/client-ui';
4
+ import {RelativeTimeProvider} from '@shipfox/react-ui/relative-time';
5
+ import {toast} from '@shipfox/react-ui/toast';
6
+ import {useNavigate} from '@tanstack/react-router';
7
+ import {useEffect, useId, useRef, useState} from 'react';
8
+ import {
9
+ type JobExecution,
10
+ resolveJobExecution,
11
+ type StepSourceLocation,
12
+ } from '#core/workflow-run.js';
13
+ import {
14
+ type WorkflowRunSelectionInput,
15
+ withoutWorkflowRunSelectionSearch,
16
+ } from '#core/workflow-run-url-state.js';
17
+ import {
18
+ useCancelWorkflowRunMutation,
19
+ useRerunWorkflowRunMutation,
20
+ useWorkflowRunAttemptQuery,
21
+ } from '#hooks/api/workflow-runs.js';
22
+ import {JobGraph} from '../job-graph/index.js';
23
+ import {WorkflowRunSummary} from '../workflow-run-summary/index.js';
24
+ import {WorkflowSourcePanel} from '../workflow-source-panel/index.js';
25
+ import {JobCard} from './job-card.js';
26
+ import {resolveWorkflowRunSelection} from './workflow-run-selection.js';
27
+ import {
28
+ WorkflowRunNotFound,
29
+ WorkflowRunSkeleton,
30
+ WorkflowRunStaleError,
31
+ } from './workflow-run-states.js';
32
+
33
+ interface WorkflowSourceFocus {
34
+ stepId: string;
35
+ location: StepSourceLocation;
36
+ }
37
+
38
+ export interface WorkflowRunViewProps {
39
+ workspaceId: string;
40
+ projectId: string;
41
+ workflowRunId?: string | undefined;
42
+ selection?: WorkflowRunSelectionInput | undefined;
43
+ onSelectionChange?: ((selection: WorkflowRunSelectionInput) => void) | undefined;
44
+ }
45
+
46
+ /**
47
+ * Renders the run for `workflowRunId`, or a skeleton while `workflowRunId` is still unknown (the page is
48
+ * resolving which run to show) or the run is loading, so the page never branches on the
49
+ * loading state itself.
50
+ */
51
+ export function WorkflowRunView({
52
+ workspaceId,
53
+ projectId,
54
+ workflowRunId,
55
+ selection,
56
+ onSelectionChange,
57
+ }: WorkflowRunViewProps) {
58
+ const runQuery = useWorkflowRunAttemptQuery({workflowRunId, runAttempt: selection?.runAttempt});
59
+ const rerunMutation = useRerunWorkflowRunMutation(projectId);
60
+
61
+ return (
62
+ <RelativeTimeProvider>
63
+ <div className="flex min-h-0 min-w-0 flex-1 overflow-hidden">
64
+ <RunViewContent
65
+ workspaceId={workspaceId}
66
+ projectId={projectId}
67
+ query={runQuery}
68
+ rerunMutation={rerunMutation}
69
+ selection={selection}
70
+ onSelectionChange={onSelectionChange}
71
+ />
72
+ </div>
73
+ </RelativeTimeProvider>
74
+ );
75
+ }
76
+
77
+ function RunViewContent({
78
+ workspaceId,
79
+ projectId,
80
+ query,
81
+ rerunMutation,
82
+ selection,
83
+ onSelectionChange,
84
+ }: {
85
+ workspaceId: string;
86
+ projectId: string;
87
+ query: ReturnType<typeof useWorkflowRunAttemptQuery>;
88
+ rerunMutation: ReturnType<typeof useRerunWorkflowRunMutation>;
89
+ selection: WorkflowRunSelectionInput | undefined;
90
+ onSelectionChange: ((selection: WorkflowRunSelectionInput) => void) | undefined;
91
+ }) {
92
+ const navigate = useNavigate();
93
+ const [selectedJobId, setSelectedJobId] = useState<string | undefined>();
94
+ const [selectedJobExecutionId, setSelectedJobExecutionId] = useState<string | undefined>();
95
+ const [sourcePanelOpen, setSourcePanelOpen] = useState(false);
96
+ const [sourceFocus, setSourceFocus] = useState<WorkflowSourceFocus | null>(null);
97
+ const sourcePanelId = useId();
98
+ const sourceButtonRef = useRef<HTMLButtonElement>(null);
99
+ // The button that last opened the panel (summary or a step detail), so Escape /
100
+ // Close returns focus to whoever opened it.
101
+ const lastSourceTriggerRef = useRef<HTMLButtonElement | null>(null);
102
+ const selectionControlled = selection !== undefined;
103
+ const sourceAvailable =
104
+ query.data?.sourceSnapshot !== null && query.data?.sourceSnapshot !== undefined;
105
+ const cancelMutation = useCancelWorkflowRunMutation(query.data);
106
+
107
+ useEffect(() => {
108
+ if (!sourceAvailable) {
109
+ setSourcePanelOpen(false);
110
+ setSourceFocus(null);
111
+ }
112
+ }, [sourceAvailable]);
113
+
114
+ // If a refetch drops the focused step or its location, degrade to whole-workflow
115
+ // focus so the panel never points at an unmounted Source button.
116
+ useEffect(() => {
117
+ if (!sourceFocus) return;
118
+ const stillLocated = query.data?.jobs.some((job) =>
119
+ job.jobExecutions.some((execution) =>
120
+ execution.steps.some((step) => step.id === sourceFocus.stepId && step.sourceLocation),
121
+ ),
122
+ );
123
+ if (!stillLocated) {
124
+ setSourceFocus(null);
125
+ lastSourceTriggerRef.current = sourceButtonRef.current;
126
+ }
127
+ }, [sourceFocus, query.data]);
128
+
129
+ if (query.isPending) return <WorkflowRunSkeleton />;
130
+
131
+ // Only show the full error placeholder when nothing ever loaded. A refetch that fails after
132
+ // a prior success keeps the loaded run on screen (see below) instead of wiping the pane,
133
+ // since active-run polling can hit a transient API error.
134
+ if (query.isError && query.data === undefined) {
135
+ if (query.error instanceof ApiError && query.error.status === 404) {
136
+ return <WorkflowRunNotFound />;
137
+ }
138
+ return <QueryLoadError query={query} subject="workflow run" icon="pulseLine" />;
139
+ }
140
+
141
+ if (query.data === undefined) return <WorkflowRunSkeleton />;
142
+
143
+ const runData = query.data;
144
+ const resolvedSelection = selectionControlled
145
+ ? resolveWorkflowRunSelection({run: runData, selection})
146
+ : undefined;
147
+ const selectedJob = selectionControlled
148
+ ? resolvedSelection?.job
149
+ : (runData.jobs.find((job) => job.id === selectedJobId) ?? runData.jobs.at(0));
150
+ const selectedJobExecution = selectionControlled
151
+ ? resolvedSelection?.jobExecution
152
+ : selectedJob
153
+ ? resolveJobExecution(selectedJob, selectedJobExecutionId)
154
+ : undefined;
155
+ const selectedAttemptId = selectionControlled
156
+ ? (resolvedSelection?.selectedAttemptId ?? null)
157
+ : undefined;
158
+ // Explicit per-step focus wins; fall back to the URL-selected step so deep links
159
+ // still pre-highlight when the summary opens the whole-workflow source.
160
+ const highlightedLineRange =
161
+ sourceFocus?.location ?? resolvedSelection?.step?.sourceLocation ?? null;
162
+ const sourceSnapshot = runData.sourceSnapshot;
163
+ async function rerun(mode: WorkflowRunRerunModeDto) {
164
+ try {
165
+ const run = await rerunMutation.mutateAsync({workflowRunId: runData.id, mode});
166
+ toast.success('Re-run started');
167
+ await navigate({
168
+ to: '/workspaces/$wid/projects/$pid/runs/$workflowRunId',
169
+ params: {wid: workspaceId, pid: projectId, workflowRunId: run.id},
170
+ search: ((previous: Record<string, unknown>) =>
171
+ withoutWorkflowRunSelectionSearch(previous)) as never,
172
+ });
173
+ } catch (error) {
174
+ toast.error(error instanceof ApiError ? error.message : 'Could not start re-run');
175
+ }
176
+ }
177
+
178
+ function selectJob(jobId: string | undefined) {
179
+ if (!selectionControlled) {
180
+ setSelectedJobId(jobId);
181
+ setSelectedJobExecutionId(undefined);
182
+ return;
183
+ }
184
+
185
+ onSelectionChange?.(
186
+ jobId ? {jobId, runAttempt: selection?.runAttempt} : {runAttempt: selection?.runAttempt},
187
+ );
188
+ }
189
+
190
+ function selectJobExecution(jobExecutionId: string | undefined) {
191
+ if (!selectionControlled) {
192
+ setSelectedJobExecutionId(jobExecutionId);
193
+ return;
194
+ }
195
+ if (!selectedJob) return;
196
+
197
+ onSelectionChange?.({
198
+ jobId: selectedJob.id,
199
+ jobExecutionId,
200
+ runAttempt: selection?.runAttempt,
201
+ });
202
+ }
203
+
204
+ function selectAttempt(attemptId: string | undefined) {
205
+ if (!selectionControlled || !selectedJob || !selectedJobExecution) return;
206
+
207
+ if (!attemptId) {
208
+ onSelectionChange?.({
209
+ jobId: selectedJob.id,
210
+ jobExecutionId: selectedJobExecution.id,
211
+ runAttempt: selection?.runAttempt,
212
+ });
213
+ return;
214
+ }
215
+
216
+ const match = findAttemptSelection(selectedJobExecution, attemptId);
217
+ if (!match) return;
218
+
219
+ onSelectionChange?.({
220
+ jobId: selectedJob.id,
221
+ jobExecutionId: selectedJobExecution.id,
222
+ stepId: match.stepId,
223
+ stepAttemptId: match.attemptId,
224
+ runAttempt: selection?.runAttempt,
225
+ });
226
+ }
227
+
228
+ function openWholeWorkflowSource() {
229
+ setSourceFocus(null);
230
+ lastSourceTriggerRef.current = sourceButtonRef.current;
231
+ setSourcePanelOpen(true);
232
+ }
233
+
234
+ function toggleWholeWorkflowSource() {
235
+ if (sourcePanelOpen && sourceFocus === null) {
236
+ closeSourcePanel();
237
+ return;
238
+ }
239
+ openWholeWorkflowSource();
240
+ }
241
+
242
+ function openStepSource(
243
+ stepId: string,
244
+ location: StepSourceLocation,
245
+ trigger: HTMLButtonElement | null,
246
+ ) {
247
+ setSourceFocus({stepId, location});
248
+ lastSourceTriggerRef.current = trigger;
249
+ setSourcePanelOpen(true);
250
+ }
251
+
252
+ function closeSourcePanel() {
253
+ const trigger = lastSourceTriggerRef.current;
254
+ const fallbackTrigger = sourceButtonRef.current;
255
+ setSourcePanelOpen(false);
256
+ // Defer so focus lands after the panel unmounts; clear the focus only after
257
+ // focusing so the opener button is still expanded (force-visible) on return.
258
+ window.setTimeout(() => {
259
+ const focusTarget = trigger?.isConnected ? trigger : fallbackTrigger;
260
+ focusTarget?.focus();
261
+ setSourceFocus(null);
262
+ }, 0);
263
+ }
264
+
265
+ function cancelRun() {
266
+ cancelMutation.mutate(undefined, {
267
+ onError: (error) => {
268
+ toast.error(cancelErrorMessage(error));
269
+ },
270
+ });
271
+ }
272
+
273
+ return (
274
+ <>
275
+ <div className="flex min-w-0 flex-1 flex-col">
276
+ <WorkflowRunSummary
277
+ workspaceId={workspaceId}
278
+ projectId={projectId}
279
+ run={runData}
280
+ sourceAvailable={sourceAvailable}
281
+ sourceOpen={sourcePanelOpen && sourceFocus === null}
282
+ sourcePanelId={sourcePanelId}
283
+ sourceButtonRef={sourceButtonRef}
284
+ onSourceToggle={toggleWholeWorkflowSource}
285
+ cancelling={cancelMutation.isPending}
286
+ onCancel={cancelRun}
287
+ rerunPending={rerunMutation.isPending}
288
+ onRerun={(mode) => void rerun(mode)}
289
+ latestAttempt={runData.latestAttempt}
290
+ />
291
+ {query.isError ? <WorkflowRunStaleError query={query} /> : null}
292
+ <div className="min-h-0 flex-1 overflow-auto bg-background-neutral-base p-16">
293
+ <div className="mx-auto flex w-full max-w-[1280px] flex-col gap-16">
294
+ <JobGraph
295
+ run={runData}
296
+ selectedJobId={selectedJob?.id}
297
+ onSelectedJobChange={selectJob}
298
+ />
299
+ {selectedJob ? (
300
+ <JobCard
301
+ workspaceId={workspaceId}
302
+ job={selectedJob}
303
+ selectedJobExecution={selectedJobExecution}
304
+ selectedAttemptId={selectedJob.carriedOver ? undefined : selectedAttemptId}
305
+ onSelectedJobExecutionChange={selectJobExecution}
306
+ onSelectedAttemptChange={selectionControlled ? selectAttempt : undefined}
307
+ sourcePanelId={sourcePanelId}
308
+ sourceAvailable={sourceAvailable}
309
+ focusedSourceStepId={sourceFocus?.stepId ?? null}
310
+ onOpenStepSource={openStepSource}
311
+ />
312
+ ) : null}
313
+ </div>
314
+ </div>
315
+ </div>
316
+ <WorkflowSourcePanel
317
+ id={sourcePanelId}
318
+ source={sourceSnapshot}
319
+ open={sourcePanelOpen && sourceAvailable}
320
+ onClose={closeSourcePanel}
321
+ highlightedLineRange={highlightedLineRange}
322
+ scrollHighlightedIntoView
323
+ />
324
+ </>
325
+ );
326
+ }
327
+
328
+ function cancelErrorMessage(error: unknown): string {
329
+ if (error instanceof ApiError && error.code === 'run-already-finished') {
330
+ return 'This workflow run has already finished.';
331
+ }
332
+ return 'Could not cancel workflow run.';
333
+ }
334
+
335
+ function findAttemptSelection(jobExecution: JobExecution, attemptId: string) {
336
+ for (const step of jobExecution.steps) {
337
+ const attempt = step.attempts.find((candidate) => candidate.id === attemptId);
338
+ if (attempt) return {stepId: step.id, attemptId: attempt.id};
339
+ }
340
+ return undefined;
341
+ }
@@ -0,0 +1 @@
1
+ export * from './workflow-source-panel.js';
@@ -0,0 +1,110 @@
1
+ import {argosScreenshot} from '@argos-ci/storybook/vitest';
2
+ import type {Meta, StoryObj} from '@storybook/react';
3
+ import {screen, waitFor} from 'storybook/test';
4
+ import {WorkflowSourcePanel} from './workflow-source-panel.js';
5
+
6
+ const meta = {
7
+ title: 'Workflows/SourcePanel',
8
+ component: WorkflowSourcePanel,
9
+ parameters: {
10
+ layout: 'fullscreen',
11
+ argos: {
12
+ modes: {
13
+ light: {theme: 'light'},
14
+ dark: {theme: 'dark'},
15
+ },
16
+ },
17
+ },
18
+ decorators: [
19
+ (Story) => (
20
+ <div className="flex min-h-screen justify-end bg-background-neutral-base">
21
+ <Story />
22
+ </div>
23
+ ),
24
+ ],
25
+ args: {
26
+ id: 'workflow-source-panel',
27
+ open: true,
28
+ source: {
29
+ format: 'yaml',
30
+ content: `name: deploy-web
31
+ on:
32
+ workflow_dispatch:
33
+
34
+ jobs:
35
+ build:
36
+ runs-on: ubuntu-latest
37
+ steps:
38
+ - uses: actions/checkout@v4
39
+ - run: pnpm install --frozen-lockfile
40
+ - run: pnpm test --filter=@shipfox/client-workflows`,
41
+ },
42
+ highlightedLineRange: {startLine: 10, endLine: 11},
43
+ onClose: () => undefined,
44
+ },
45
+ } satisfies Meta<typeof WorkflowSourcePanel>;
46
+
47
+ export default meta;
48
+ type Story = StoryObj<typeof meta>;
49
+ type WorkflowSourcePanelStoryContext = Parameters<NonNullable<Story['play']>>[0];
50
+
51
+ async function captureHighlightedSourcePanel(
52
+ ctx: WorkflowSourcePanelStoryContext,
53
+ screenshotName: string,
54
+ ) {
55
+ await screen.findByRole('dialog', {name: 'Workflow source'});
56
+ await waitFor(
57
+ () => {
58
+ if (!document.querySelector('.shiki-override')) {
59
+ throw new Error('Shiki highlighting has not rendered yet');
60
+ }
61
+ },
62
+ {timeout: 10_000},
63
+ );
64
+ await argosScreenshot(ctx, screenshotName);
65
+ }
66
+
67
+ export const Playground: Story = {
68
+ play: async (ctx) => {
69
+ await captureHighlightedSourcePanel(ctx, 'Workflow Source Panel Open');
70
+ },
71
+ };
72
+
73
+ export const LongSource: Story = {
74
+ play: async (ctx) => {
75
+ await captureHighlightedSourcePanel(ctx, 'Workflow Source Panel Long Source');
76
+ },
77
+ args: {
78
+ source: {
79
+ format: 'yaml',
80
+ content: Array.from(
81
+ {length: 24},
82
+ (_, index) => ` - run: pnpm test --filter=@shipfox/package-${index + 1}`,
83
+ ).join('\n'),
84
+ },
85
+ },
86
+ };
87
+
88
+ export const HighlightedDeepInFile: Story = {
89
+ play: async (ctx) => {
90
+ await captureHighlightedSourcePanel(ctx, 'Workflow Source Panel Highlighted Deep In File');
91
+ },
92
+ args: {
93
+ source: {
94
+ format: 'yaml',
95
+ content: Array.from(
96
+ {length: 44},
97
+ (_, index) => ` - run: pnpm test --filter=@shipfox/package-${index + 1}`,
98
+ ).join('\n'),
99
+ },
100
+ highlightedLineRange: {startLine: 34, endLine: 36},
101
+ scrollHighlightedIntoView: true,
102
+ },
103
+ };
104
+
105
+ export const MissingSource: Story = {
106
+ args: {
107
+ open: false,
108
+ source: null,
109
+ },
110
+ };
@@ -0,0 +1,96 @@
1
+ import {render, screen, waitFor, within} from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import {WorkflowSourcePanel} from './workflow-source-panel.js';
4
+
5
+ describe('WorkflowSourcePanel', () => {
6
+ test('renders workflow source in a labelled dialog', async () => {
7
+ renderPanel({open: true});
8
+
9
+ const dialog = await screen.findByRole('dialog', {name: 'Workflow source'});
10
+
11
+ expect(dialog).toHaveAttribute('id', 'workflow-source-panel');
12
+ expect(within(dialog).getByText('workflow.yaml')).toBeInTheDocument();
13
+ await waitFor(() => expect(dialog).toHaveTextContent('jobs:'));
14
+ expect(within(dialog).getByRole('button', {name: 'Copy to clipboard'})).toBeInTheDocument();
15
+ expect(within(dialog).getByRole('button', {name: 'Close source'})).toBeInTheDocument();
16
+ });
17
+
18
+ test('closes with Escape while focus is inside the sheet', async () => {
19
+ const user = userEvent.setup();
20
+ const onClose = vi.fn();
21
+ renderPanel({open: true, onClose});
22
+
23
+ (await screen.findByRole('button', {name: 'Close source'})).focus();
24
+ await user.keyboard('{Escape}');
25
+
26
+ expect(onClose).toHaveBeenCalledTimes(1);
27
+ });
28
+
29
+ test('closes from the close button through the sheet close path', async () => {
30
+ const user = userEvent.setup();
31
+ const onClose = vi.fn();
32
+ renderPanel({open: true, onClose});
33
+
34
+ await user.click(await screen.findByRole('button', {name: 'Close source'}));
35
+
36
+ expect(onClose).toHaveBeenCalledTimes(1);
37
+ });
38
+
39
+ test('does not expose sheet controls while closed', () => {
40
+ renderPanel({open: false});
41
+
42
+ expect(screen.queryByRole('dialog', {name: 'Workflow source'})).not.toBeInTheDocument();
43
+ expect(screen.queryByRole('button', {name: 'Close source'})).not.toBeInTheDocument();
44
+ });
45
+
46
+ test('highlights the selected source line range', async () => {
47
+ renderPanel({
48
+ open: true,
49
+ highlightedLineRange: {startLine: 2, endLine: 3},
50
+ });
51
+
52
+ await screen.findByRole('dialog', {name: 'Workflow source'});
53
+
54
+ const highlightedLines = document.body.querySelectorAll('.line.highlighted-line');
55
+ expect(highlightedLines).toHaveLength(2);
56
+ expect(highlightedLines[0]).toHaveTextContent('build:');
57
+ expect(highlightedLines[1]).toHaveTextContent('steps: []');
58
+ });
59
+
60
+ test('renders source without highlighted lines when no range is provided', async () => {
61
+ renderPanel({open: true});
62
+
63
+ await screen.findByRole('dialog', {name: 'Workflow source'});
64
+
65
+ expect(document.body.querySelector('.line.highlighted-line')).toBeNull();
66
+ });
67
+
68
+ test('does not render a sheet without source content', () => {
69
+ renderPanel({open: true, source: null});
70
+
71
+ expect(screen.queryByRole('dialog', {name: 'Workflow source'})).not.toBeInTheDocument();
72
+ expect(screen.queryByRole('button', {name: 'Close source'})).not.toBeInTheDocument();
73
+ });
74
+ });
75
+
76
+ function renderPanel({
77
+ open,
78
+ source = {format: 'yaml', content: 'jobs:\n build:\n steps: []'},
79
+ onClose = vi.fn(),
80
+ highlightedLineRange,
81
+ }: {
82
+ open: boolean;
83
+ source?: Parameters<typeof WorkflowSourcePanel>[0]['source'];
84
+ onClose?: () => void;
85
+ highlightedLineRange?: Parameters<typeof WorkflowSourcePanel>[0]['highlightedLineRange'];
86
+ }) {
87
+ return render(
88
+ <WorkflowSourcePanel
89
+ id="workflow-source-panel"
90
+ open={open}
91
+ source={source}
92
+ onClose={onClose}
93
+ highlightedLineRange={highlightedLineRange}
94
+ />,
95
+ );
96
+ }
@@ -0,0 +1,136 @@
1
+ import {Button} from '@shipfox/react-ui/button';
2
+ import {
3
+ CodeBlock,
4
+ CodeBlockBody,
5
+ CodeBlockContent,
6
+ CodeBlockCopyButton,
7
+ CodeBlockFilename,
8
+ CodeBlockFiles,
9
+ CodeBlockHeader,
10
+ type CodeBlockHighlightedLineRange,
11
+ CodeBlockItem,
12
+ } from '@shipfox/react-ui/code-block';
13
+ import {Sheet, SheetClose, SheetContent, SheetTitle} from '@shipfox/react-ui/sheet';
14
+ import {cn} from '@shipfox/react-ui/utils';
15
+ import type {WorkflowSourceSnapshot} from '#core/workflow-run.js';
16
+
17
+ const WORKFLOW_SOURCE_FILENAME = 'workflow.yaml';
18
+ const WORKFLOW_SOURCE_CODE_THEMES = {
19
+ light: 'vitesse-dark',
20
+ dark: 'vitesse-dark',
21
+ };
22
+
23
+ export interface WorkflowSourcePanelProps {
24
+ id: string;
25
+ source: WorkflowSourceSnapshot | null;
26
+ open: boolean;
27
+ onClose: () => void;
28
+ highlightedLineRange?: CodeBlockHighlightedLineRange | null | undefined;
29
+ scrollHighlightedIntoView?: boolean | undefined;
30
+ className?: string | undefined;
31
+ }
32
+
33
+ export function WorkflowSourcePanel({
34
+ id,
35
+ source,
36
+ open,
37
+ onClose,
38
+ highlightedLineRange,
39
+ scrollHighlightedIntoView,
40
+ className,
41
+ }: WorkflowSourcePanelProps) {
42
+ const sheetOpen = open && source !== null;
43
+
44
+ if (!sheetOpen) return null;
45
+
46
+ return (
47
+ <Sheet
48
+ open
49
+ onOpenChange={(nextOpen) => {
50
+ if (!nextOpen) onClose();
51
+ }}
52
+ >
53
+ {source ? (
54
+ <SheetContent
55
+ id={id}
56
+ side="right"
57
+ aria-describedby={undefined}
58
+ className={cn(
59
+ 'w-screen max-w-none border-l-0 bg-background-contrast-base p-0 shadow-none sm:w-[min(85vw,1120px)] sm:max-w-none [&_.shadow-separator-inset]:shadow-none',
60
+ className,
61
+ )}
62
+ >
63
+ <SheetTitle className="sr-only">Workflow source</SheetTitle>
64
+ <WorkflowSourcePanelContent
65
+ source={source}
66
+ highlightedLineRange={highlightedLineRange}
67
+ scrollHighlightedIntoView={scrollHighlightedIntoView}
68
+ />
69
+ </SheetContent>
70
+ ) : null}
71
+ </Sheet>
72
+ );
73
+ }
74
+
75
+ function WorkflowSourcePanelContent({
76
+ source,
77
+ highlightedLineRange,
78
+ scrollHighlightedIntoView,
79
+ }: {
80
+ source: WorkflowSourceSnapshot;
81
+ highlightedLineRange: CodeBlockHighlightedLineRange | null | undefined;
82
+ scrollHighlightedIntoView: boolean | undefined;
83
+ }) {
84
+ const data = [
85
+ {
86
+ language: 'yaml',
87
+ filename: WORKFLOW_SOURCE_FILENAME,
88
+ code: source.content,
89
+ },
90
+ ];
91
+
92
+ return (
93
+ <CodeBlock
94
+ data={data}
95
+ className="flex size-full flex-col rounded-none bg-background-contrast-base shadow-none"
96
+ >
97
+ <CodeBlockHeader className="shrink-0 border-b border-border-contrast-base bg-background-contrast-base">
98
+ <CodeBlockFiles>
99
+ {(item) => <CodeBlockFilename value={item.filename}>{item.filename}</CodeBlockFilename>}
100
+ </CodeBlockFiles>
101
+ <CodeBlockCopyButton />
102
+ <SheetClose asChild>
103
+ <Button
104
+ type="button"
105
+ variant="transparentMuted"
106
+ size="sm"
107
+ iconLeft="close"
108
+ aria-label="Close source"
109
+ />
110
+ </SheetClose>
111
+ </CodeBlockHeader>
112
+ <CodeBlockBody className="flex min-h-0 flex-1 overflow-auto scrollbar">
113
+ {(item) => (
114
+ <CodeBlockItem
115
+ value={item.filename}
116
+ className={cn(
117
+ 'min-h-full px-0 pb-0',
118
+ '[&>div]:rounded-none [&>div]:border-0 [&>div]:bg-background-contrast-base [&>div]:dark:bg-background-contrast-base',
119
+ '[&_code]:!text-sm [&_code]:!text-foreground-neutral-on-inverted [&_.line]:!text-sm [&_.line]:before:!text-sm [&_.line]:before:!text-foreground-neutral-muted',
120
+ )}
121
+ >
122
+ <CodeBlockContent
123
+ language="yaml"
124
+ themes={WORKFLOW_SOURCE_CODE_THEMES}
125
+ syntaxHighlighting
126
+ highlightedLineRange={highlightedLineRange}
127
+ scrollHighlightedIntoView={scrollHighlightedIntoView}
128
+ >
129
+ {item.code}
130
+ </CodeBlockContent>
131
+ </CodeBlockItem>
132
+ )}
133
+ </CodeBlockBody>
134
+ </CodeBlock>
135
+ );
136
+ }