@shipfox/client-workflows 14.0.1 → 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 +21 -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 +3 -3
  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,789 +0,0 @@
1
- import type {
2
- WorkflowExecutionEventDto,
3
- WorkflowRunJobDetailDto,
4
- WorkflowRunJobExecutionDetailDto,
5
- WorkflowRunStepDetailDto,
6
- } from '@shipfox/api-workflows-dto';
7
- import {Text} from '@shipfox/react-ui/typography';
8
- import type {Meta, StoryObj} from '@storybook/react';
9
- import {type ReactNode, useState} from 'react';
10
- import {screen, userEvent, within} from 'storybook/test';
11
- import {type Job, resolveJobExecution} from '#core/workflow-run.js';
12
- import {
13
- workflowJob,
14
- workflowJobExecutionDto,
15
- workflowStepAttemptDto,
16
- workflowStepDto,
17
- } from '#test/fixtures/workflow-run.js';
18
- import type {StepExpandedContext} from '../step-list/index.js';
19
- import {JobCard} from './job-card.js';
20
-
21
- const SWITCH_EXECUTION_PATTERN = /Switch job execution/;
22
- const EXECUTION_FIVE_PATTERN = /#5/;
23
-
24
- const QUEUED_AT = '2026-06-26T11:54:00.000Z';
25
- const STARTED_AT = '2026-06-26T11:57:46.000Z';
26
- const FINISHED_AT = '2026-06-26T12:00:00.000Z';
27
- const LISTENING_BASE_AT = Date.parse('2026-06-26T10:00:00.000Z');
28
-
29
- const meta = {
30
- title: 'Workflows/JobCard',
31
- component: JobCard,
32
- parameters: {
33
- layout: 'centered',
34
- },
35
- decorators: [
36
- (Story) => (
37
- <div className="w-760 bg-background-neutral-base p-16">
38
- <Story />
39
- </div>
40
- ),
41
- ],
42
- args: {
43
- job: singleExecutionJob(),
44
- selectedJobExecution: singleExecutionJob().jobExecutions[0],
45
- selectedAttemptId: null,
46
- onSelectedJobExecutionChange: undefined,
47
- onSelectedAttemptChange: undefined,
48
- renderExpandedStep: StepDetailPlaceholder,
49
- },
50
- } satisfies Meta<typeof JobCard>;
51
-
52
- export default meta;
53
- type Story = StoryObj<typeof meta>;
54
- type JobCardStoryContext = Parameters<NonNullable<Story['play']>>[0];
55
-
56
- async function openJobExecutionSwitcher(ctx: JobCardStoryContext) {
57
- const canvas = within(ctx.canvasElement);
58
-
59
- await userEvent.click(await canvas.findByRole('button', {name: SWITCH_EXECUTION_PATTERN}));
60
- await screen.findByRole('menu');
61
- await screen.findByRole('menuitem', {name: EXECUTION_FIVE_PATTERN});
62
- }
63
-
64
- export const Playground: Story = {
65
- render: () => <JobCardStory job={singleExecutionJob()} />,
66
- };
67
-
68
- export const States: Story = {
69
- render: () => (
70
- <div className="flex flex-col gap-16">
71
- <StorySection label="single execution">
72
- <JobCardStory job={singleExecutionJob()} />
73
- </StorySection>
74
- <StorySection label="execution name with missing job name">
75
- <JobCardStory job={keyFallbackJob()} />
76
- </StorySection>
77
- <StorySection label="queueing execution">
78
- <JobCardStory job={queueingExecutionJob()} initialJobExecutionId="exec-queueing" />
79
- </StorySection>
80
- <StorySection label="failed then succeeded">
81
- <JobCardStory job={failedThenSucceededJob()} initialJobExecutionId="exec-2" />
82
- </StorySection>
83
- <StorySection label="running retry">
84
- <JobCardStory job={runningRetryJob()} initialJobExecutionId="exec-2" />
85
- </StorySection>
86
- </div>
87
- ),
88
- };
89
-
90
- export const EmptyStates: Story = {
91
- render: () => (
92
- <div className="flex flex-col gap-16">
93
- <StorySection label="pending, no execution">
94
- <JobCardStory job={pendingNoExecutionJob()} />
95
- </StorySection>
96
- <StorySection label="listening, waiting for trigger events">
97
- <JobCardStory job={listeningJobNoExecutions()} />
98
- </StorySection>
99
- <StorySection label="listener resolved, no executions">
100
- <JobCardStory job={listeningJobResolvedNoExecutions()} />
101
- </StorySection>
102
- <StorySection label="skipped, no execution">
103
- <JobCardStory job={zeroExecutionJob()} />
104
- </StorySection>
105
- <StorySection label="cancelled, no execution">
106
- <JobCardStory job={cancelledNoExecutionJob()} />
107
- </StorySection>
108
- <StorySection label="finished, missing execution record">
109
- <JobCardStory job={finishedWithoutExecutionJob()} />
110
- </StorySection>
111
- <StorySection label="finished, no steps">
112
- <JobCardStory job={finishedWithoutStepsJob()} initialJobExecutionId="exec-empty" />
113
- </StorySection>
114
- <StorySection label="carried over">
115
- <JobCardStory job={carriedOverJob()} />
116
- </StorySection>
117
- </div>
118
- ),
119
- };
120
-
121
- export const ListeningProgression: Story = {
122
- render: () => (
123
- <div className="flex flex-col gap-16">
124
- <StorySection label="pending, no executions">
125
- <JobCardStory job={listeningJobPending()} />
126
- </StorySection>
127
- <StorySection label="listening, 0 executions">
128
- <JobCardStory job={listeningJobNoExecutions()} />
129
- </StorySection>
130
- <StorySection label="listening, 1 running execution">
131
- <JobCardStory job={listeningJobOneExecution()} initialJobExecutionId="exec-listen-one-1" />
132
- </StorySection>
133
- <StorySection label="listening, mixed executions">
134
- <JobCardStory job={listeningJobMixedExecutions()} initialJobExecutionId="exec-listen-6" />
135
- </StorySection>
136
- <StorySection label="failed, resolved">
137
- <JobCardStory job={listeningJobResolved()} initialJobExecutionId="exec-listen-resolved-4" />
138
- </StorySection>
139
- </div>
140
- ),
141
- };
142
-
143
- export const ManyExecutions: Story = {
144
- render: () => (
145
- <div className="flex flex-col gap-16">
146
- <StorySection label="listening, 64 executions, short name">
147
- <JobCardStory
148
- job={manyExecutionsListeningJob('gate', 'job-many-short')}
149
- initialJobExecutionId="exec-many-job-many-short-64"
150
- />
151
- </StorySection>
152
- <StorySection label="listening, 64 executions, medium name">
153
- <JobCardStory
154
- job={manyExecutionsListeningJob('release-gates', 'job-many-medium')}
155
- initialJobExecutionId="exec-many-job-many-medium-64"
156
- />
157
- </StorySection>
158
- <StorySection label="listening, 64 executions, long name">
159
- <JobCardStory
160
- job={manyExecutionsListeningJob(
161
- 'release-production-multi-region-with-canary-validation',
162
- 'job-many-long',
163
- )}
164
- initialJobExecutionId="exec-many-job-many-long-64"
165
- />
166
- </StorySection>
167
- </div>
168
- ),
169
- };
170
-
171
- export const SourceAction: Story = {
172
- render: () => (
173
- <JobCardStory
174
- job={sourceActionJob()}
175
- initialJobExecutionId="exec-source-action"
176
- initialAttemptId="attempt-checkout-exec-source-action"
177
- sourceAvailable
178
- sourcePanelId="workflow-source-panel-story"
179
- />
180
- ),
181
- };
182
-
183
- export const Content: Story = {
184
- decorators: [
185
- (Story) => (
186
- <div className="w-440 bg-background-neutral-base p-16">
187
- <Story />
188
- </div>
189
- ),
190
- ],
191
- render: () => (
192
- <div className="flex flex-col gap-16">
193
- <StorySection label="regular long name">
194
- <JobCardStory job={longNameManyExecutionsJob()} initialJobExecutionId="exec-6" />
195
- </StorySection>
196
- <StorySection label="listening long name">
197
- <JobCardStory job={longNameListeningJob()} initialJobExecutionId="exec-long-listening-3" />
198
- </StorySection>
199
- </div>
200
- ),
201
- };
202
-
203
- export const TestListeningSwitcherOpen: Story = {
204
- render: () => (
205
- <JobCardStory job={listeningJobMixedExecutions()} initialJobExecutionId="exec-listen-6" />
206
- ),
207
- play: openJobExecutionSwitcher,
208
- };
209
-
210
- function JobCardStory({
211
- job,
212
- initialJobExecutionId,
213
- initialAttemptId = null,
214
- sourceAvailable,
215
- sourcePanelId,
216
- }: {
217
- job: Job;
218
- initialJobExecutionId?: string | undefined;
219
- initialAttemptId?: string | null | undefined;
220
- sourceAvailable?: boolean | undefined;
221
- sourcePanelId?: string | undefined;
222
- }) {
223
- const [selectedJobExecutionId, setSelectedJobExecutionId] = useState(initialJobExecutionId);
224
- const [selectedAttemptId, setSelectedAttemptId] = useState<string | null>(initialAttemptId);
225
- const selectedJobExecution = resolveJobExecution(job, selectedJobExecutionId);
226
-
227
- function selectJobExecution(jobExecutionId: string | undefined) {
228
- setSelectedJobExecutionId(jobExecutionId);
229
- setSelectedAttemptId(null);
230
- }
231
-
232
- return (
233
- <JobCard
234
- job={job}
235
- selectedJobExecution={selectedJobExecution}
236
- selectedAttemptId={selectedAttemptId}
237
- onSelectedJobExecutionChange={selectJobExecution}
238
- onSelectedAttemptChange={(attemptId) => setSelectedAttemptId(attemptId ?? null)}
239
- renderExpandedStep={StepDetailPlaceholder}
240
- sourceAvailable={sourceAvailable}
241
- sourcePanelId={sourcePanelId}
242
- focusedSourceStepId={null}
243
- onOpenStepSource={() => undefined}
244
- />
245
- );
246
- }
247
-
248
- function StorySection({label, children}: {label: string; children: ReactNode}) {
249
- return (
250
- <div className="flex min-w-0 flex-col gap-8">
251
- <Text size="xs" className="font-code text-foreground-neutral-muted">
252
- {label}
253
- </Text>
254
- {children}
255
- </div>
256
- );
257
- }
258
-
259
- function StepDetailPlaceholder({attempt, attemptStatus}: StepExpandedContext) {
260
- return (
261
- <div className="rounded-6 border border-border-neutral-base bg-background-components-base px-12 py-10">
262
- <Text size="xs" className="font-code text-foreground-neutral-muted">
263
- attempt #{attempt} - {attemptStatus}
264
- </Text>
265
- <Text size="sm" className="text-foreground-neutral-subtle">
266
- Step detail content renders here in the run viewer.
267
- </Text>
268
- </div>
269
- );
270
- }
271
-
272
- function singleExecutionJob(): Job {
273
- return makeJob({
274
- id: 'job-build',
275
- name: 'build',
276
- status: 'succeeded',
277
- job_executions: [
278
- makeExecution({
279
- id: 'exec-1',
280
- job_id: 'job-build',
281
- sequence: 1,
282
- status: 'succeeded',
283
- queued_at: QUEUED_AT,
284
- started_at: STARTED_AT,
285
- finished_at: FINISHED_AT,
286
- steps: [
287
- makeStep('checkout', 'succeeded', 0, 'exec-1'),
288
- makeStep('install', 'succeeded', 1, 'exec-1'),
289
- makeStep('test', 'succeeded', 2, 'exec-1'),
290
- ],
291
- }),
292
- ],
293
- });
294
- }
295
-
296
- function keyFallbackJob(): Job {
297
- return makeJob({
298
- id: 'job-key-fallback',
299
- key: 'release-production',
300
- name: null,
301
- status: 'running',
302
- job_executions: [
303
- makeExecution({
304
- id: 'exec-key-fallback',
305
- job_id: 'job-key-fallback',
306
- sequence: 1,
307
- status: 'running',
308
- queued_at: QUEUED_AT,
309
- started_at: STARTED_AT,
310
- steps: [
311
- makeStep('package', 'succeeded', 0, 'exec-key-fallback'),
312
- makeStep('deploy', 'running', 1, 'exec-key-fallback'),
313
- ],
314
- }),
315
- ],
316
- });
317
- }
318
-
319
- function queueingExecutionJob(): Job {
320
- return makeJob({
321
- id: 'job-queueing',
322
- name: 'allocate-runner',
323
- status: 'pending',
324
- job_executions: [
325
- makeExecution({
326
- id: 'exec-queueing',
327
- job_id: 'job-queueing',
328
- sequence: 1,
329
- status: 'pending',
330
- queued_at: QUEUED_AT,
331
- started_at: null,
332
- finished_at: null,
333
- steps: [],
334
- }),
335
- ],
336
- });
337
- }
338
-
339
- function failedThenSucceededJob(): Job {
340
- return makeJob({
341
- id: 'job-deploy',
342
- name: 'deploy',
343
- status: 'succeeded',
344
- job_executions: [
345
- makeExecution({
346
- id: 'exec-1',
347
- job_id: 'job-deploy',
348
- sequence: 1,
349
- status: 'failed',
350
- status_reason: 'step_failed',
351
- queued_at: '2026-06-26T11:46:00.000Z',
352
- started_at: '2026-06-26T11:48:00.000Z',
353
- finished_at: '2026-06-26T11:50:30.000Z',
354
- steps: [
355
- makeStep('deploy', 'failed', 0, 'exec-1', {
356
- attempts: [
357
- workflowStepAttemptDto({
358
- id: 'attempt-deploy-1',
359
- step_id: 'step-deploy-1',
360
- status: 'failed',
361
- exit_code: 1,
362
- error: {message: 'Deployment failed'},
363
- }),
364
- ],
365
- }),
366
- ],
367
- }),
368
- makeExecution({
369
- id: 'exec-2',
370
- job_id: 'job-deploy',
371
- sequence: 2,
372
- status: 'succeeded',
373
- queued_at: QUEUED_AT,
374
- started_at: STARTED_AT,
375
- finished_at: FINISHED_AT,
376
- steps: [
377
- makeStep('deploy', 'succeeded', 0, 'exec-2'),
378
- makeStep('smoke-test', 'succeeded', 1, 'exec-2'),
379
- ],
380
- }),
381
- ],
382
- });
383
- }
384
-
385
- function runningRetryJob(): Job {
386
- return makeJob({
387
- id: 'job-release',
388
- name: 'release-production',
389
- status: 'running',
390
- job_executions: [
391
- makeExecution({
392
- id: 'exec-1',
393
- job_id: 'job-release',
394
- sequence: 1,
395
- status: 'failed',
396
- status_reason: 'step_failed',
397
- queued_at: '2026-06-26T11:46:00.000Z',
398
- started_at: '2026-06-26T11:48:00.000Z',
399
- finished_at: '2026-06-26T11:52:00.000Z',
400
- steps: [makeStep('package', 'failed', 0, 'exec-1')],
401
- }),
402
- makeExecution({
403
- id: 'exec-2',
404
- job_id: 'job-release',
405
- sequence: 2,
406
- status: 'running',
407
- queued_at: QUEUED_AT,
408
- started_at: STARTED_AT,
409
- steps: [
410
- makeStep('package', 'succeeded', 0, 'exec-2'),
411
- makeStep('publish', 'running', 1, 'exec-2', {
412
- current_attempt: 1,
413
- attempts: [
414
- workflowStepAttemptDto({
415
- id: 'attempt-publish-running',
416
- step_id: 'step-publish-exec-2',
417
- status: 'running',
418
- exit_code: null,
419
- finished_at: null,
420
- }),
421
- ],
422
- }),
423
- makeStep('notify', 'pending', 2, 'exec-2', {attempts: []}),
424
- ],
425
- }),
426
- ],
427
- });
428
- }
429
-
430
- function sourceActionJob(): Job {
431
- return makeJob({
432
- id: 'job-source-action',
433
- name: 'build',
434
- status: 'succeeded',
435
- job_executions: [
436
- makeExecution({
437
- id: 'exec-source-action',
438
- job_id: 'job-source-action',
439
- sequence: 1,
440
- status: 'succeeded',
441
- queued_at: QUEUED_AT,
442
- started_at: STARTED_AT,
443
- finished_at: FINISHED_AT,
444
- steps: [
445
- makeStep('checkout', 'succeeded', 0, 'exec-source-action', {
446
- source_location: {start_line: 12, end_line: 16},
447
- }),
448
- makeStep('install', 'succeeded', 1, 'exec-source-action'),
449
- makeStep('test', 'succeeded', 2, 'exec-source-action'),
450
- ],
451
- }),
452
- ],
453
- });
454
- }
455
-
456
- function pendingNoExecutionJob(): Job {
457
- return makeJob({
458
- id: 'job-pending-empty',
459
- name: 'wait-for-runner',
460
- status: 'pending',
461
- job_executions: [],
462
- });
463
- }
464
-
465
- function zeroExecutionJob(): Job {
466
- return makeJob({
467
- id: 'job-skipped',
468
- name: 'deploy-preview',
469
- status: 'skipped',
470
- status_reason: 'dependency_not_completed',
471
- dependencies: ['build'],
472
- job_executions: [],
473
- });
474
- }
475
-
476
- function cancelledNoExecutionJob(): Job {
477
- return makeJob({
478
- id: 'job-cancelled-empty',
479
- name: 'deploy-production',
480
- status: 'cancelled',
481
- status_reason: 'run_cancelled',
482
- job_executions: [],
483
- });
484
- }
485
-
486
- function finishedWithoutStepsJob(): Job {
487
- return makeJob({
488
- id: 'job-empty-steps',
489
- name: 'noop',
490
- status: 'succeeded',
491
- job_executions: [
492
- makeExecution({
493
- id: 'exec-empty',
494
- job_id: 'job-empty-steps',
495
- sequence: 1,
496
- status: 'succeeded',
497
- queued_at: QUEUED_AT,
498
- started_at: STARTED_AT,
499
- finished_at: FINISHED_AT,
500
- steps: [],
501
- }),
502
- ],
503
- });
504
- }
505
-
506
- function finishedWithoutExecutionJob(): Job {
507
- return makeJob({
508
- id: 'job-missing-execution',
509
- name: 'publish-artifacts',
510
- status: 'succeeded',
511
- job_executions: [],
512
- });
513
- }
514
-
515
- function carriedOverJob(): Job {
516
- return makeJob({
517
- id: 'job-test',
518
- name: 'test',
519
- status: 'succeeded',
520
- carried_over: true,
521
- job_executions: [],
522
- });
523
- }
524
-
525
- function listeningJobPending(): Job {
526
- return makeJob({
527
- id: 'job-listening-pending',
528
- name: 'release-gates',
529
- mode: 'listening',
530
- status: 'pending',
531
- listener_status: 'inactive',
532
- job_executions: [],
533
- });
534
- }
535
-
536
- function listeningJobNoExecutions(): Job {
537
- return makeJob({
538
- id: 'job-listening-empty',
539
- key: 'deploy-window',
540
- name: null,
541
- mode: 'listening',
542
- status: 'running',
543
- listener_status: 'listening',
544
- job_executions: [],
545
- });
546
- }
547
-
548
- function listeningJobResolvedNoExecutions(): Job {
549
- return makeJob({
550
- id: 'job-listening-resolved-empty',
551
- name: 'release-gates',
552
- mode: 'listening',
553
- status: 'succeeded',
554
- listener_status: 'resolved',
555
- resolution_reason: 'max_executions',
556
- job_executions: [],
557
- });
558
- }
559
-
560
- function listeningJobOneExecution(): Job {
561
- return makeJob({
562
- id: 'job-listening-one',
563
- name: 'release-gates',
564
- mode: 'listening',
565
- status: 'running',
566
- listener_status: 'listening',
567
- job_executions: [makeListeningExecution(1, 'running', 'job-listening-one', 'exec-listen-one')],
568
- });
569
- }
570
-
571
- function listeningJobMixedExecutions(): Job {
572
- return makeJob({
573
- id: 'job-listening-mixed',
574
- name: 'release-gates',
575
- mode: 'listening',
576
- status: 'running',
577
- listener_status: 'listening',
578
- job_executions: [
579
- ...Array.from({length: 2}, (_, index) =>
580
- makeListeningExecution(index + 1, 'running', 'job-listening-mixed'),
581
- ),
582
- ...Array.from({length: 3}, (_, index) =>
583
- makeListeningExecution(index + 3, 'succeeded', 'job-listening-mixed'),
584
- ),
585
- makeListeningExecution(6, 'failed', 'job-listening-mixed', 'exec-listen', {
586
- trigger_events: [
587
- makeTriggerEvent(6, 'deployment_status'),
588
- makeTriggerEvent(7, 'check_run'),
589
- ],
590
- }),
591
- ],
592
- });
593
- }
594
-
595
- function listeningJobResolved(): Job {
596
- return makeJob({
597
- id: 'job-listening-resolved',
598
- name: 'release-production-gates',
599
- mode: 'listening',
600
- status: 'failed',
601
- listener_status: 'resolved',
602
- resolution_reason: 'max_executions',
603
- job_executions: Array.from({length: 4}, (_, index) =>
604
- makeListeningExecution(
605
- index + 1,
606
- index === 3 ? 'failed' : 'succeeded',
607
- 'job-listening-resolved',
608
- 'exec-listen-resolved',
609
- ),
610
- ),
611
- });
612
- }
613
-
614
- function manyExecutionsListeningJob(name: string, id: string): Job {
615
- return makeJob({
616
- id,
617
- name,
618
- mode: 'listening',
619
- status: 'running',
620
- listener_status: 'listening',
621
- job_executions: Array.from({length: 64}, (_, index) => {
622
- const sequence = index + 1;
623
- const status = manyExecutionStatus(sequence);
624
- return makeListeningExecution(sequence, status, id, `exec-many-${id}`, {
625
- name: variedExecutionName(sequence),
626
- });
627
- }),
628
- });
629
- }
630
-
631
- function longNameManyExecutionsJob(): Job {
632
- return makeJob({
633
- id: 'job-long',
634
- name: 'release-production-multi-region-with-canary-validation-and-post-deploy-observability',
635
- status: 'running',
636
- job_executions: Array.from({length: 6}, (_, index) => {
637
- const sequence = index + 1;
638
- const status: WorkflowRunJobExecutionDetailDto['status'] =
639
- sequence < 6 ? 'failed' : 'running';
640
- return makeExecution({
641
- id: `exec-${sequence}`,
642
- job_id: 'job-long',
643
- sequence,
644
- status,
645
- status_reason: status === 'failed' ? 'step_failed' : null,
646
- queued_at: `2026-06-26T11:${String(36 + index * 3).padStart(2, '0')}:00.000Z`,
647
- started_at: `2026-06-26T11:${String(38 + index * 3).padStart(2, '0')}:00.000Z`,
648
- finished_at:
649
- status === 'failed'
650
- ? `2026-06-26T11:${String(40 + index * 3).padStart(2, '0')}:00.000Z`
651
- : null,
652
- steps: [
653
- makeStep(
654
- 'run-production-canary-smoke-tests-with-cross-region-checkout-payment-and-notification-validation',
655
- status,
656
- 0,
657
- `exec-${sequence}`,
658
- ),
659
- ],
660
- });
661
- }),
662
- });
663
- }
664
-
665
- function longNameListeningJob(): Job {
666
- return makeJob({
667
- id: 'job-long-listening',
668
- name: 'release-production-multi-region-with-canary-validation-and-post-deploy-observability',
669
- mode: 'listening',
670
- status: 'running',
671
- listener_status: 'listening',
672
- job_executions: Array.from({length: 3}, (_, index) =>
673
- makeListeningExecution(
674
- index + 1,
675
- index === 2 ? 'running' : 'succeeded',
676
- 'job-long-listening',
677
- 'exec-long-listening',
678
- ),
679
- ),
680
- });
681
- }
682
-
683
- function makeListeningExecution(
684
- sequence: number,
685
- status: WorkflowRunJobExecutionDetailDto['status'],
686
- jobId: string,
687
- idPrefix = 'exec-listen',
688
- overrides: Partial<WorkflowRunJobExecutionDetailDto> = {},
689
- ): WorkflowRunJobExecutionDetailDto {
690
- return makeExecution({
691
- id: `${idPrefix}-${sequence}`,
692
- job_id: jobId,
693
- sequence,
694
- name: `event-${sequence}`,
695
- status,
696
- status_reason: status === 'failed' ? 'step_failed' : null,
697
- queued_at: listeningTimestamp(sequence, 0),
698
- started_at: listeningTimestamp(sequence, 2),
699
- finished_at: status === 'running' ? null : listeningTimestamp(sequence, 3),
700
- trigger_events: [makeTriggerEvent(sequence, variedExecutionName(sequence))],
701
- steps: [makeStep('handle-event', status, 0, `${idPrefix}-${sequence}`)],
702
- ...overrides,
703
- });
704
- }
705
-
706
- function makeTriggerEvent(
707
- sequence: number,
708
- event = `event-${sequence}`,
709
- ): WorkflowExecutionEventDto {
710
- return {
711
- source: 'github',
712
- event,
713
- delivery_id: `delivery-${sequence}`,
714
- received_at: listeningTimestamp(sequence, 1),
715
- project: null,
716
- repository: null,
717
- ref: null,
718
- commit: null,
719
- data: {sequence},
720
- };
721
- }
722
-
723
- function manyExecutionStatus(sequence: number): WorkflowRunJobExecutionDetailDto['status'] {
724
- if (sequence === 64) return 'running';
725
- if (sequence % 11 === 0) return 'failed';
726
- if (sequence % 7 === 0) return 'cancelled';
727
- return 'succeeded';
728
- }
729
-
730
- function variedExecutionName(sequence: number): string {
731
- const names = [
732
- 'push',
733
- 'gate-opened',
734
- 'deployment-status-received',
735
- 'manual-approval-received-from-release-coordinator',
736
- 'production-canary-validation-completed-for-eu-west-and-us-east',
737
- ];
738
-
739
- return names[(sequence - 1) % names.length] ?? 'event';
740
- }
741
-
742
- function listeningTimestamp(sequence: number, offsetMinutes: number): string {
743
- return new Date(LISTENING_BASE_AT + (sequence + offsetMinutes) * 60_000).toISOString();
744
- }
745
-
746
- function makeJob(overrides: Partial<WorkflowRunJobDetailDto>): Job {
747
- return workflowJob(overrides);
748
- }
749
-
750
- function makeExecution(
751
- overrides: Partial<WorkflowRunJobExecutionDetailDto> & {
752
- id: string;
753
- job_id: string;
754
- sequence: number;
755
- },
756
- ): WorkflowRunJobExecutionDetailDto {
757
- return workflowJobExecutionDto(overrides);
758
- }
759
-
760
- function makeStep(
761
- name: string,
762
- status: string,
763
- position: number,
764
- jobExecutionId: string,
765
- overrides: Partial<WorkflowRunStepDetailDto> = {},
766
- ): WorkflowRunStepDetailDto {
767
- const stepId = `step-${name}-${jobExecutionId}`;
768
- return workflowStepDto({
769
- id: stepId,
770
- job_execution_id: jobExecutionId,
771
- name,
772
- position,
773
- status,
774
- attempts:
775
- overrides.attempts ??
776
- (status === 'pending'
777
- ? []
778
- : [
779
- workflowStepAttemptDto({
780
- id: `attempt-${name}-${jobExecutionId}`,
781
- step_id: stepId,
782
- status,
783
- exit_code: status === 'failed' ? 1 : null,
784
- finished_at: status === 'running' ? null : FINISHED_AT,
785
- }),
786
- ]),
787
- ...overrides,
788
- });
789
- }