@shipfox/client-workflows 14.0.0 → 15.0.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 (223) hide show
  1. package/.storybook/preview.tsx +7 -2
  2. package/.turbo/turbo-build.log +1 -1
  3. package/CHANGELOG.md +35 -0
  4. package/dist/components/job-detail/agent-config-failure-callout.d.ts.map +1 -0
  5. package/dist/components/{workflow-run-view → job-detail}/agent-config-failure-callout.js +2 -2
  6. package/dist/components/job-detail/agent-config-failure-callout.js.map +1 -0
  7. package/dist/components/job-detail/job-detail-header.d.ts +8 -0
  8. package/dist/components/job-detail/job-detail-header.d.ts.map +1 -0
  9. package/dist/components/job-detail/job-detail-header.js +111 -0
  10. package/dist/components/job-detail/job-detail-header.js.map +1 -0
  11. package/dist/components/job-detail/job-detail-view.d.ts +16 -0
  12. package/dist/components/job-detail/job-detail-view.d.ts.map +1 -0
  13. package/dist/components/job-detail/job-detail-view.js +405 -0
  14. package/dist/components/job-detail/job-detail-view.js.map +1 -0
  15. package/dist/components/job-detail/job-empty-states.d.ts +10 -0
  16. package/dist/components/job-detail/job-empty-states.d.ts.map +1 -0
  17. package/dist/components/job-detail/job-empty-states.js +195 -0
  18. package/dist/components/job-detail/job-empty-states.js.map +1 -0
  19. package/dist/components/job-detail/job-execution-switcher.d.ts.map +1 -0
  20. package/dist/components/job-detail/job-execution-switcher.js.map +1 -0
  21. package/dist/components/job-detail/job-execution-time-text.d.ts.map +1 -0
  22. package/dist/components/job-detail/job-execution-time-text.js.map +1 -0
  23. package/dist/components/job-detail/job-selection.d.ts +24 -0
  24. package/dist/components/job-detail/job-selection.d.ts.map +1 -0
  25. package/dist/components/job-detail/job-selection.js +66 -0
  26. package/dist/components/job-detail/job-selection.js.map +1 -0
  27. package/dist/components/job-detail/step-attempt-log-panel.d.ts +15 -0
  28. package/dist/components/job-detail/step-attempt-log-panel.d.ts.map +1 -0
  29. package/dist/components/{workflow-run-view → job-detail}/step-attempt-log-panel.js +61 -16
  30. package/dist/components/job-detail/step-attempt-log-panel.js.map +1 -0
  31. package/dist/components/job-graph/job-graph-content.js +1 -1
  32. package/dist/components/job-graph/job-graph-content.js.map +1 -1
  33. package/dist/components/step-list/step-list-model.js +2 -2
  34. package/dist/components/step-list/step-list-model.js.map +1 -1
  35. package/dist/components/step-list/step-list.d.ts +1 -0
  36. package/dist/components/step-list/step-list.d.ts.map +1 -1
  37. package/dist/components/step-list/step-list.js +29 -4
  38. package/dist/components/step-list/step-list.js.map +1 -1
  39. package/dist/components/workflow-run-duration-label.d.ts +9 -4
  40. package/dist/components/workflow-run-duration-label.d.ts.map +1 -1
  41. package/dist/components/workflow-run-duration-label.js +27 -7
  42. package/dist/components/workflow-run-duration-label.js.map +1 -1
  43. package/dist/components/workflow-run-list/workflow-run-row.d.ts.map +1 -1
  44. package/dist/components/workflow-run-list/workflow-run-row.js +6 -5
  45. package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
  46. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
  47. package/dist/components/workflow-run-summary/workflow-run-summary.js +68 -64
  48. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
  49. package/dist/components/workflow-run-tabs/index.d.ts +0 -3
  50. package/dist/components/workflow-run-tabs/index.d.ts.map +1 -1
  51. package/dist/components/workflow-run-tabs/index.js +0 -3
  52. package/dist/components/workflow-run-tabs/index.js.map +1 -1
  53. package/dist/components/workflow-run-tabs/run-annotations-empty.d.ts +3 -1
  54. package/dist/components/workflow-run-tabs/run-annotations-empty.d.ts.map +1 -1
  55. package/dist/components/workflow-run-tabs/run-annotations-empty.js +2 -2
  56. package/dist/components/workflow-run-tabs/run-annotations-empty.js.map +1 -1
  57. package/dist/components/workflow-run-view/index.d.ts +0 -1
  58. package/dist/components/workflow-run-view/index.d.ts.map +1 -1
  59. package/dist/components/workflow-run-view/index.js +0 -1
  60. package/dist/components/workflow-run-view/index.js.map +1 -1
  61. package/dist/components/workflow-run-view/run-workspace-nav.d.ts +20 -0
  62. package/dist/components/workflow-run-view/run-workspace-nav.d.ts.map +1 -0
  63. package/dist/components/workflow-run-view/run-workspace-nav.js +271 -0
  64. package/dist/components/workflow-run-view/run-workspace-nav.js.map +1 -0
  65. package/dist/components/workflow-run-view/workflow-run-selection.d.ts.map +1 -1
  66. package/dist/components/workflow-run-view/workflow-run-selection.js +1 -14
  67. package/dist/components/workflow-run-view/workflow-run-selection.js.map +1 -1
  68. package/dist/components/workflow-run-view/workflow-run-view.d.ts +10 -8
  69. package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
  70. package/dist/components/workflow-run-view/workflow-run-view.js +250 -236
  71. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
  72. package/dist/components/workflow-status/status-visuals.d.ts.map +1 -1
  73. package/dist/components/workflow-status/status-visuals.js +10 -2
  74. package/dist/components/workflow-status/status-visuals.js.map +1 -1
  75. package/dist/components/workflow-status/workflow-status-icon.d.ts +3 -1
  76. package/dist/components/workflow-status/workflow-status-icon.d.ts.map +1 -1
  77. package/dist/components/workflow-status/workflow-status-icon.js +13 -2
  78. package/dist/components/workflow-status/workflow-status-icon.js.map +1 -1
  79. package/dist/core/entities/job-execution.d.ts +9 -0
  80. package/dist/core/entities/job-execution.d.ts.map +1 -1
  81. package/dist/core/entities/job-execution.js +13 -3
  82. package/dist/core/entities/job-execution.js.map +1 -1
  83. package/dist/core/entities/job.d.ts.map +1 -1
  84. package/dist/core/entities/step-attempt.d.ts.map +1 -1
  85. package/dist/core/entities/step.d.ts +2 -0
  86. package/dist/core/entities/step.d.ts.map +1 -1
  87. package/dist/core/entities/step.js +13 -0
  88. package/dist/core/entities/step.js.map +1 -1
  89. package/dist/core/entities/workflow-run-attempt.d.ts.map +1 -1
  90. package/dist/core/entities/workflow-run.d.ts +56 -1
  91. package/dist/core/entities/workflow-run.d.ts.map +1 -1
  92. package/dist/core/entities/workflow-run.js +98 -0
  93. package/dist/core/entities/workflow-run.js.map +1 -1
  94. package/dist/core/workflow-run.d.ts +4 -4
  95. package/dist/core/workflow-run.d.ts.map +1 -1
  96. package/dist/core/workflow-run.js +3 -3
  97. package/dist/core/workflow-run.js.map +1 -1
  98. package/dist/feature.d.ts +20 -16
  99. package/dist/feature.d.ts.map +1 -1
  100. package/dist/feature.js +5 -0
  101. package/dist/feature.js.map +1 -1
  102. package/dist/hooks/api/workflow-runs.d.ts +5 -3
  103. package/dist/hooks/api/workflow-runs.d.ts.map +1 -1
  104. package/dist/hooks/api/workflow-runs.js +5 -4
  105. package/dist/hooks/api/workflow-runs.js.map +1 -1
  106. package/dist/index.d.ts +1 -0
  107. package/dist/index.d.ts.map +1 -1
  108. package/dist/index.js +1 -0
  109. package/dist/index.js.map +1 -1
  110. package/dist/pages/workflow-job-detail-page.d.ts +11 -0
  111. package/dist/pages/workflow-job-detail-page.d.ts.map +1 -0
  112. package/dist/pages/workflow-job-detail-page.js +66 -0
  113. package/dist/pages/workflow-job-detail-page.js.map +1 -0
  114. package/dist/pages/workflow-run-detail-page.d.ts.map +1 -1
  115. package/dist/pages/workflow-run-detail-page.js +4 -37
  116. package/dist/pages/workflow-run-detail-page.js.map +1 -1
  117. package/dist/routes/inputs.d.ts +47 -34
  118. package/dist/routes/inputs.d.ts.map +1 -1
  119. package/dist/routes/inputs.js +69 -37
  120. package/dist/routes/inputs.js.map +1 -1
  121. package/dist/routes/job-detail.d.ts +10 -0
  122. package/dist/routes/job-detail.d.ts.map +1 -0
  123. package/dist/routes/job-detail.js +27 -0
  124. package/dist/routes/job-detail.js.map +1 -0
  125. package/dist/routes/project-route.d.ts +1 -1
  126. package/dist/routes/run-detail.d.ts.map +1 -1
  127. package/dist/routes/runs.d.ts.map +1 -1
  128. package/dist/routes/workflows.d.ts.map +1 -1
  129. package/dist/tsconfig.test.tsbuildinfo +1 -1
  130. package/package.json +7 -7
  131. package/src/components/{workflow-run-view → job-detail}/agent-config-failure-callout.stories.tsx +1 -1
  132. package/src/components/{workflow-run-view → job-detail}/agent-config-failure-callout.tsx +2 -2
  133. package/src/components/job-detail/job-detail-header.test.tsx +31 -0
  134. package/src/components/job-detail/job-detail-header.tsx +111 -0
  135. package/src/components/job-detail/job-detail-view.stories.tsx +526 -0
  136. package/src/components/job-detail/job-detail-view.tsx +450 -0
  137. package/src/components/job-detail/job-empty-states.test.ts +27 -0
  138. package/src/components/job-detail/job-empty-states.tsx +248 -0
  139. package/src/components/job-detail/job-selection.test.ts +188 -0
  140. package/src/components/job-detail/job-selection.ts +96 -0
  141. package/src/components/{workflow-run-view → job-detail}/step-attempt-log-panel.test.tsx +32 -9
  142. package/src/components/{workflow-run-view → job-detail}/step-attempt-log-panel.tsx +52 -18
  143. package/src/components/job-graph/job-graph-content.tsx +1 -1
  144. package/src/components/step-list/step-list-model.test.ts +1 -1
  145. package/src/components/step-list/step-list-model.ts +2 -2
  146. package/src/components/step-list/step-list.stories.tsx +1 -1
  147. package/src/components/step-list/step-list.test.tsx +33 -0
  148. package/src/components/step-list/step-list.tsx +40 -9
  149. package/src/components/workflow-run-duration-label.tsx +50 -12
  150. package/src/components/workflow-run-list/workflow-run-row.tsx +6 -4
  151. package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +38 -0
  152. package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +79 -5
  153. package/src/components/workflow-run-summary/workflow-run-summary.tsx +56 -60
  154. package/src/components/workflow-run-tabs/index.ts +0 -3
  155. package/src/components/workflow-run-tabs/run-annotations-empty.tsx +6 -2
  156. package/src/components/workflow-run-view/index.ts +0 -1
  157. package/src/components/workflow-run-view/run-workspace-nav.test.tsx +242 -0
  158. package/src/components/workflow-run-view/run-workspace-nav.tsx +301 -0
  159. package/src/components/workflow-run-view/workflow-run-selection.ts +1 -23
  160. package/src/components/workflow-run-view/workflow-run-view.test.tsx +62 -1136
  161. package/src/components/workflow-run-view/workflow-run-view.tsx +330 -321
  162. package/src/components/workflow-status/status-visuals.ts +5 -2
  163. package/src/components/workflow-status/workflow-status-icon.stories.tsx +1 -0
  164. package/src/components/workflow-status/workflow-status-icon.tsx +26 -1
  165. package/src/core/entities/job-execution.ts +17 -2
  166. package/src/core/entities/step.ts +26 -0
  167. package/src/core/entities/workflow-run-display.test.ts +159 -0
  168. package/src/core/entities/workflow-run.ts +139 -1
  169. package/src/core/workflow-run.ts +16 -1
  170. package/src/feature.ts +5 -0
  171. package/src/hooks/api/workflow-runs.ts +6 -2
  172. package/src/index.ts +1 -0
  173. package/src/page-harness-budget.test.ts +1 -0
  174. package/src/pages/workflow-job-detail-page.test.tsx +539 -0
  175. package/src/pages/workflow-job-detail-page.tsx +84 -0
  176. package/src/pages/workflow-run-detail-page.tsx +3 -43
  177. package/src/pages/workflow-run-pages.test.tsx +77 -332
  178. package/src/routes/inputs.test.ts +56 -6
  179. package/src/routes/inputs.ts +64 -17
  180. package/src/routes/job-detail.tsx +29 -0
  181. package/test/pages.tsx +25 -3
  182. package/test/render.tsx +6 -1
  183. package/tsconfig.build.tsbuildinfo +1 -1
  184. package/dist/components/workflow-run-tabs/run-jobs-list.d.ts +0 -18
  185. package/dist/components/workflow-run-tabs/run-jobs-list.d.ts.map +0 -1
  186. package/dist/components/workflow-run-tabs/run-jobs-list.js +0 -107
  187. package/dist/components/workflow-run-tabs/run-jobs-list.js.map +0 -1
  188. package/dist/components/workflow-run-tabs/run-tab-count.d.ts +0 -6
  189. package/dist/components/workflow-run-tabs/run-tab-count.d.ts.map +0 -1
  190. package/dist/components/workflow-run-tabs/run-tab-count.js +0 -27
  191. package/dist/components/workflow-run-tabs/run-tab-count.js.map +0 -1
  192. package/dist/components/workflow-run-tabs/run-tab-strip.d.ts +0 -8
  193. package/dist/components/workflow-run-tabs/run-tab-strip.d.ts.map +0 -1
  194. package/dist/components/workflow-run-tabs/run-tab-strip.js +0 -82
  195. package/dist/components/workflow-run-tabs/run-tab-strip.js.map +0 -1
  196. package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts.map +0 -1
  197. package/dist/components/workflow-run-view/agent-config-failure-callout.js.map +0 -1
  198. package/dist/components/workflow-run-view/job-card.d.ts +0 -17
  199. package/dist/components/workflow-run-view/job-card.d.ts.map +0 -1
  200. package/dist/components/workflow-run-view/job-card.js +0 -496
  201. package/dist/components/workflow-run-view/job-card.js.map +0 -1
  202. package/dist/components/workflow-run-view/job-execution-switcher.d.ts.map +0 -1
  203. package/dist/components/workflow-run-view/job-execution-switcher.js.map +0 -1
  204. package/dist/components/workflow-run-view/job-execution-time-text.d.ts.map +0 -1
  205. package/dist/components/workflow-run-view/job-execution-time-text.js.map +0 -1
  206. package/dist/components/workflow-run-view/step-attempt-log-panel.d.ts +0 -9
  207. package/dist/components/workflow-run-view/step-attempt-log-panel.d.ts.map +0 -1
  208. package/dist/components/workflow-run-view/step-attempt-log-panel.js.map +0 -1
  209. package/src/components/workflow-run-tabs/run-jobs-list.tsx +0 -151
  210. package/src/components/workflow-run-tabs/run-tab-count.tsx +0 -28
  211. package/src/components/workflow-run-tabs/run-tab-strip.stories.tsx +0 -52
  212. package/src/components/workflow-run-tabs/run-tab-strip.test.tsx +0 -117
  213. package/src/components/workflow-run-tabs/run-tab-strip.tsx +0 -97
  214. package/src/components/workflow-run-view/job-card.stories.tsx +0 -789
  215. package/src/components/workflow-run-view/job-card.test.tsx +0 -115
  216. package/src/components/workflow-run-view/job-card.tsx +0 -614
  217. /package/dist/components/{workflow-run-view → job-detail}/agent-config-failure-callout.d.ts +0 -0
  218. /package/dist/components/{workflow-run-view → job-detail}/job-execution-switcher.d.ts +0 -0
  219. /package/dist/components/{workflow-run-view → job-detail}/job-execution-switcher.js +0 -0
  220. /package/dist/components/{workflow-run-view → job-detail}/job-execution-time-text.d.ts +0 -0
  221. /package/dist/components/{workflow-run-view → job-detail}/job-execution-time-text.js +0 -0
  222. /package/src/components/{workflow-run-view → job-detail}/job-execution-switcher.tsx +0 -0
  223. /package/src/components/{workflow-run-view → job-detail}/job-execution-time-text.tsx +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../src/components/workflow-run-view/step-attempt-log-panel.tsx"],"sourcesContent":["import {\n isMissingStepLogStreamError,\n LogView,\n LogViewSkeleton,\n useStepAttemptLogsQuery,\n} from '@shipfox/client-logs';\nimport {Button} from '@shipfox/react-ui/button';\nimport {Callout} from '@shipfox/react-ui/callout';\nimport {Text} from '@shipfox/react-ui/typography';\nimport {type UIEvent, useEffect, useRef} from 'react';\n\nconst TAIL_FOLLOW_THRESHOLD_PX = 24;\nconst INITIAL_LOG_ERROR_RETRY_COUNT = 5;\nconst INITIAL_LOG_ERROR_RETRY_DELAY_MS = 1_500;\nconst logSurfaceClasses = 'max-h-[40vh] rounded-8 md:max-h-[280px]';\n\nexport interface StepAttemptLogPanelProps {\n stepId: string;\n attempt: number;\n attemptStatus: string;\n initialErrorRetryCount?: number | undefined;\n initialErrorRetryDelayMs?: number | undefined;\n}\n\nexport function StepAttemptLogPanel({\n stepId,\n attempt,\n attemptStatus,\n initialErrorRetryCount = INITIAL_LOG_ERROR_RETRY_COUNT,\n initialErrorRetryDelayMs = INITIAL_LOG_ERROR_RETRY_DELAY_MS,\n}: StepAttemptLogPanelProps) {\n const panelRef = useRef<HTMLDivElement>(null);\n const shouldFollowTailRef = useRef(true);\n const missingStreamRetryCount = attemptStatus === 'running' ? undefined : initialErrorRetryCount;\n const retryMissingStream = attemptStatus === 'running' || isTerminalAttemptStatus(attemptStatus);\n const query = useStepAttemptLogsQuery(stepId, attempt, {\n retryMissingStream,\n missingStreamRetryCount,\n missingStreamRetryDelayMs: initialErrorRetryDelayMs,\n initialErrorRetryCount,\n initialErrorRetryDelayMs,\n });\n const records = query.data?.records ?? [];\n const recordCount = records.length;\n const anchorToFailure = attemptStatus === 'failed';\n const missingActiveStream =\n retryMissingStream && query.data === undefined && isMissingStepLogStreamError(query.error);\n const initialError = query.isError && query.data === undefined && !missingActiveStream;\n const staleError = query.isError && query.data !== undefined;\n\n useEffect(() => {\n if (recordCount === 0) return undefined;\n if (anchorToFailure) return undefined;\n if (!shouldFollowTailRef.current) return undefined;\n\n const frame = scheduleAnimationFrame(() => {\n const scrollElement = panelRef.current?.querySelector<HTMLElement>('[data-slot=\"log-rows\"]');\n if (!scrollElement) return;\n scrollElement.scrollTop = scrollElement.scrollHeight;\n });\n\n return () => {\n cancelScheduledFrame(frame);\n };\n }, [anchorToFailure, recordCount]);\n\n function handleLogScroll(event: UIEvent<HTMLDivElement>) {\n const element = event.currentTarget;\n const distanceFromBottom = element.scrollHeight - element.scrollTop - element.clientHeight;\n shouldFollowTailRef.current = distanceFromBottom <= TAIL_FOLLOW_THRESHOLD_PX;\n }\n\n if (query.isPending) {\n return <StepLogsLoadingSurface label=\"Loading logs\" />;\n }\n\n if (missingActiveStream) {\n return <StepLogsLoadingSurface label=\"Waiting for logs\" />;\n }\n\n if (initialError) {\n return <StepLogsError retrying={query.isFetching} onRetry={() => void query.refetch()} />;\n }\n\n return (\n <div ref={panelRef} className=\"flex min-w-0 flex-col gap-8\">\n {staleError ? (\n <Callout role=\"alert\" type=\"warning\" className=\"px-10 py-8\">\n <div className=\"flex min-w-0 flex-1 items-center justify-between gap-8\">\n <Text size=\"xs\">Could not refresh logs.</Text>\n <Button\n type=\"button\"\n size=\"2xs\"\n variant=\"secondary\"\n isLoading={query.isFetching}\n onClick={() => void query.refetch()}\n >\n Retry\n </Button>\n </div>\n </Callout>\n ) : null}\n <LogView\n records={records}\n emptyState={query.data?.complete ? 'complete' : 'pending'}\n anchorToFailure={anchorToFailure}\n className={logSurfaceClasses}\n onScroll={handleLogScroll}\n />\n </div>\n );\n}\n\nfunction StepLogsLoadingSurface({label}: {label: string}) {\n return (\n <div role=\"status\" aria-label={label}>\n <LogViewSkeleton className={logSurfaceClasses} />\n </div>\n );\n}\n\nfunction scheduleAnimationFrame(callback: FrameRequestCallback): number {\n if (typeof globalThis.requestAnimationFrame === 'function') {\n return globalThis.requestAnimationFrame(callback);\n }\n return window.setTimeout(() => callback(Date.now()), 0);\n}\n\nfunction cancelScheduledFrame(frame: number) {\n if (typeof globalThis.cancelAnimationFrame === 'function') {\n globalThis.cancelAnimationFrame(frame);\n return;\n }\n window.clearTimeout(frame);\n}\n\nfunction isTerminalAttemptStatus(status: string): boolean {\n return status === 'succeeded' || status === 'failed' || status === 'cancelled';\n}\n\nfunction StepLogsError({retrying, onRetry}: {retrying: boolean; onRetry: () => void}) {\n return (\n <Callout role=\"alert\" type=\"error\" className=\"px-10 py-8\">\n <div className=\"flex min-w-0 flex-1 items-center justify-between gap-8\">\n <Text size=\"xs\">Could not load logs.</Text>\n <Button type=\"button\" size=\"2xs\" variant=\"secondary\" isLoading={retrying} onClick={onRetry}>\n Retry\n </Button>\n </div>\n </Callout>\n );\n}\n"],"names":["isMissingStepLogStreamError","LogView","LogViewSkeleton","useStepAttemptLogsQuery","Button","Callout","Text","useEffect","useRef","TAIL_FOLLOW_THRESHOLD_PX","INITIAL_LOG_ERROR_RETRY_COUNT","INITIAL_LOG_ERROR_RETRY_DELAY_MS","logSurfaceClasses","StepAttemptLogPanel","stepId","attempt","attemptStatus","initialErrorRetryCount","initialErrorRetryDelayMs","panelRef","shouldFollowTailRef","missingStreamRetryCount","undefined","retryMissingStream","isTerminalAttemptStatus","query","missingStreamRetryDelayMs","records","data","recordCount","length","anchorToFailure","missingActiveStream","error","initialError","isError","staleError","current","frame","scheduleAnimationFrame","scrollElement","querySelector","scrollTop","scrollHeight","cancelScheduledFrame","handleLogScroll","event","element","currentTarget","distanceFromBottom","clientHeight","isPending","StepLogsLoadingSurface","label","StepLogsError","retrying","isFetching","onRetry","refetch","div","ref","className","role","type","size","variant","isLoading","onClick","emptyState","complete","onScroll","aria-label","callback","globalThis","requestAnimationFrame","window","setTimeout","Date","now","cancelAnimationFrame","clearTimeout","status"],"mappings":"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"}
@@ -1,151 +0,0 @@
1
- import {EmptyState} from '@shipfox/react-ui/empty-state';
2
- import {useTimeTick} from '@shipfox/react-ui/time-ticker';
3
- import {cn} from '@shipfox/react-ui/utils';
4
- import {
5
- defaultJobExecution,
6
- deriveJobDisplayStatus,
7
- deriveJobExecutionDisplayStatus,
8
- type Job,
9
- } from '#core/workflow-run.js';
10
- import {formatJobDurationAccessibleLabel} from '../job-graph/job-duration-format.js';
11
- import {JobDurationLabel} from '../job-graph/job-duration-label.js';
12
- import {JobCard} from '../workflow-run-view/job-card.js';
13
- import {WorkflowStatusIcon} from '../workflow-status/workflow-status-icon.js';
14
-
15
- export interface RunJobsListProps {
16
- jobs: Job[];
17
- selectedJobId?: string | undefined;
18
- onSelectedJobChange: (jobId: string | undefined) => void;
19
- workspaceSlug?: string | undefined;
20
- selectedJobExecution: ReturnType<typeof defaultJobExecution>;
21
- selectedAttemptId: string | null | undefined;
22
- onSelectedJobExecutionChange: ((jobExecutionId: string | undefined) => void) | undefined;
23
- onSelectedAttemptChange: ((attemptId: string | undefined) => void) | undefined;
24
- sourcePanelId?: string | undefined;
25
- sourceAvailable?: boolean | undefined;
26
- focusedSourceStepId?: string | null | undefined;
27
- onOpenStepSource?: Parameters<typeof JobCard>[0]['onOpenStepSource'];
28
- }
29
-
30
- export function RunJobsList({
31
- jobs,
32
- selectedJobId,
33
- onSelectedJobChange,
34
- workspaceSlug,
35
- selectedJobExecution,
36
- selectedAttemptId,
37
- onSelectedJobExecutionChange,
38
- onSelectedAttemptChange,
39
- sourcePanelId,
40
- sourceAvailable,
41
- focusedSourceStepId,
42
- onOpenStepSource,
43
- }: RunJobsListProps) {
44
- useTimeTick();
45
-
46
- if (jobs.length === 0) {
47
- return (
48
- <section
49
- aria-label="Workflow jobs"
50
- className="rounded-8 border border-border-neutral-base bg-background-components-base"
51
- >
52
- <div className="min-h-160">
53
- <RunJobsEmptyState />
54
- </div>
55
- </section>
56
- );
57
- }
58
-
59
- const orderedJobs = [...jobs].sort(compareJobs);
60
-
61
- return (
62
- <section aria-label="Workflow jobs" className="flex min-w-0 flex-col gap-8">
63
- <ul aria-label="Run jobs" className="m-0 flex min-w-0 list-none flex-col gap-8 p-0">
64
- {orderedJobs.map((job) => {
65
- const selected = job.id === selectedJobId;
66
- const status = deriveJobDisplayStatus(job);
67
- const execution = defaultJobExecution(job);
68
- const durationLabel = formatJobDurationAccessibleLabel(
69
- job.displayDuration,
70
- execution ? deriveJobExecutionDisplayStatus(execution) : undefined,
71
- );
72
- const rowLabel = [
73
- job.displayName,
74
- statusLabel(status),
75
- durationLabel,
76
- job.carriedOver ? 'reused' : undefined,
77
- ]
78
- .filter((part): part is string => Boolean(part))
79
- .join(', ');
80
-
81
- return (
82
- <li key={job.id} className="min-w-0">
83
- <button
84
- type="button"
85
- aria-expanded={selected}
86
- aria-controls={selected ? `job-card-${job.id}` : undefined}
87
- aria-label={rowLabel}
88
- onClick={() => onSelectedJobChange(selected ? undefined : job.id)}
89
- className={cn(
90
- 'relative flex min-h-44 w-full min-w-0 items-center gap-8 rounded-8 bg-background-neutral-base px-12 text-left shadow-button-neutral outline-none transition-colors hover:bg-background-components-hover focus-visible:shadow-button-neutral-focus',
91
- selected &&
92
- 'bg-background-components-hover hover:bg-background-components-pressed',
93
- )}
94
- >
95
- {selected ? (
96
- <span
97
- aria-hidden="true"
98
- className="absolute inset-y-6 left-0 w-px rounded-full bg-border-highlights-interactive"
99
- />
100
- ) : null}
101
- <WorkflowStatusIcon status={status} size={14} tooltip={false} />
102
- <span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground-neutral-base">
103
- {job.displayName}
104
- </span>
105
- <JobDurationLabel duration={job.displayDuration} />
106
- </button>
107
- {selected ? (
108
- <div id={`job-card-${job.id}`} className="mt-8">
109
- <JobCard
110
- workspaceSlug={workspaceSlug}
111
- job={job}
112
- selectedJobExecution={selectedJobExecution}
113
- selectedAttemptId={job.carriedOver ? undefined : selectedAttemptId}
114
- onSelectedJobExecutionChange={onSelectedJobExecutionChange}
115
- onSelectedAttemptChange={onSelectedAttemptChange}
116
- sourcePanelId={sourcePanelId}
117
- sourceAvailable={sourceAvailable}
118
- focusedSourceStepId={focusedSourceStepId}
119
- onOpenStepSource={onOpenStepSource}
120
- />
121
- </div>
122
- ) : null}
123
- </li>
124
- );
125
- })}
126
- </ul>
127
- </section>
128
- );
129
- }
130
-
131
- function RunJobsEmptyState() {
132
- return (
133
- <EmptyState
134
- icon="componentLine"
135
- title="No jobs yet"
136
- description="This run has not materialized jobs."
137
- />
138
- );
139
- }
140
-
141
- function compareJobs(left: Job, right: Job): number {
142
- return (
143
- left.position - right.position ||
144
- left.displayName.localeCompare(right.displayName) ||
145
- left.id.localeCompare(right.id)
146
- );
147
- }
148
-
149
- function statusLabel(status: ReturnType<typeof deriveJobDisplayStatus>): string {
150
- return status === 'listening' ? 'Listening' : status.charAt(0).toUpperCase() + status.slice(1);
151
- }
@@ -1,28 +0,0 @@
1
- import {Badge} from '@shipfox/react-ui/badge';
2
-
3
- export function RunTabCount({
4
- count,
5
- alertCount = 0,
6
- alertLabel,
7
- }: {
8
- count: number | undefined;
9
- alertCount?: number | undefined;
10
- alertLabel: string;
11
- }) {
12
- if (count === undefined && alertCount <= 0) return null;
13
-
14
- return (
15
- <>
16
- {count === undefined ? null : (
17
- <Badge size="2xs" variant="neutral" aria-hidden="true">
18
- {count}
19
- </Badge>
20
- )}
21
- {alertCount > 0 ? (
22
- <Badge size="2xs" variant="error" aria-hidden="true">
23
- {alertCount} {alertLabel}
24
- </Badge>
25
- ) : null}
26
- </>
27
- );
28
- }
@@ -1,52 +0,0 @@
1
- import {Tabs} from '@shipfox/react-ui/tabs';
2
- import type {Meta, StoryObj} from '@storybook/react';
3
- import type {RunAnnotationSummary} from '#core/workflow-run-tabs.js';
4
- import {RunTabStrip} from './run-tab-strip.js';
5
-
6
- const ANNOTATION_SUMMARY: RunAnnotationSummary = {
7
- total: 8,
8
- error: 2,
9
- warning: 3,
10
- info: 2,
11
- success: 1,
12
- };
13
-
14
- const meta = {
15
- title: 'Workflows/RunTabStrip',
16
- component: RunTabStrip,
17
- parameters: {layout: 'fullscreen'},
18
- render: (args) => (
19
- <div className="bg-background-neutral-background">
20
- <Tabs defaultValue="summary">
21
- <RunTabStrip {...args} />
22
- </Tabs>
23
- </div>
24
- ),
25
- args: {
26
- jobCount: 6,
27
- jobsFailed: 1,
28
- annotationSummary: ANNOTATION_SUMMARY,
29
- },
30
- } satisfies Meta<typeof RunTabStrip>;
31
-
32
- export default meta;
33
- type Story = StoryObj<typeof meta>;
34
-
35
- export const Playground: Story = {};
36
-
37
- export const NoCounts: Story = {
38
- args: {
39
- jobCount: undefined,
40
- annotationSummary: undefined,
41
- },
42
- };
43
-
44
- export const NarrowLayout: Story = {
45
- decorators: [
46
- (Story) => (
47
- <div className="w-[360px] max-w-full overflow-hidden border">
48
- <Story />
49
- </div>
50
- ),
51
- ],
52
- };
@@ -1,117 +0,0 @@
1
- import {Tabs} from '@shipfox/react-ui/tabs';
2
- import {
3
- createMemoryHistory,
4
- createRootRoute,
5
- createRoute,
6
- createRouter,
7
- Outlet,
8
- RouterProvider,
9
- } from '@tanstack/react-router';
10
- import {act, render, screen, within} from '@testing-library/react';
11
- import userEvent from '@testing-library/user-event';
12
- import type {RunAnnotationSummary} from '#core/workflow-run-tabs.js';
13
- import {RunTabStrip} from './run-tab-strip.js';
14
-
15
- const ANNOTATION_SUMMARY: RunAnnotationSummary = {
16
- total: 5,
17
- error: 2,
18
- warning: 1,
19
- info: 2,
20
- success: 0,
21
- };
22
-
23
- describe('RunTabStrip', () => {
24
- test('exposes tab counts and keyboard navigation', async () => {
25
- const user = userEvent.setup();
26
-
27
- await renderStrip({annotationSummary: ANNOTATION_SUMMARY, jobCount: 3, jobsFailed: 1});
28
-
29
- const jobsTab = screen.getByRole('tab', {name: 'Jobs, 3 jobs, 1 failed'});
30
- const annotationsTab = screen.getByRole('tab', {
31
- name: 'Annotations, 5 annotations, 2 errors',
32
- });
33
- expect(within(jobsTab).getByText('3')).toHaveClass('bg-tag-neutral-bg');
34
- expect(within(jobsTab).getByText('1 failed')).toHaveClass('bg-tag-error-bg');
35
- expect(within(annotationsTab).getByText('5')).toHaveClass('bg-tag-neutral-bg');
36
- expect(within(annotationsTab).getByText('2 errors')).toHaveClass('bg-tag-error-bg');
37
- expect(screen.queryByRole('link', {name: '2 errors'})).not.toBeInTheDocument();
38
- expect(screen.getByRole('tab', {name: 'Source'})).toHaveClass(
39
- 'data-[state=inactive]:text-foreground-neutral-subtle',
40
- 'data-[state=inactive]:hover:text-foreground-neutral-base',
41
- );
42
-
43
- const summary = screen.getByRole('tab', {name: 'Summary'});
44
- expect(screen.getByRole('tablist', {name: 'Run sections'})).toHaveClass('min-w-full', 'px-16');
45
- expect(summary.parentElement?.parentElement).toHaveClass('bg-background-neutral-background');
46
- summary.focus();
47
- await user.keyboard('{ArrowRight}');
48
-
49
- expect(screen.getByRole('tab', {name: 'Jobs, 3 jobs, 1 failed'})).toHaveAttribute(
50
- 'aria-selected',
51
- 'true',
52
- );
53
- expect(screen.getByRole('tab', {name: 'Summary'})).toHaveAttribute('tabindex', '-1');
54
- });
55
-
56
- test('does not reserve count space when data is absent', async () => {
57
- await renderStrip({annotationSummary: undefined, jobCount: undefined});
58
-
59
- const jobsTab = screen.getByRole('tab', {name: 'Jobs'});
60
- const annotationsTab = screen.getByRole('tab', {name: 'Annotations'});
61
-
62
- expect(jobsTab).toBeInTheDocument();
63
- expect(annotationsTab).toBeInTheDocument();
64
- expect(jobsTab.querySelector('[data-slot="badge"]')).toBeNull();
65
- expect(annotationsTab.querySelector('[data-slot="badge"]')).toBeNull();
66
- expect(screen.queryByText('5 annotations')).not.toBeInTheDocument();
67
- });
68
-
69
- test('singularizes accessible count labels', async () => {
70
- await renderStrip({
71
- annotationSummary: {total: 1, error: 1, warning: 0, info: 0, success: 0},
72
- jobCount: 1,
73
- jobsFailed: 1,
74
- });
75
-
76
- expect(screen.getByRole('tab', {name: 'Jobs, 1 job, 1 failed'})).toBeInTheDocument();
77
- expect(
78
- screen.getByRole('tab', {name: 'Annotations, 1 annotation, 1 error'}),
79
- ).toBeInTheDocument();
80
- });
81
- });
82
-
83
- async function renderStrip({
84
- annotationSummary,
85
- jobCount,
86
- jobsFailed = 0,
87
- }: {
88
- annotationSummary?: RunAnnotationSummary | undefined;
89
- jobCount?: number | undefined;
90
- jobsFailed?: number | undefined;
91
- } = {}) {
92
- const rootRoute = createRootRoute({component: Outlet});
93
- const runRoute = createRoute({
94
- getParentRoute: () => rootRoute,
95
- path: '/w/$workspaceSlug/p/$projectSlug/runs/$workflowRunId',
96
- component: () => (
97
- <Tabs defaultValue="summary">
98
- <RunTabStrip
99
- jobCount={jobCount}
100
- jobsFailed={jobsFailed}
101
- annotationSummary={annotationSummary}
102
- />
103
- </Tabs>
104
- ),
105
- });
106
- const router = createRouter({
107
- history: createMemoryHistory({initialEntries: ['/w/acme/p/project/runs/run-1']}),
108
- routeTree: rootRoute.addChildren([runRoute]),
109
- });
110
- await router.load();
111
- let result: ReturnType<typeof render> | undefined;
112
- await act(() => {
113
- result = render(<RouterProvider router={router} />);
114
- });
115
- if (!result) throw new Error('Run tab strip did not render.');
116
- return result;
117
- }
@@ -1,97 +0,0 @@
1
- import {TabsList, TabsTrigger} from '@shipfox/react-ui/tabs';
2
- import type {RunAnnotationSummary} from '#core/workflow-run-tabs.js';
3
- import {RunTabCount} from './run-tab-count.js';
4
-
5
- export interface RunTabStripProps {
6
- jobCount?: number | undefined;
7
- jobsFailed?: number | undefined;
8
- annotationSummary?: RunAnnotationSummary | undefined;
9
- }
10
-
11
- export function RunTabStrip({jobCount, jobsFailed = 0, annotationSummary}: RunTabStripProps) {
12
- const annotationCount = annotationSummary?.total;
13
- const annotationErrors = annotationSummary?.error ?? 0;
14
-
15
- return (
16
- <div className="min-w-0 overflow-x-auto border-b border-border-neutral-base bg-background-neutral-background [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
17
- <TabsList
18
- aria-label="Run sections"
19
- className="h-40 min-h-40 w-max min-w-full gap-16 px-16 [@media(pointer:coarse)]:h-44 [@media(pointer:coarse)]:min-h-44"
20
- >
21
- <TabsTrigger
22
- value="summary"
23
- className="h-40 min-h-40 py-0 text-xs data-[state=inactive]:text-foreground-neutral-subtle data-[state=inactive]:hover:text-foreground-neutral-base [@media(pointer:coarse)]:h-44 [@media(pointer:coarse)]:min-h-44"
24
- >
25
- Summary
26
- </TabsTrigger>
27
- <TabsTrigger
28
- value="jobs"
29
- aria-label={runTabAccessibleName({
30
- label: 'Jobs',
31
- total: jobCount,
32
- totalSingular: 'job',
33
- totalPlural: 'jobs',
34
- alertCount: jobsFailed,
35
- alertSingular: 'failed',
36
- alertPlural: 'failed',
37
- })}
38
- className="h-40 min-h-40 gap-6 py-0 text-xs data-[state=inactive]:text-foreground-neutral-subtle data-[state=inactive]:hover:text-foreground-neutral-base [@media(pointer:coarse)]:h-44 [@media(pointer:coarse)]:min-h-44"
39
- >
40
- <span>Jobs</span>
41
- <RunTabCount count={jobCount} alertCount={jobsFailed} alertLabel="failed" />
42
- </TabsTrigger>
43
- <TabsTrigger
44
- value="annotations"
45
- aria-label={runTabAccessibleName({
46
- label: 'Annotations',
47
- total: annotationCount,
48
- totalSingular: 'annotation',
49
- totalPlural: 'annotations',
50
- alertCount: annotationErrors,
51
- alertSingular: 'error',
52
- alertPlural: 'errors',
53
- })}
54
- className="h-40 min-h-40 gap-6 py-0 text-xs data-[state=inactive]:text-foreground-neutral-subtle data-[state=inactive]:hover:text-foreground-neutral-base [@media(pointer:coarse)]:h-44 [@media(pointer:coarse)]:min-h-44"
55
- >
56
- <span>Annotations</span>
57
- <RunTabCount
58
- count={annotationCount}
59
- alertCount={annotationErrors}
60
- alertLabel={annotationErrors === 1 ? 'error' : 'errors'}
61
- />
62
- </TabsTrigger>
63
- <TabsTrigger
64
- value="source"
65
- className="h-40 min-h-40 py-0 text-xs data-[state=inactive]:text-foreground-neutral-subtle data-[state=inactive]:hover:text-foreground-neutral-base [@media(pointer:coarse)]:h-44 [@media(pointer:coarse)]:min-h-44"
66
- >
67
- Source
68
- </TabsTrigger>
69
- </TabsList>
70
- </div>
71
- );
72
- }
73
-
74
- function runTabAccessibleName({
75
- label,
76
- total,
77
- totalSingular,
78
- totalPlural,
79
- alertCount,
80
- alertSingular,
81
- alertPlural,
82
- }: {
83
- label: string;
84
- total: number | undefined;
85
- totalSingular: string;
86
- totalPlural: string;
87
- alertCount: number;
88
- alertSingular: string;
89
- alertPlural: string;
90
- }): string {
91
- const parts = [label];
92
-
93
- if (total !== undefined) parts.push(`${total} ${total === 1 ? totalSingular : totalPlural}`);
94
- if (alertCount > 0) parts.push(`${alertCount} ${alertCount === 1 ? alertSingular : alertPlural}`);
95
-
96
- return parts.join(', ');
97
- }