@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,107 @@
1
+ export function buildJobGraphModel({ run }) {
2
+ const sortedJobs = [
3
+ ...run.jobs
4
+ ].sort(compareJobs);
5
+ const byKey = new Map(sortedJobs.map((job)=>[
6
+ job.key,
7
+ job
8
+ ]));
9
+ const columnMemo = new Map();
10
+ function columnFor(job, visiting = new Set()) {
11
+ const cached = columnMemo.get(job.id);
12
+ if (cached !== undefined) return cached;
13
+ if (visiting.has(job.id)) return 0;
14
+ const nextVisiting = new Set(visiting);
15
+ nextVisiting.add(job.id);
16
+ const dependencyColumns = job.dependencies.map((dependencyKey)=>byKey.get(dependencyKey)).filter((dependency)=>dependency !== undefined).map((dependency)=>columnFor(dependency, nextVisiting));
17
+ const column = dependencyColumns.length === 0 ? 0 : Math.max(...dependencyColumns) + 1;
18
+ columnMemo.set(job.id, column);
19
+ return column;
20
+ }
21
+ const nodesWithoutRows = sortedJobs.map((job)=>jobGraphNode(job, {
22
+ column: columnFor(job),
23
+ row: 0,
24
+ currentDependencyCount: currentDependencyCount(job, byKey)
25
+ }));
26
+ const grouped = groupColumns(nodesWithoutRows);
27
+ const nodes = grouped.flat();
28
+ const edges = buildEdges(sortedJobs, byKey);
29
+ return {
30
+ nodes,
31
+ edges,
32
+ columns: grouped
33
+ };
34
+ }
35
+ function compareJobs(left, right) {
36
+ return left.position - right.position || (left.name ?? left.key).localeCompare(right.name ?? right.key) || left.id.localeCompare(right.id);
37
+ }
38
+ function groupColumns(nodes) {
39
+ const byColumn = new Map();
40
+ for (const node of nodes){
41
+ const column = byColumn.get(node.column) ?? [];
42
+ column.push(node);
43
+ byColumn.set(node.column, column);
44
+ }
45
+ return [
46
+ ...byColumn.entries()
47
+ ].sort(([left], [right])=>left - right).map(([, column])=>column.sort((left, right)=>left.position - right.position || (left.name ?? left.key).localeCompare(right.name ?? right.key) || left.id.localeCompare(right.id)).map((node, row)=>jobGraphNode(node, {
48
+ ...node,
49
+ row
50
+ })));
51
+ }
52
+ function buildEdges(jobs, byKey) {
53
+ const triggerEdges = jobs.filter((job)=>job.dependencies.length === 0).map((job)=>({
54
+ id: `trigger:${job.id}`,
55
+ from: 'trigger',
56
+ to: job.id,
57
+ kind: 'trigger'
58
+ }));
59
+ const dependencyEdges = jobs.flatMap((job)=>job.dependencies.flatMap((dependencyKey)=>{
60
+ const dependency = byKey.get(dependencyKey);
61
+ if (!dependency) return [];
62
+ return [
63
+ {
64
+ id: `${dependency.id}:${job.id}`,
65
+ from: dependency.id,
66
+ to: job.id,
67
+ kind: 'dependency'
68
+ }
69
+ ];
70
+ }));
71
+ return [
72
+ ...triggerEdges,
73
+ ...dependencyEdges
74
+ ];
75
+ }
76
+ function currentDependencyCount(job, byKey) {
77
+ return job.dependencies.filter((dependencyKey)=>{
78
+ const dependency = byKey.get(dependencyKey);
79
+ return dependency?.status === 'pending' || dependency?.status === 'running';
80
+ }).length;
81
+ }
82
+ function jobGraphNode(job, layout) {
83
+ return Object.assign(Object.create(Object.getPrototypeOf(job)), job, layout);
84
+ }
85
+ export function nextJobGraphNodeId({ model, currentNodeId, key }) {
86
+ const current = model.nodes.find((node)=>node.id === currentNodeId);
87
+ if (!current) return undefined;
88
+ switch(key){
89
+ case 'ArrowRight':
90
+ return nodeInAdjacentColumn(model, current, 1)?.id;
91
+ case 'ArrowLeft':
92
+ return nodeInAdjacentColumn(model, current, -1)?.id;
93
+ case 'ArrowDown':
94
+ case 'j':
95
+ return model.columns[current.column]?.[current.row + 1]?.id;
96
+ case 'ArrowUp':
97
+ case 'k':
98
+ return model.columns[current.column]?.[current.row - 1]?.id;
99
+ }
100
+ }
101
+ function nodeInAdjacentColumn(model, current, offset) {
102
+ const column = model.columns[current.column + offset];
103
+ if (!column || column.length === 0) return undefined;
104
+ return column[Math.min(current.row, column.length - 1)];
105
+ }
106
+
107
+ //# sourceMappingURL=graph-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/job-graph/graph-model.ts"],"sourcesContent":["import type {Job, WorkflowRunDetail} from '#core/workflow-run.js';\n\nexport type JobGraphNode = Job & {\n column: number;\n row: number;\n currentDependencyCount: number;\n};\n\nexport type JobGraphNavigationKey =\n | 'ArrowRight'\n | 'ArrowLeft'\n | 'ArrowDown'\n | 'ArrowUp'\n | 'j'\n | 'k';\n\nexport interface JobGraphEdge {\n id: string;\n from: string;\n to: string;\n kind: 'trigger' | 'dependency';\n}\n\nexport interface JobGraphModel {\n nodes: JobGraphNode[];\n edges: JobGraphEdge[];\n columns: JobGraphNode[][];\n}\n\nexport function buildJobGraphModel({run}: {run: WorkflowRunDetail}): JobGraphModel {\n const sortedJobs = [...run.jobs].sort(compareJobs);\n const byKey = new Map(sortedJobs.map((job) => [job.key, job]));\n const columnMemo = new Map<string, number>();\n\n function columnFor(job: Job, visiting = new Set<string>()): number {\n const cached = columnMemo.get(job.id);\n if (cached !== undefined) return cached;\n if (visiting.has(job.id)) return 0;\n\n const nextVisiting = new Set(visiting);\n nextVisiting.add(job.id);\n\n const dependencyColumns = job.dependencies\n .map((dependencyKey) => byKey.get(dependencyKey))\n .filter((dependency): dependency is Job => dependency !== undefined)\n .map((dependency) => columnFor(dependency, nextVisiting));\n\n const column = dependencyColumns.length === 0 ? 0 : Math.max(...dependencyColumns) + 1;\n columnMemo.set(job.id, column);\n return column;\n }\n\n const nodesWithoutRows = sortedJobs.map((job) =>\n jobGraphNode(job, {\n column: columnFor(job),\n row: 0,\n currentDependencyCount: currentDependencyCount(job, byKey),\n }),\n );\n\n const grouped = groupColumns(nodesWithoutRows);\n const nodes = grouped.flat();\n const edges = buildEdges(sortedJobs, byKey);\n\n return {\n nodes,\n edges,\n columns: grouped,\n };\n}\n\nfunction compareJobs(left: Job, right: Job): number {\n return (\n left.position - right.position ||\n (left.name ?? left.key).localeCompare(right.name ?? right.key) ||\n left.id.localeCompare(right.id)\n );\n}\n\nfunction groupColumns(nodes: JobGraphNode[]): JobGraphNode[][] {\n const byColumn = new Map<number, JobGraphNode[]>();\n for (const node of nodes) {\n const column = byColumn.get(node.column) ?? [];\n column.push(node);\n byColumn.set(node.column, column);\n }\n\n return [...byColumn.entries()]\n .sort(([left], [right]) => left - right)\n .map(([, column]) =>\n column\n .sort(\n (left, right) =>\n left.position - right.position ||\n (left.name ?? left.key).localeCompare(right.name ?? right.key) ||\n left.id.localeCompare(right.id),\n )\n .map((node, row) => jobGraphNode(node, {...node, row})),\n );\n}\n\nfunction buildEdges(jobs: readonly Job[], byKey: ReadonlyMap<string, Job>): JobGraphEdge[] {\n const triggerEdges = jobs\n .filter((job) => job.dependencies.length === 0)\n .map((job) => ({\n id: `trigger:${job.id}`,\n from: 'trigger',\n to: job.id,\n kind: 'trigger' as const,\n }));\n\n const dependencyEdges = jobs.flatMap((job) =>\n job.dependencies.flatMap((dependencyKey) => {\n const dependency = byKey.get(dependencyKey);\n if (!dependency) return [];\n return [\n {\n id: `${dependency.id}:${job.id}`,\n from: dependency.id,\n to: job.id,\n kind: 'dependency' as const,\n },\n ];\n }),\n );\n\n return [...triggerEdges, ...dependencyEdges];\n}\n\nfunction currentDependencyCount(job: Job, byKey: ReadonlyMap<string, Job>): number {\n return job.dependencies.filter((dependencyKey) => {\n const dependency = byKey.get(dependencyKey);\n return dependency?.status === 'pending' || dependency?.status === 'running';\n }).length;\n}\n\nfunction jobGraphNode(\n job: Job,\n layout: Pick<JobGraphNode, 'column' | 'row' | 'currentDependencyCount'>,\n): JobGraphNode {\n return Object.assign(Object.create(Object.getPrototypeOf(job)), job, layout);\n}\n\nexport function nextJobGraphNodeId({\n model,\n currentNodeId,\n key,\n}: {\n model: JobGraphModel;\n currentNodeId: string;\n key: JobGraphNavigationKey;\n}): string | undefined {\n const current = model.nodes.find((node) => node.id === currentNodeId);\n if (!current) return undefined;\n\n switch (key) {\n case 'ArrowRight':\n return nodeInAdjacentColumn(model, current, 1)?.id;\n case 'ArrowLeft':\n return nodeInAdjacentColumn(model, current, -1)?.id;\n case 'ArrowDown':\n case 'j':\n return model.columns[current.column]?.[current.row + 1]?.id;\n case 'ArrowUp':\n case 'k':\n return model.columns[current.column]?.[current.row - 1]?.id;\n }\n}\n\nfunction nodeInAdjacentColumn(\n model: JobGraphModel,\n current: JobGraphNode,\n offset: -1 | 1,\n): JobGraphNode | undefined {\n const column = model.columns[current.column + offset];\n if (!column || column.length === 0) return undefined;\n return column[Math.min(current.row, column.length - 1)];\n}\n"],"names":["buildJobGraphModel","run","sortedJobs","jobs","sort","compareJobs","byKey","Map","map","job","key","columnMemo","columnFor","visiting","Set","cached","get","id","undefined","has","nextVisiting","add","dependencyColumns","dependencies","dependencyKey","filter","dependency","column","length","Math","max","set","nodesWithoutRows","jobGraphNode","row","currentDependencyCount","grouped","groupColumns","nodes","flat","edges","buildEdges","columns","left","right","position","name","localeCompare","byColumn","node","push","entries","triggerEdges","from","to","kind","dependencyEdges","flatMap","status","layout","Object","assign","create","getPrototypeOf","nextJobGraphNodeId","model","currentNodeId","current","find","nodeInAdjacentColumn","offset","min"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { JobGraph } from './job-graph.js';
2
+ export type { JobGraphProps } from './types.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/job-graph/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,QAAQ,EAAC,MAAM,gBAAgB,CAAC;AACxC,YAAY,EAAC,aAAa,EAAC,MAAM,YAAY,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { JobGraph } from './job-graph.js';
2
+
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/job-graph/index.ts"],"sourcesContent":["export {JobGraph} from './job-graph.js';\nexport type {JobGraphProps} from './types.js';\n"],"names":["JobGraph"],"mappings":"AAAA,SAAQA,QAAQ,QAAO,iBAAiB"}
@@ -0,0 +1,4 @@
1
+ import type { JobDisplayDuration, JobExecutionTime } from '#core/workflow-run.js';
2
+ export declare function formatJobExecutionTimeLabel(time: JobExecutionTime): string;
3
+ export declare function formatJobDurationAccessibleLabel(duration: JobDisplayDuration | null): string | undefined;
4
+ //# sourceMappingURL=job-duration-format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job-duration-format.d.ts","sourceRoot":"","sources":["../../../src/components/job-graph/job-duration-format.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,kBAAkB,EAAE,gBAAgB,EAAC,MAAM,uBAAuB,CAAC;AAEhF,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,gBAAgB,GAAG,MAAM,CAW1E;AAED,wBAAgB,gCAAgC,CAC9C,QAAQ,EAAE,kBAAkB,GAAG,IAAI,GAClC,MAAM,GAAG,SAAS,CAcpB"}
@@ -0,0 +1,41 @@
1
+ import { humanDuration } from '@shipfox/react-ui/utils';
2
+ export function formatJobExecutionTimeLabel(time) {
3
+ switch(time.state){
4
+ case 'live':
5
+ return humanDuration(time.fromIso);
6
+ case 'fixed':
7
+ return formatFixedDurationLabel(time.elapsed);
8
+ default:
9
+ {
10
+ const exhaustive = time;
11
+ return exhaustive;
12
+ }
13
+ }
14
+ }
15
+ export function formatJobDurationAccessibleLabel(duration) {
16
+ if (duration === null) return undefined;
17
+ const label = formatJobExecutionTimeLabel(duration);
18
+ switch(duration.kind){
19
+ case 'queue':
20
+ return duration.state === 'live' ? `queueing ${label}` : `queued ${label}`;
21
+ case 'run':
22
+ return duration.state === 'live' ? `running ${label}` : `ran ${label}`;
23
+ default:
24
+ {
25
+ const exhaustive = duration;
26
+ return exhaustive;
27
+ }
28
+ }
29
+ }
30
+ function formatFixedDurationLabel({ years = 0, months = 0, weeks = 0, days = 0, hours = 0, minutes = 0, seconds = 0 }) {
31
+ const totalDays = years * 365 + months * 30 + weeks * 7 + days;
32
+ const totalHours = totalDays * 24 + hours;
33
+ if (totalHours > 0) return `${totalHours}h ${pad2(minutes)}m`;
34
+ if (minutes > 0) return `${minutes}m ${pad2(seconds)}s`;
35
+ return `${seconds}s`;
36
+ }
37
+ function pad2(value) {
38
+ return value.toString().padStart(2, '0');
39
+ }
40
+
41
+ //# sourceMappingURL=job-duration-format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/job-graph/job-duration-format.ts"],"sourcesContent":["import {humanDuration} from '@shipfox/react-ui/utils';\nimport type {JobDisplayDuration, JobExecutionTime} from '#core/workflow-run.js';\n\nexport function formatJobExecutionTimeLabel(time: JobExecutionTime): string {\n switch (time.state) {\n case 'live':\n return humanDuration(time.fromIso);\n case 'fixed':\n return formatFixedDurationLabel(time.elapsed);\n default: {\n const exhaustive: never = time;\n return exhaustive;\n }\n }\n}\n\nexport function formatJobDurationAccessibleLabel(\n duration: JobDisplayDuration | null,\n): string | undefined {\n if (duration === null) return undefined;\n\n const label = formatJobExecutionTimeLabel(duration);\n switch (duration.kind) {\n case 'queue':\n return duration.state === 'live' ? `queueing ${label}` : `queued ${label}`;\n case 'run':\n return duration.state === 'live' ? `running ${label}` : `ran ${label}`;\n default: {\n const exhaustive: never = duration;\n return exhaustive;\n }\n }\n}\n\nfunction formatFixedDurationLabel({\n years = 0,\n months = 0,\n weeks = 0,\n days = 0,\n hours = 0,\n minutes = 0,\n seconds = 0,\n}: Extract<JobExecutionTime, {state: 'fixed'}>['elapsed']): string {\n const totalDays = years * 365 + months * 30 + weeks * 7 + days;\n const totalHours = totalDays * 24 + hours;\n\n if (totalHours > 0) return `${totalHours}h ${pad2(minutes)}m`;\n if (minutes > 0) return `${minutes}m ${pad2(seconds)}s`;\n return `${seconds}s`;\n}\n\nfunction pad2(value: number): string {\n return value.toString().padStart(2, '0');\n}\n"],"names":["humanDuration","formatJobExecutionTimeLabel","time","state","fromIso","formatFixedDurationLabel","elapsed","exhaustive","formatJobDurationAccessibleLabel","duration","undefined","label","kind","years","months","weeks","days","hours","minutes","seconds","totalDays","totalHours","pad2","value","toString","padStart"],"mappings":"AAAA,SAAQA,aAAa,QAAO,0BAA0B;AAGtD,OAAO,SAASC,4BAA4BC,IAAsB;IAChE,OAAQA,KAAKC,KAAK;QAChB,KAAK;YACH,OAAOH,cAAcE,KAAKE,OAAO;QACnC,KAAK;YACH,OAAOC,yBAAyBH,KAAKI,OAAO;QAC9C;YAAS;gBACP,MAAMC,aAAoBL;gBAC1B,OAAOK;YACT;IACF;AACF;AAEA,OAAO,SAASC,iCACdC,QAAmC;IAEnC,IAAIA,aAAa,MAAM,OAAOC;IAE9B,MAAMC,QAAQV,4BAA4BQ;IAC1C,OAAQA,SAASG,IAAI;QACnB,KAAK;YACH,OAAOH,SAASN,KAAK,KAAK,SAAS,CAAC,SAAS,EAAEQ,OAAO,GAAG,CAAC,OAAO,EAAEA,OAAO;QAC5E,KAAK;YACH,OAAOF,SAASN,KAAK,KAAK,SAAS,CAAC,QAAQ,EAAEQ,OAAO,GAAG,CAAC,IAAI,EAAEA,OAAO;QACxE;YAAS;gBACP,MAAMJ,aAAoBE;gBAC1B,OAAOF;YACT;IACF;AACF;AAEA,SAASF,yBAAyB,EAChCQ,QAAQ,CAAC,EACTC,SAAS,CAAC,EACVC,QAAQ,CAAC,EACTC,OAAO,CAAC,EACRC,QAAQ,CAAC,EACTC,UAAU,CAAC,EACXC,UAAU,CAAC,EAC4C;IACvD,MAAMC,YAAYP,QAAQ,MAAMC,SAAS,KAAKC,QAAQ,IAAIC;IAC1D,MAAMK,aAAaD,YAAY,KAAKH;IAEpC,IAAII,aAAa,GAAG,OAAO,GAAGA,WAAW,EAAE,EAAEC,KAAKJ,SAAS,CAAC,CAAC;IAC7D,IAAIA,UAAU,GAAG,OAAO,GAAGA,QAAQ,EAAE,EAAEI,KAAKH,SAAS,CAAC,CAAC;IACvD,OAAO,GAAGA,QAAQ,CAAC,CAAC;AACtB;AAEA,SAASG,KAAKC,KAAa;IACzB,OAAOA,MAAMC,QAAQ,GAAGC,QAAQ,CAAC,GAAG;AACtC"}
@@ -0,0 +1,5 @@
1
+ import type { JobDisplayDuration } from '#core/workflow-run.js';
2
+ export declare function JobDurationLabel({ duration }: {
3
+ duration: JobDisplayDuration | null;
4
+ }): import("react").JSX.Element | null;
5
+ //# sourceMappingURL=job-duration-label.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job-duration-label.d.ts","sourceRoot":"","sources":["../../../src/components/job-graph/job-duration-label.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,uBAAuB,CAAC;AAG9D,wBAAgB,gBAAgB,CAAC,EAAC,QAAQ,EAAC,EAAE;IAAC,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAAA;CAAC,sCAQjF"}
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useTimeTick } from '@shipfox/react-ui/time-ticker';
3
+ import { Code } from '@shipfox/react-ui/typography';
4
+ import { humanDuration } from '@shipfox/react-ui/utils';
5
+ import { formatJobExecutionTimeLabel } from './job-duration-format.js';
6
+ export function JobDurationLabel({ duration }) {
7
+ if (duration === null) return null;
8
+ if (duration.state === 'live') {
9
+ return /*#__PURE__*/ _jsx(LiveDurationText, {
10
+ fromIso: duration.fromIso
11
+ });
12
+ }
13
+ return /*#__PURE__*/ _jsx(DurationText, {
14
+ children: formatJobExecutionTimeLabel(duration)
15
+ });
16
+ }
17
+ function LiveDurationText({ fromIso }) {
18
+ useTimeTick();
19
+ return /*#__PURE__*/ _jsx(DurationText, {
20
+ children: humanDuration(fromIso)
21
+ });
22
+ }
23
+ function DurationText({ children }) {
24
+ // The status icon names the state; the node aria-label carries the verb.
25
+ return /*#__PURE__*/ _jsx(Code, {
26
+ as: "span",
27
+ variant: "label",
28
+ className: "shrink-0 tabular-nums text-foreground-neutral-muted",
29
+ children: children
30
+ });
31
+ }
32
+
33
+ //# sourceMappingURL=job-duration-label.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/job-graph/job-duration-label.tsx"],"sourcesContent":["import {useTimeTick} from '@shipfox/react-ui/time-ticker';\nimport {Code} from '@shipfox/react-ui/typography';\nimport {humanDuration} from '@shipfox/react-ui/utils';\nimport type {JobDisplayDuration} from '#core/workflow-run.js';\nimport {formatJobExecutionTimeLabel} from './job-duration-format.js';\n\nexport function JobDurationLabel({duration}: {duration: JobDisplayDuration | null}) {\n if (duration === null) return null;\n\n if (duration.state === 'live') {\n return <LiveDurationText fromIso={duration.fromIso} />;\n }\n\n return <DurationText>{formatJobExecutionTimeLabel(duration)}</DurationText>;\n}\n\nfunction LiveDurationText({fromIso}: {fromIso: string}) {\n useTimeTick();\n return <DurationText>{humanDuration(fromIso)}</DurationText>;\n}\n\nfunction DurationText({children}: {children: string}) {\n // The status icon names the state; the node aria-label carries the verb.\n return (\n <Code as=\"span\" variant=\"label\" className=\"shrink-0 tabular-nums text-foreground-neutral-muted\">\n {children}\n </Code>\n );\n}\n"],"names":["useTimeTick","Code","humanDuration","formatJobExecutionTimeLabel","JobDurationLabel","duration","state","LiveDurationText","fromIso","DurationText","children","as","variant","className"],"mappings":";AAAA,SAAQA,WAAW,QAAO,gCAAgC;AAC1D,SAAQC,IAAI,QAAO,+BAA+B;AAClD,SAAQC,aAAa,QAAO,0BAA0B;AAEtD,SAAQC,2BAA2B,QAAO,2BAA2B;AAErE,OAAO,SAASC,iBAAiB,EAACC,QAAQ,EAAwC;IAChF,IAAIA,aAAa,MAAM,OAAO;IAE9B,IAAIA,SAASC,KAAK,KAAK,QAAQ;QAC7B,qBAAO,KAACC;YAAiBC,SAASH,SAASG,OAAO;;IACpD;IAEA,qBAAO,KAACC;kBAAcN,4BAA4BE;;AACpD;AAEA,SAASE,iBAAiB,EAACC,OAAO,EAAoB;IACpDR;IACA,qBAAO,KAACS;kBAAcP,cAAcM;;AACtC;AAEA,SAASC,aAAa,EAACC,QAAQ,EAAqB;IAClD,yEAAyE;IACzE,qBACE,KAACT;QAAKU,IAAG;QAAOC,SAAQ;QAAQC,WAAU;kBACvCH;;AAGP"}
@@ -0,0 +1,9 @@
1
+ import type { WorkflowRunDetail } from '#core/workflow-run.js';
2
+ import type { JobGraphModel } from './graph-model.js';
3
+ export declare function JobGraphContent({ model, trigger, selectedJobId, onSelectJob, }: {
4
+ model: JobGraphModel;
5
+ trigger: Pick<WorkflowRunDetail, 'triggerDisplayLabel' | 'triggerLabel' | 'triggerProvider' | 'triggerSource'>;
6
+ selectedJobId?: string | undefined;
7
+ onSelectJob: (jobId: string | undefined) => void;
8
+ }): import("react").JSX.Element;
9
+ //# sourceMappingURL=job-graph-content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job-graph-content.d.ts","sourceRoot":"","sources":["../../../src/components/job-graph/job-graph-content.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAC,aAAa,EAAwB,MAAM,kBAAkB,CAAC;AAW3E,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,OAAO,EACP,aAAa,EACb,WAAW,GACZ,EAAE;IACD,KAAK,EAAE,aAAa,CAAC;IACrB,OAAO,EAAE,IAAI,CACX,iBAAiB,EACjB,qBAAqB,GAAG,cAAc,GAAG,iBAAiB,GAAG,eAAe,CAC7E,CAAC;IACF,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;CAClD,+BAuFA"}
@@ -0,0 +1,181 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { EmptyState } from '@shipfox/react-ui/empty-state';
3
+ import { TimeTickerProvider } from '@shipfox/react-ui/time-ticker';
4
+ import { cn } from '@shipfox/react-ui/utils';
5
+ import { useRef, useState } from 'react';
6
+ import { nextJobGraphNodeId } from './graph-model.js';
7
+ import { JobNode, TriggerNode } from './job-node.js';
8
+ const NODE_WIDTH = 208;
9
+ const NODE_HEIGHT = 48;
10
+ const COLUMN_GAP = 64;
11
+ const ROW_GAP = 20;
12
+ const TRIGGER_WIDTH = 36;
13
+ const PADDING = 16;
14
+ export function JobGraphContent({ model, trigger, selectedJobId, onSelectJob }) {
15
+ const nodeRefs = useRef(new Map());
16
+ const [hoveredJobId, setHoveredJobId] = useState();
17
+ if (model.nodes.length === 0) {
18
+ return /*#__PURE__*/ _jsx(EmptyState, {
19
+ className: "min-h-160 px-16 py-24",
20
+ icon: "componentLine",
21
+ title: "No jobs yet",
22
+ description: "This run has not materialized jobs.",
23
+ variant: "compact"
24
+ });
25
+ }
26
+ const maxRows = Math.max(1, ...model.columns.map((column)=>column.length));
27
+ const contentWidth = PADDING * 2 + TRIGGER_WIDTH + COLUMN_GAP + model.columns.length * NODE_WIDTH + Math.max(0, model.columns.length - 1) * COLUMN_GAP;
28
+ const contentHeight = PADDING * 2 + maxRows * NODE_HEIGHT + Math.max(0, maxRows - 1) * ROW_GAP;
29
+ function setNodeRef(jobId) {
30
+ return (element)=>{
31
+ if (element) {
32
+ nodeRefs.current.set(jobId, element);
33
+ } else {
34
+ nodeRefs.current.delete(jobId);
35
+ }
36
+ };
37
+ }
38
+ function handleKeyDown(event) {
39
+ const key = navigationKey(event.key);
40
+ if (!key) return;
41
+ const currentNodeId = event.currentTarget.dataset.jobId;
42
+ if (!currentNodeId) return;
43
+ const nextNodeId = nextJobGraphNodeId({
44
+ model,
45
+ currentNodeId,
46
+ key
47
+ });
48
+ if (!nextNodeId) return;
49
+ event.preventDefault();
50
+ onSelectJob(nextNodeId);
51
+ nodeRefs.current.get(nextNodeId)?.focus();
52
+ }
53
+ return /*#__PURE__*/ _jsx(TimeTickerProvider, {
54
+ intervalMs: 1000,
55
+ reducedMotionIntervalMs: 10_000,
56
+ children: /*#__PURE__*/ _jsx("div", {
57
+ className: "min-h-0 overflow-auto bg-background-neutral-base",
58
+ children: /*#__PURE__*/ _jsxs("div", {
59
+ className: "relative",
60
+ style: {
61
+ width: contentWidth,
62
+ minHeight: contentHeight
63
+ },
64
+ children: [
65
+ /*#__PURE__*/ _jsx(GraphEdges, {
66
+ model: model,
67
+ hoveredJobId: hoveredJobId
68
+ }),
69
+ /*#__PURE__*/ _jsx("div", {
70
+ className: "absolute",
71
+ style: {
72
+ left: PADDING,
73
+ top: PADDING + (NODE_HEIGHT - TRIGGER_WIDTH) / 2
74
+ },
75
+ children: /*#__PURE__*/ _jsx(TriggerNode, {
76
+ trigger: trigger
77
+ })
78
+ }),
79
+ model.columns.map((column, columnIndex)=>/*#__PURE__*/ _jsx("div", {
80
+ className: "absolute flex flex-col gap-20",
81
+ style: {
82
+ left: jobLeft(columnIndex),
83
+ top: PADDING
84
+ },
85
+ children: column.map((node)=>/*#__PURE__*/ _jsx(JobNode, {
86
+ node: node,
87
+ selected: node.id === selectedJobId,
88
+ ref: setNodeRef(node.id),
89
+ onSelect: ()=>onSelectJob(node.id === selectedJobId ? undefined : node.id),
90
+ onKeyDown: handleKeyDown,
91
+ onHoverStart: ()=>setHoveredJobId(node.id),
92
+ onHoverEnd: ()=>setHoveredJobId((current)=>current === node.id ? undefined : current)
93
+ }, node.id))
94
+ }, column.map((node)=>node.id).join(':')))
95
+ ]
96
+ })
97
+ })
98
+ });
99
+ }
100
+ function navigationKey(key) {
101
+ if (key === 'ArrowRight' || key === 'ArrowLeft' || key === 'ArrowDown' || key === 'ArrowUp' || key === 'j' || key === 'k') {
102
+ return key;
103
+ }
104
+ return undefined;
105
+ }
106
+ function GraphEdges({ model, hoveredJobId }) {
107
+ const byId = new Map(model.nodes.map((node)=>[
108
+ node.id,
109
+ node
110
+ ]));
111
+ const edgeViews = model.edges.map((edge)=>{
112
+ const fromNode = byId.get(edge.from);
113
+ const toNode = byId.get(edge.to);
114
+ const from = edge.from === 'trigger' ? triggerPoint() : jobRightPoint(fromNode);
115
+ const to = jobLeftPoint(toNode);
116
+ if (!from || !to) return undefined;
117
+ return {
118
+ id: edge.id,
119
+ highlighted: edge.from === hoveredJobId || edge.to === hoveredJobId,
120
+ path: edgePath({
121
+ from,
122
+ fromNode,
123
+ to,
124
+ toNode
125
+ })
126
+ };
127
+ }).filter((edgeView)=>edgeView !== undefined).sort((left, right)=>Number(left.highlighted) - Number(right.highlighted));
128
+ return /*#__PURE__*/ _jsx("svg", {
129
+ className: "pointer-events-none absolute inset-0 size-full",
130
+ "aria-hidden": "true",
131
+ children: edgeViews.map((edge)=>{
132
+ return /*#__PURE__*/ _jsx("g", {
133
+ className: cn('text-foreground-neutral-muted transition-colors', edge.highlighted && 'text-foreground-neutral-base'),
134
+ children: /*#__PURE__*/ _jsx("path", {
135
+ "data-edge-id": edge.id,
136
+ d: edge.path,
137
+ fill: "none",
138
+ stroke: "currentColor",
139
+ strokeWidth: edge.highlighted ? 1.5 : 1
140
+ })
141
+ }, edge.id);
142
+ })
143
+ });
144
+ }
145
+ function edgePath({ from, fromNode, to, toNode }) {
146
+ if (fromNode && toNode && toNode.column - fromNode.column > 1 && from.y !== to.y) {
147
+ const targetLaneX = to.x - COLUMN_GAP / 2;
148
+ return `M ${from.x} ${from.y} H ${targetLaneX} V ${to.y} H ${to.x}`;
149
+ }
150
+ const midX = from.x + (to.x - from.x) / 2;
151
+ return `M ${from.x} ${from.y} H ${midX} V ${to.y} H ${to.x}`;
152
+ }
153
+ function triggerPoint() {
154
+ return {
155
+ x: PADDING + TRIGGER_WIDTH,
156
+ y: PADDING + NODE_HEIGHT / 2
157
+ };
158
+ }
159
+ function jobLeft(column) {
160
+ // Dependency depths are contiguous, so a node's depth matches its rendered column index.
161
+ return PADDING + TRIGGER_WIDTH + COLUMN_GAP + column * (NODE_WIDTH + COLUMN_GAP);
162
+ }
163
+ function jobTop(row) {
164
+ return PADDING + row * (NODE_HEIGHT + ROW_GAP);
165
+ }
166
+ function jobLeftPoint(node) {
167
+ if (!node) return undefined;
168
+ return {
169
+ x: jobLeft(node.column),
170
+ y: jobTop(node.row) + NODE_HEIGHT / 2
171
+ };
172
+ }
173
+ function jobRightPoint(node) {
174
+ if (!node) return undefined;
175
+ return {
176
+ x: jobLeft(node.column) + NODE_WIDTH,
177
+ y: jobTop(node.row) + NODE_HEIGHT / 2
178
+ };
179
+ }
180
+
181
+ //# sourceMappingURL=job-graph-content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/job-graph/job-graph-content.tsx"],"sourcesContent":["import {EmptyState} from '@shipfox/react-ui/empty-state';\nimport {TimeTickerProvider} from '@shipfox/react-ui/time-ticker';\nimport {cn} from '@shipfox/react-ui/utils';\nimport type {KeyboardEvent} from 'react';\nimport {useRef, useState} from 'react';\nimport type {WorkflowRunDetail} from '#core/workflow-run.js';\nimport type {JobGraphModel, JobGraphNavigationKey} from './graph-model.js';\nimport {nextJobGraphNodeId} from './graph-model.js';\nimport {JobNode, TriggerNode} from './job-node.js';\n\nconst NODE_WIDTH = 208;\nconst NODE_HEIGHT = 48;\nconst COLUMN_GAP = 64;\nconst ROW_GAP = 20;\nconst TRIGGER_WIDTH = 36;\nconst PADDING = 16;\n\nexport function JobGraphContent({\n model,\n trigger,\n selectedJobId,\n onSelectJob,\n}: {\n model: JobGraphModel;\n trigger: Pick<\n WorkflowRunDetail,\n 'triggerDisplayLabel' | 'triggerLabel' | 'triggerProvider' | 'triggerSource'\n >;\n selectedJobId?: string | undefined;\n onSelectJob: (jobId: string | undefined) => void;\n}) {\n const nodeRefs = useRef(new Map<string, HTMLButtonElement>());\n const [hoveredJobId, setHoveredJobId] = useState<string | undefined>();\n\n if (model.nodes.length === 0) {\n return (\n <EmptyState\n className=\"min-h-160 px-16 py-24\"\n icon=\"componentLine\"\n title=\"No jobs yet\"\n description=\"This run has not materialized jobs.\"\n variant=\"compact\"\n />\n );\n }\n\n const maxRows = Math.max(1, ...model.columns.map((column) => column.length));\n const contentWidth =\n PADDING * 2 +\n TRIGGER_WIDTH +\n COLUMN_GAP +\n model.columns.length * NODE_WIDTH +\n Math.max(0, model.columns.length - 1) * COLUMN_GAP;\n const contentHeight = PADDING * 2 + maxRows * NODE_HEIGHT + Math.max(0, maxRows - 1) * ROW_GAP;\n\n function setNodeRef(jobId: string) {\n return (element: HTMLButtonElement | null) => {\n if (element) {\n nodeRefs.current.set(jobId, element);\n } else {\n nodeRefs.current.delete(jobId);\n }\n };\n }\n\n function handleKeyDown(event: KeyboardEvent<HTMLButtonElement>) {\n const key = navigationKey(event.key);\n if (!key) return;\n\n const currentNodeId = event.currentTarget.dataset.jobId;\n if (!currentNodeId) return;\n\n const nextNodeId = nextJobGraphNodeId({model, currentNodeId, key});\n if (!nextNodeId) return;\n\n event.preventDefault();\n onSelectJob(nextNodeId);\n nodeRefs.current.get(nextNodeId)?.focus();\n }\n\n return (\n <TimeTickerProvider intervalMs={1000} reducedMotionIntervalMs={10_000}>\n <div className=\"min-h-0 overflow-auto bg-background-neutral-base\">\n <div className=\"relative\" style={{width: contentWidth, minHeight: contentHeight}}>\n <GraphEdges model={model} hoveredJobId={hoveredJobId} />\n <div\n className=\"absolute\"\n style={{left: PADDING, top: PADDING + (NODE_HEIGHT - TRIGGER_WIDTH) / 2}}\n >\n <TriggerNode trigger={trigger} />\n </div>\n {model.columns.map((column, columnIndex) => (\n <div\n key={column.map((node) => node.id).join(':')}\n className=\"absolute flex flex-col gap-20\"\n style={{left: jobLeft(columnIndex), top: PADDING}}\n >\n {column.map((node) => (\n <JobNode\n key={node.id}\n node={node}\n selected={node.id === selectedJobId}\n ref={setNodeRef(node.id)}\n onSelect={() => onSelectJob(node.id === selectedJobId ? undefined : node.id)}\n onKeyDown={handleKeyDown}\n onHoverStart={() => setHoveredJobId(node.id)}\n onHoverEnd={() =>\n setHoveredJobId((current) => (current === node.id ? undefined : current))\n }\n />\n ))}\n </div>\n ))}\n </div>\n </div>\n </TimeTickerProvider>\n );\n}\n\nfunction navigationKey(key: string): JobGraphNavigationKey | undefined {\n if (\n key === 'ArrowRight' ||\n key === 'ArrowLeft' ||\n key === 'ArrowDown' ||\n key === 'ArrowUp' ||\n key === 'j' ||\n key === 'k'\n ) {\n return key;\n }\n return undefined;\n}\n\nfunction GraphEdges({\n model,\n hoveredJobId,\n}: {\n model: JobGraphModel;\n hoveredJobId?: string | undefined;\n}) {\n const byId = new Map(model.nodes.map((node) => [node.id, node]));\n const edgeViews = model.edges\n .map((edge) => {\n const fromNode = byId.get(edge.from);\n const toNode = byId.get(edge.to);\n const from = edge.from === 'trigger' ? triggerPoint() : jobRightPoint(fromNode);\n const to = jobLeftPoint(toNode);\n if (!from || !to) return undefined;\n\n return {\n id: edge.id,\n highlighted: edge.from === hoveredJobId || edge.to === hoveredJobId,\n path: edgePath({from, fromNode, to, toNode}),\n };\n })\n .filter((edgeView): edgeView is NonNullable<typeof edgeView> => edgeView !== undefined)\n .sort((left, right) => Number(left.highlighted) - Number(right.highlighted));\n\n return (\n <svg className=\"pointer-events-none absolute inset-0 size-full\" aria-hidden=\"true\">\n {edgeViews.map((edge) => {\n return (\n <g\n key={edge.id}\n className={cn(\n 'text-foreground-neutral-muted transition-colors',\n edge.highlighted && 'text-foreground-neutral-base',\n )}\n >\n <path\n data-edge-id={edge.id}\n d={edge.path}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={edge.highlighted ? 1.5 : 1}\n />\n </g>\n );\n })}\n </svg>\n );\n}\n\nfunction edgePath({\n from,\n fromNode,\n to,\n toNode,\n}: {\n from: {x: number; y: number};\n fromNode: {column: number} | undefined;\n to: {x: number; y: number};\n toNode: {column: number} | undefined;\n}) {\n if (fromNode && toNode && toNode.column - fromNode.column > 1 && from.y !== to.y) {\n const targetLaneX = to.x - COLUMN_GAP / 2;\n return `M ${from.x} ${from.y} H ${targetLaneX} V ${to.y} H ${to.x}`;\n }\n\n const midX = from.x + (to.x - from.x) / 2;\n return `M ${from.x} ${from.y} H ${midX} V ${to.y} H ${to.x}`;\n}\n\nfunction triggerPoint() {\n return {x: PADDING + TRIGGER_WIDTH, y: PADDING + NODE_HEIGHT / 2};\n}\n\nfunction jobLeft(column: number): number {\n // Dependency depths are contiguous, so a node's depth matches its rendered column index.\n return PADDING + TRIGGER_WIDTH + COLUMN_GAP + column * (NODE_WIDTH + COLUMN_GAP);\n}\n\nfunction jobTop(row: number): number {\n return PADDING + row * (NODE_HEIGHT + ROW_GAP);\n}\n\nfunction jobLeftPoint(node: {column: number; row: number} | undefined) {\n if (!node) return undefined;\n return {x: jobLeft(node.column), y: jobTop(node.row) + NODE_HEIGHT / 2};\n}\n\nfunction jobRightPoint(node: {column: number; row: number} | undefined) {\n if (!node) return undefined;\n return {x: jobLeft(node.column) + NODE_WIDTH, y: jobTop(node.row) + NODE_HEIGHT / 2};\n}\n"],"names":["EmptyState","TimeTickerProvider","cn","useRef","useState","nextJobGraphNodeId","JobNode","TriggerNode","NODE_WIDTH","NODE_HEIGHT","COLUMN_GAP","ROW_GAP","TRIGGER_WIDTH","PADDING","JobGraphContent","model","trigger","selectedJobId","onSelectJob","nodeRefs","Map","hoveredJobId","setHoveredJobId","nodes","length","className","icon","title","description","variant","maxRows","Math","max","columns","map","column","contentWidth","contentHeight","setNodeRef","jobId","element","current","set","delete","handleKeyDown","event","key","navigationKey","currentNodeId","currentTarget","dataset","nextNodeId","preventDefault","get","focus","intervalMs","reducedMotionIntervalMs","div","style","width","minHeight","GraphEdges","left","top","columnIndex","jobLeft","node","selected","id","ref","onSelect","undefined","onKeyDown","onHoverStart","onHoverEnd","join","byId","edgeViews","edges","edge","fromNode","from","toNode","to","triggerPoint","jobRightPoint","jobLeftPoint","highlighted","path","edgePath","filter","edgeView","sort","right","Number","svg","aria-hidden","g","data-edge-id","d","fill","stroke","strokeWidth","y","targetLaneX","x","midX","jobTop","row"],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { WorkflowRunDetail } from '#core/workflow-run.js';
2
+ import type { JobGraphModel } from './graph-model.js';
3
+ export declare function JobGraphView({ model, trigger, selectedJobId, defaultSelectedJobId, onSelectedJobChange, className, }: {
4
+ model: JobGraphModel;
5
+ trigger: Pick<WorkflowRunDetail, 'triggerDisplayLabel' | 'triggerLabel' | 'triggerProvider' | 'triggerSource'>;
6
+ selectedJobId?: string | undefined;
7
+ defaultSelectedJobId?: string | undefined;
8
+ onSelectedJobChange?: ((jobId: string | undefined) => void) | undefined;
9
+ className?: string | undefined;
10
+ }): import("react").JSX.Element;
11
+ //# sourceMappingURL=job-graph-view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job-graph-view.d.ts","sourceRoot":"","sources":["../../../src/components/job-graph/job-graph-view.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,kBAAkB,CAAC;AAGpD,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,OAAO,EACP,aAAa,EACb,oBAAoB,EACpB,mBAAmB,EACnB,SAAS,GACV,EAAE;IACD,KAAK,EAAE,aAAa,CAAC;IACrB,OAAO,EAAE,IAAI,CACX,iBAAiB,EACjB,qBAAqB,GAAG,cAAc,GAAG,iBAAiB,GAAG,eAAe,CAC7E,CAAC;IACF,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,oBAAoB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,mBAAmB,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;IACxE,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC,+BAqBA"}
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cn } from '@shipfox/react-ui/utils';
3
+ import { useState } from 'react';
4
+ import { JobGraphContent } from './job-graph-content.js';
5
+ export function JobGraphView({ model, trigger, selectedJobId, defaultSelectedJobId, onSelectedJobChange, className }) {
6
+ const [localSelectedJobId, setLocalSelectedJobId] = useState(defaultSelectedJobId);
7
+ const selected = selectedJobId ?? localSelectedJobId;
8
+ function selectJob(jobId) {
9
+ setLocalSelectedJobId(jobId);
10
+ onSelectedJobChange?.(jobId);
11
+ }
12
+ return /*#__PURE__*/ _jsx("section", {
13
+ "aria-label": "Workflow jobs",
14
+ className: cn('min-h-0', className),
15
+ children: /*#__PURE__*/ _jsx(JobGraphContent, {
16
+ model: model,
17
+ trigger: trigger,
18
+ selectedJobId: selected,
19
+ onSelectJob: selectJob
20
+ })
21
+ });
22
+ }
23
+
24
+ //# sourceMappingURL=job-graph-view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/job-graph/job-graph-view.tsx"],"sourcesContent":["import {cn} from '@shipfox/react-ui/utils';\nimport {useState} from 'react';\nimport type {WorkflowRunDetail} from '#core/workflow-run.js';\nimport type {JobGraphModel} from './graph-model.js';\nimport {JobGraphContent} from './job-graph-content.js';\n\nexport function JobGraphView({\n model,\n trigger,\n selectedJobId,\n defaultSelectedJobId,\n onSelectedJobChange,\n className,\n}: {\n model: JobGraphModel;\n trigger: Pick<\n WorkflowRunDetail,\n 'triggerDisplayLabel' | 'triggerLabel' | 'triggerProvider' | 'triggerSource'\n >;\n selectedJobId?: string | undefined;\n defaultSelectedJobId?: string | undefined;\n onSelectedJobChange?: ((jobId: string | undefined) => void) | undefined;\n className?: string | undefined;\n}) {\n const [localSelectedJobId, setLocalSelectedJobId] = useState<string | undefined>(\n defaultSelectedJobId,\n );\n const selected = selectedJobId ?? localSelectedJobId;\n\n function selectJob(jobId: string | undefined) {\n setLocalSelectedJobId(jobId);\n onSelectedJobChange?.(jobId);\n }\n\n return (\n <section aria-label=\"Workflow jobs\" className={cn('min-h-0', className)}>\n <JobGraphContent\n model={model}\n trigger={trigger}\n selectedJobId={selected}\n onSelectJob={selectJob}\n />\n </section>\n );\n}\n"],"names":["cn","useState","JobGraphContent","JobGraphView","model","trigger","selectedJobId","defaultSelectedJobId","onSelectedJobChange","className","localSelectedJobId","setLocalSelectedJobId","selected","selectJob","jobId","section","aria-label","onSelectJob"],"mappings":";AAAA,SAAQA,EAAE,QAAO,0BAA0B;AAC3C,SAAQC,QAAQ,QAAO,QAAQ;AAG/B,SAAQC,eAAe,QAAO,yBAAyB;AAEvD,OAAO,SAASC,aAAa,EAC3BC,KAAK,EACLC,OAAO,EACPC,aAAa,EACbC,oBAAoB,EACpBC,mBAAmB,EACnBC,SAAS,EAWV;IACC,MAAM,CAACC,oBAAoBC,sBAAsB,GAAGV,SAClDM;IAEF,MAAMK,WAAWN,iBAAiBI;IAElC,SAASG,UAAUC,KAAyB;QAC1CH,sBAAsBG;QACtBN,sBAAsBM;IACxB;IAEA,qBACE,KAACC;QAAQC,cAAW;QAAgBP,WAAWT,GAAG,WAAWS;kBAC3D,cAAA,KAACP;YACCE,OAAOA;YACPC,SAASA;YACTC,eAAeM;YACfK,aAAaJ;;;AAIrB"}
@@ -0,0 +1,3 @@
1
+ import type { JobGraphProps } from './types.js';
2
+ export declare function JobGraph({ run, selectedJobId, defaultSelectedJobId, onSelectedJobChange, className, }: JobGraphProps): import("react").JSX.Element;
3
+ //# sourceMappingURL=job-graph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job-graph.d.ts","sourceRoot":"","sources":["../../../src/components/job-graph/job-graph.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,YAAY,CAAC;AAE9C,wBAAgB,QAAQ,CAAC,EACvB,GAAG,EACH,aAAa,EACb,oBAAoB,EACpB,mBAAmB,EACnB,SAAS,GACV,EAAE,aAAa,+BAYf"}
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useMemo } from 'react';
3
+ import { buildJobGraphModel } from './graph-model.js';
4
+ import { JobGraphView } from './job-graph-view.js';
5
+ export function JobGraph({ run, selectedJobId, defaultSelectedJobId, onSelectedJobChange, className }) {
6
+ const model = useMemo(()=>buildJobGraphModel({
7
+ run
8
+ }), [
9
+ run
10
+ ]);
11
+ return /*#__PURE__*/ _jsx(JobGraphView, {
12
+ model: model,
13
+ trigger: run,
14
+ selectedJobId: selectedJobId,
15
+ defaultSelectedJobId: defaultSelectedJobId,
16
+ onSelectedJobChange: onSelectedJobChange,
17
+ className: className
18
+ });
19
+ }
20
+
21
+ //# sourceMappingURL=job-graph.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/job-graph/job-graph.tsx"],"sourcesContent":["import {useMemo} from 'react';\nimport {buildJobGraphModel} from './graph-model.js';\nimport {JobGraphView} from './job-graph-view.js';\nimport type {JobGraphProps} from './types.js';\n\nexport function JobGraph({\n run,\n selectedJobId,\n defaultSelectedJobId,\n onSelectedJobChange,\n className,\n}: JobGraphProps) {\n const model = useMemo(() => buildJobGraphModel({run}), [run]);\n return (\n <JobGraphView\n model={model}\n trigger={run}\n selectedJobId={selectedJobId}\n defaultSelectedJobId={defaultSelectedJobId}\n onSelectedJobChange={onSelectedJobChange}\n className={className}\n />\n );\n}\n"],"names":["useMemo","buildJobGraphModel","JobGraphView","JobGraph","run","selectedJobId","defaultSelectedJobId","onSelectedJobChange","className","model","trigger"],"mappings":";AAAA,SAAQA,OAAO,QAAO,QAAQ;AAC9B,SAAQC,kBAAkB,QAAO,mBAAmB;AACpD,SAAQC,YAAY,QAAO,sBAAsB;AAGjD,OAAO,SAASC,SAAS,EACvBC,GAAG,EACHC,aAAa,EACbC,oBAAoB,EACpBC,mBAAmB,EACnBC,SAAS,EACK;IACd,MAAMC,QAAQT,QAAQ,IAAMC,mBAAmB;YAACG;QAAG,IAAI;QAACA;KAAI;IAC5D,qBACE,KAACF;QACCO,OAAOA;QACPC,SAASN;QACTC,eAAeA;QACfC,sBAAsBA;QACtBC,qBAAqBA;QACrBC,WAAWA;;AAGjB"}
@@ -0,0 +1,16 @@
1
+ import type { KeyboardEventHandler, Ref } from 'react';
2
+ import type { WorkflowRunDetail } from '#core/workflow-run.js';
3
+ import type { JobGraphNode } from './graph-model.js';
4
+ export declare function TriggerNode({ trigger, }: {
5
+ trigger: Pick<WorkflowRunDetail, 'triggerDisplayLabel' | 'triggerLabel' | 'triggerProvider' | 'triggerSource'>;
6
+ }): import("react").JSX.Element;
7
+ export declare function JobNode({ node, selected, onSelect, onKeyDown, onHoverStart, onHoverEnd, ref, }: {
8
+ node: JobGraphNode;
9
+ selected: boolean;
10
+ onSelect: () => void;
11
+ onKeyDown: KeyboardEventHandler<HTMLButtonElement>;
12
+ onHoverStart: () => void;
13
+ onHoverEnd: () => void;
14
+ ref?: Ref<HTMLButtonElement>;
15
+ }): import("react").JSX.Element;
16
+ //# sourceMappingURL=job-node.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job-node.d.ts","sourceRoot":"","sources":["../../../src/components/job-graph/job-node.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAC,oBAAoB,EAAE,GAAG,EAAC,MAAM,OAAO,CAAC;AAGrD,OAAO,KAAK,EAAmC,iBAAiB,EAAC,MAAM,uBAAuB,CAAC;AAC/F,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,kBAAkB,CAAC;AAMnD,wBAAgB,WAAW,CAAC,EAC1B,OAAO,GACR,EAAE;IACD,OAAO,EAAE,IAAI,CACX,iBAAiB,EACjB,qBAAqB,GAAG,cAAc,GAAG,iBAAiB,GAAG,eAAe,CAC7E,CAAC;CACH,+BA0BA;AAED,wBAAgB,OAAO,CAAC,EACtB,IAAI,EACJ,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,UAAU,EACV,GAAG,GACJ,EAAE;IACD,IAAI,EAAE,YAAY,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,EAAE,oBAAoB,CAAC,iBAAiB,CAAC,CAAC;IACnD,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,GAAG,CAAC,EAAE,GAAG,CAAC,iBAAiB,CAAC,CAAC;CAC9B,+BA+CA"}