@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.
- package/.storybook/preview.tsx +7 -2
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +21 -0
- package/dist/components/job-detail/agent-config-failure-callout.d.ts.map +1 -0
- package/dist/components/{workflow-run-view → job-detail}/agent-config-failure-callout.js +2 -2
- package/dist/components/job-detail/agent-config-failure-callout.js.map +1 -0
- package/dist/components/job-detail/job-detail-header.d.ts +8 -0
- package/dist/components/job-detail/job-detail-header.d.ts.map +1 -0
- package/dist/components/job-detail/job-detail-header.js +111 -0
- package/dist/components/job-detail/job-detail-header.js.map +1 -0
- package/dist/components/job-detail/job-detail-view.d.ts +16 -0
- package/dist/components/job-detail/job-detail-view.d.ts.map +1 -0
- package/dist/components/job-detail/job-detail-view.js +405 -0
- package/dist/components/job-detail/job-detail-view.js.map +1 -0
- package/dist/components/job-detail/job-empty-states.d.ts +10 -0
- package/dist/components/job-detail/job-empty-states.d.ts.map +1 -0
- package/dist/components/job-detail/job-empty-states.js +195 -0
- package/dist/components/job-detail/job-empty-states.js.map +1 -0
- package/dist/components/job-detail/job-execution-switcher.d.ts.map +1 -0
- package/dist/components/job-detail/job-execution-switcher.js.map +1 -0
- package/dist/components/job-detail/job-execution-time-text.d.ts.map +1 -0
- package/dist/components/job-detail/job-execution-time-text.js.map +1 -0
- package/dist/components/job-detail/job-selection.d.ts +24 -0
- package/dist/components/job-detail/job-selection.d.ts.map +1 -0
- package/dist/components/job-detail/job-selection.js +66 -0
- package/dist/components/job-detail/job-selection.js.map +1 -0
- package/dist/components/job-detail/step-attempt-log-panel.d.ts +15 -0
- package/dist/components/job-detail/step-attempt-log-panel.d.ts.map +1 -0
- package/dist/components/{workflow-run-view → job-detail}/step-attempt-log-panel.js +61 -16
- package/dist/components/job-detail/step-attempt-log-panel.js.map +1 -0
- package/dist/components/job-graph/job-graph-content.js +1 -1
- package/dist/components/job-graph/job-graph-content.js.map +1 -1
- package/dist/components/step-list/step-list-model.js +2 -2
- package/dist/components/step-list/step-list-model.js.map +1 -1
- package/dist/components/step-list/step-list.d.ts +1 -0
- package/dist/components/step-list/step-list.d.ts.map +1 -1
- package/dist/components/step-list/step-list.js +29 -4
- package/dist/components/step-list/step-list.js.map +1 -1
- package/dist/components/workflow-run-duration-label.d.ts +9 -4
- package/dist/components/workflow-run-duration-label.d.ts.map +1 -1
- package/dist/components/workflow-run-duration-label.js +27 -7
- package/dist/components/workflow-run-duration-label.js.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-row.d.ts.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-row.js +6 -5
- package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
- package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
- package/dist/components/workflow-run-summary/workflow-run-summary.js +68 -64
- package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
- package/dist/components/workflow-run-tabs/index.d.ts +0 -3
- package/dist/components/workflow-run-tabs/index.d.ts.map +1 -1
- package/dist/components/workflow-run-tabs/index.js +0 -3
- package/dist/components/workflow-run-tabs/index.js.map +1 -1
- package/dist/components/workflow-run-tabs/run-annotations-empty.d.ts +3 -1
- package/dist/components/workflow-run-tabs/run-annotations-empty.d.ts.map +1 -1
- package/dist/components/workflow-run-tabs/run-annotations-empty.js +2 -2
- package/dist/components/workflow-run-tabs/run-annotations-empty.js.map +1 -1
- package/dist/components/workflow-run-view/index.d.ts +0 -1
- package/dist/components/workflow-run-view/index.d.ts.map +1 -1
- package/dist/components/workflow-run-view/index.js +0 -1
- package/dist/components/workflow-run-view/index.js.map +1 -1
- package/dist/components/workflow-run-view/run-workspace-nav.d.ts +20 -0
- package/dist/components/workflow-run-view/run-workspace-nav.d.ts.map +1 -0
- package/dist/components/workflow-run-view/run-workspace-nav.js +271 -0
- package/dist/components/workflow-run-view/run-workspace-nav.js.map +1 -0
- package/dist/components/workflow-run-view/workflow-run-selection.d.ts.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-selection.js +1 -14
- package/dist/components/workflow-run-view/workflow-run-selection.js.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-view.d.ts +10 -8
- package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-view.js +250 -236
- package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
- package/dist/components/workflow-status/status-visuals.d.ts.map +1 -1
- package/dist/components/workflow-status/status-visuals.js +10 -2
- package/dist/components/workflow-status/status-visuals.js.map +1 -1
- package/dist/components/workflow-status/workflow-status-icon.d.ts +3 -1
- package/dist/components/workflow-status/workflow-status-icon.d.ts.map +1 -1
- package/dist/components/workflow-status/workflow-status-icon.js +13 -2
- package/dist/components/workflow-status/workflow-status-icon.js.map +1 -1
- package/dist/core/entities/job-execution.d.ts +9 -0
- package/dist/core/entities/job-execution.d.ts.map +1 -1
- package/dist/core/entities/job-execution.js +13 -3
- package/dist/core/entities/job-execution.js.map +1 -1
- package/dist/core/entities/job.d.ts.map +1 -1
- package/dist/core/entities/step-attempt.d.ts.map +1 -1
- package/dist/core/entities/step.d.ts +2 -0
- package/dist/core/entities/step.d.ts.map +1 -1
- package/dist/core/entities/step.js +13 -0
- package/dist/core/entities/step.js.map +1 -1
- package/dist/core/entities/workflow-run-attempt.d.ts.map +1 -1
- package/dist/core/entities/workflow-run.d.ts +56 -1
- package/dist/core/entities/workflow-run.d.ts.map +1 -1
- package/dist/core/entities/workflow-run.js +98 -0
- package/dist/core/entities/workflow-run.js.map +1 -1
- package/dist/core/workflow-run.d.ts +4 -4
- package/dist/core/workflow-run.d.ts.map +1 -1
- package/dist/core/workflow-run.js +3 -3
- package/dist/core/workflow-run.js.map +1 -1
- package/dist/feature.d.ts +20 -16
- package/dist/feature.d.ts.map +1 -1
- package/dist/feature.js +5 -0
- package/dist/feature.js.map +1 -1
- package/dist/hooks/api/workflow-runs.d.ts +5 -3
- package/dist/hooks/api/workflow-runs.d.ts.map +1 -1
- package/dist/hooks/api/workflow-runs.js +5 -4
- package/dist/hooks/api/workflow-runs.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/pages/workflow-job-detail-page.d.ts +11 -0
- package/dist/pages/workflow-job-detail-page.d.ts.map +1 -0
- package/dist/pages/workflow-job-detail-page.js +66 -0
- package/dist/pages/workflow-job-detail-page.js.map +1 -0
- package/dist/pages/workflow-run-detail-page.d.ts.map +1 -1
- package/dist/pages/workflow-run-detail-page.js +4 -37
- package/dist/pages/workflow-run-detail-page.js.map +1 -1
- package/dist/routes/inputs.d.ts +47 -34
- package/dist/routes/inputs.d.ts.map +1 -1
- package/dist/routes/inputs.js +69 -37
- package/dist/routes/inputs.js.map +1 -1
- package/dist/routes/job-detail.d.ts +10 -0
- package/dist/routes/job-detail.d.ts.map +1 -0
- package/dist/routes/job-detail.js +27 -0
- package/dist/routes/job-detail.js.map +1 -0
- package/dist/routes/project-route.d.ts +1 -1
- package/dist/routes/run-detail.d.ts.map +1 -1
- package/dist/routes/runs.d.ts.map +1 -1
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/components/{workflow-run-view → job-detail}/agent-config-failure-callout.stories.tsx +1 -1
- package/src/components/{workflow-run-view → job-detail}/agent-config-failure-callout.tsx +2 -2
- package/src/components/job-detail/job-detail-header.test.tsx +31 -0
- package/src/components/job-detail/job-detail-header.tsx +111 -0
- package/src/components/job-detail/job-detail-view.stories.tsx +526 -0
- package/src/components/job-detail/job-detail-view.tsx +450 -0
- package/src/components/job-detail/job-empty-states.test.ts +27 -0
- package/src/components/job-detail/job-empty-states.tsx +248 -0
- package/src/components/job-detail/job-selection.test.ts +188 -0
- package/src/components/job-detail/job-selection.ts +96 -0
- package/src/components/{workflow-run-view → job-detail}/step-attempt-log-panel.test.tsx +32 -9
- package/src/components/{workflow-run-view → job-detail}/step-attempt-log-panel.tsx +52 -18
- package/src/components/job-graph/job-graph-content.tsx +1 -1
- package/src/components/step-list/step-list-model.test.ts +1 -1
- package/src/components/step-list/step-list-model.ts +2 -2
- package/src/components/step-list/step-list.stories.tsx +1 -1
- package/src/components/step-list/step-list.test.tsx +33 -0
- package/src/components/step-list/step-list.tsx +40 -9
- package/src/components/workflow-run-duration-label.tsx +50 -12
- package/src/components/workflow-run-list/workflow-run-row.tsx +6 -4
- package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +38 -0
- package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +79 -5
- package/src/components/workflow-run-summary/workflow-run-summary.tsx +56 -60
- package/src/components/workflow-run-tabs/index.ts +0 -3
- package/src/components/workflow-run-tabs/run-annotations-empty.tsx +6 -2
- package/src/components/workflow-run-view/index.ts +0 -1
- package/src/components/workflow-run-view/run-workspace-nav.test.tsx +242 -0
- package/src/components/workflow-run-view/run-workspace-nav.tsx +301 -0
- package/src/components/workflow-run-view/workflow-run-selection.ts +1 -23
- package/src/components/workflow-run-view/workflow-run-view.test.tsx +62 -1136
- package/src/components/workflow-run-view/workflow-run-view.tsx +330 -321
- package/src/components/workflow-status/status-visuals.ts +5 -2
- package/src/components/workflow-status/workflow-status-icon.stories.tsx +1 -0
- package/src/components/workflow-status/workflow-status-icon.tsx +26 -1
- package/src/core/entities/job-execution.ts +17 -2
- package/src/core/entities/step.ts +26 -0
- package/src/core/entities/workflow-run-display.test.ts +159 -0
- package/src/core/entities/workflow-run.ts +139 -1
- package/src/core/workflow-run.ts +16 -1
- package/src/feature.ts +5 -0
- package/src/hooks/api/workflow-runs.ts +6 -2
- package/src/index.ts +1 -0
- package/src/page-harness-budget.test.ts +1 -0
- package/src/pages/workflow-job-detail-page.test.tsx +539 -0
- package/src/pages/workflow-job-detail-page.tsx +84 -0
- package/src/pages/workflow-run-detail-page.tsx +3 -43
- package/src/pages/workflow-run-pages.test.tsx +77 -332
- package/src/routes/inputs.test.ts +56 -6
- package/src/routes/inputs.ts +64 -17
- package/src/routes/job-detail.tsx +29 -0
- package/test/pages.tsx +25 -3
- package/test/render.tsx +6 -1
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/components/workflow-run-tabs/run-jobs-list.d.ts +0 -18
- package/dist/components/workflow-run-tabs/run-jobs-list.d.ts.map +0 -1
- package/dist/components/workflow-run-tabs/run-jobs-list.js +0 -107
- package/dist/components/workflow-run-tabs/run-jobs-list.js.map +0 -1
- package/dist/components/workflow-run-tabs/run-tab-count.d.ts +0 -6
- package/dist/components/workflow-run-tabs/run-tab-count.d.ts.map +0 -1
- package/dist/components/workflow-run-tabs/run-tab-count.js +0 -27
- package/dist/components/workflow-run-tabs/run-tab-count.js.map +0 -1
- package/dist/components/workflow-run-tabs/run-tab-strip.d.ts +0 -8
- package/dist/components/workflow-run-tabs/run-tab-strip.d.ts.map +0 -1
- package/dist/components/workflow-run-tabs/run-tab-strip.js +0 -82
- package/dist/components/workflow-run-tabs/run-tab-strip.js.map +0 -1
- package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts.map +0 -1
- package/dist/components/workflow-run-view/agent-config-failure-callout.js.map +0 -1
- package/dist/components/workflow-run-view/job-card.d.ts +0 -17
- package/dist/components/workflow-run-view/job-card.d.ts.map +0 -1
- package/dist/components/workflow-run-view/job-card.js +0 -496
- package/dist/components/workflow-run-view/job-card.js.map +0 -1
- package/dist/components/workflow-run-view/job-execution-switcher.d.ts.map +0 -1
- package/dist/components/workflow-run-view/job-execution-switcher.js.map +0 -1
- package/dist/components/workflow-run-view/job-execution-time-text.d.ts.map +0 -1
- package/dist/components/workflow-run-view/job-execution-time-text.js.map +0 -1
- package/dist/components/workflow-run-view/step-attempt-log-panel.d.ts +0 -9
- package/dist/components/workflow-run-view/step-attempt-log-panel.d.ts.map +0 -1
- package/dist/components/workflow-run-view/step-attempt-log-panel.js.map +0 -1
- package/src/components/workflow-run-tabs/run-jobs-list.tsx +0 -151
- package/src/components/workflow-run-tabs/run-tab-count.tsx +0 -28
- package/src/components/workflow-run-tabs/run-tab-strip.stories.tsx +0 -52
- package/src/components/workflow-run-tabs/run-tab-strip.test.tsx +0 -117
- package/src/components/workflow-run-tabs/run-tab-strip.tsx +0 -97
- package/src/components/workflow-run-view/job-card.stories.tsx +0 -789
- package/src/components/workflow-run-view/job-card.test.tsx +0 -115
- package/src/components/workflow-run-view/job-card.tsx +0 -614
- /package/dist/components/{workflow-run-view → job-detail}/agent-config-failure-callout.d.ts +0 -0
- /package/dist/components/{workflow-run-view → job-detail}/job-execution-switcher.d.ts +0 -0
- /package/dist/components/{workflow-run-view → job-detail}/job-execution-switcher.js +0 -0
- /package/dist/components/{workflow-run-view → job-detail}/job-execution-time-text.d.ts +0 -0
- /package/dist/components/{workflow-run-view → job-detail}/job-execution-time-text.js +0 -0
- /package/src/components/{workflow-run-view → job-detail}/job-execution-switcher.tsx +0 -0
- /package/src/components/{workflow-run-view → job-detail}/job-execution-time-text.tsx +0 -0
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import {render, screen, waitFor, within} from '@testing-library/react';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
3
|
import type {workflowRunDetailDto} from '#test/fixtures/workflow-run.js';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
workflowJobDto,
|
|
6
|
+
workflowJobExecutionDto,
|
|
7
|
+
workflowRunDetail,
|
|
8
|
+
} from '#test/fixtures/workflow-run.js';
|
|
5
9
|
import {WorkflowRunSummary} from './workflow-run-summary.js';
|
|
6
10
|
|
|
7
11
|
const RUN_ID = '66666666-6666-4666-8666-666666666666';
|
|
@@ -125,9 +129,9 @@ describe('WorkflowRunSummary', () => {
|
|
|
125
129
|
const heading = await screen.findByRole('heading', {name: runName});
|
|
126
130
|
|
|
127
131
|
expect(heading).toHaveAttribute('data-slot', 'tooltip-trigger');
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
);
|
|
132
|
+
// The Radix tooltip is the only name affordance: a native `title` beside it would fire a
|
|
133
|
+
// second, unstyled OS tooltip with the same text.
|
|
134
|
+
await waitFor(() => expect(within(heading).getByText(runName)).not.toHaveAttribute('title'));
|
|
131
135
|
});
|
|
132
136
|
|
|
133
137
|
test('shows the selected attempt duration, not the top-level run duration', async () => {
|
|
@@ -174,6 +178,61 @@ describe('WorkflowRunSummary', () => {
|
|
|
174
178
|
expect(duration).toHaveAttribute('aria-label', 'running 2m 14s');
|
|
175
179
|
});
|
|
176
180
|
|
|
181
|
+
test('reads a running run whose jobs have not started as queued', async () => {
|
|
182
|
+
vi.spyOn(Date, 'now').mockReturnValue(Date.parse('2026-05-07T01:02:14.000Z'));
|
|
183
|
+
renderSummary({
|
|
184
|
+
jobs: [workflowJobDto({status: 'pending'})],
|
|
185
|
+
run_attempt: runningAttemptDto(),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const summary = await screen.findByRole('region', {name: 'deploy-web'});
|
|
189
|
+
|
|
190
|
+
expect(within(summary).getAllByText('Queued')).not.toHaveLength(0);
|
|
191
|
+
expect(within(summary).queryByText('Running')).not.toBeInTheDocument();
|
|
192
|
+
expect(within(summary).getByText('2m 14s')).toHaveAttribute('aria-label', 'queued 2m 14s');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('names the job a queued run is waiting on when there is more than one', async () => {
|
|
196
|
+
renderSummary({
|
|
197
|
+
jobs: [
|
|
198
|
+
workflowJobDto({
|
|
199
|
+
name: 'lint',
|
|
200
|
+
status: 'pending',
|
|
201
|
+
job_executions: [workflowJobExecutionDto({queued_at: '2026-05-07T01:00:00.000Z'})],
|
|
202
|
+
}),
|
|
203
|
+
workflowJobDto({
|
|
204
|
+
name: 'build',
|
|
205
|
+
status: 'pending',
|
|
206
|
+
job_executions: [workflowJobExecutionDto({queued_at: '2026-05-07T00:59:00.000Z'})],
|
|
207
|
+
}),
|
|
208
|
+
],
|
|
209
|
+
run_attempt: runningAttemptDto(),
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const summary = await screen.findByRole('region', {name: 'deploy-web'});
|
|
213
|
+
|
|
214
|
+
expect(within(summary).getByText('waiting on')).toBeInTheDocument();
|
|
215
|
+
expect(within(summary).getByText('build')).toBeInTheDocument();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('leaves the waiting job unnamed when the run has a single job', async () => {
|
|
219
|
+
renderSummary({
|
|
220
|
+
jobs: [
|
|
221
|
+
workflowJobDto({
|
|
222
|
+
name: 'build',
|
|
223
|
+
status: 'pending',
|
|
224
|
+
job_executions: [workflowJobExecutionDto({queued_at: '2026-05-07T01:00:00.000Z'})],
|
|
225
|
+
}),
|
|
226
|
+
],
|
|
227
|
+
run_attempt: runningAttemptDto(),
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const summary = await screen.findByRole('region', {name: 'deploy-web'});
|
|
231
|
+
|
|
232
|
+
expect(within(summary).getAllByText('Queued')).not.toHaveLength(0);
|
|
233
|
+
expect(within(summary).queryByText('waiting on')).not.toBeInTheDocument();
|
|
234
|
+
});
|
|
235
|
+
|
|
177
236
|
test('does not render a whole-run source control', async () => {
|
|
178
237
|
renderSummary();
|
|
179
238
|
|
|
@@ -250,7 +309,9 @@ describe('WorkflowRunSummary', () => {
|
|
|
250
309
|
const onRerun = vi.fn();
|
|
251
310
|
renderSummary({status: 'failed', jobs: [workflowJobDto({status: 'failed'})]}, {onRerun});
|
|
252
311
|
|
|
253
|
-
|
|
312
|
+
const rerunButton = await screen.findByRole('button', {name: 'Re-run jobs'});
|
|
313
|
+
expect(rerunButton).toHaveClass('bg-background-neutral-base');
|
|
314
|
+
await user.click(rerunButton);
|
|
254
315
|
expect(await screen.findByRole('menuitem', {name: 'Re-run all jobs'})).toBeInTheDocument();
|
|
255
316
|
await user.click(await screen.findByRole('menuitem', {name: 'Re-run failed jobs'}));
|
|
256
317
|
|
|
@@ -328,6 +389,19 @@ function renderSummary(
|
|
|
328
389
|
render(<WorkflowRunSummary run={run} {...props} />);
|
|
329
390
|
}
|
|
330
391
|
|
|
392
|
+
function runningAttemptDto() {
|
|
393
|
+
return {
|
|
394
|
+
id: '11111111-1111-4111-8111-000000000001',
|
|
395
|
+
workflow_run_id: RUN_ID,
|
|
396
|
+
attempt: 1,
|
|
397
|
+
status: 'running' as const,
|
|
398
|
+
created_at: '2026-05-07T01:01:00.000Z',
|
|
399
|
+
started_at: '2026-05-07T01:00:00.000Z',
|
|
400
|
+
finished_at: null,
|
|
401
|
+
rerun_mode: null,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
331
405
|
function setElementWidths({scrollWidth, clientWidth}: {scrollWidth: number; clientWidth: number}) {
|
|
332
406
|
Object.defineProperty(window.HTMLElement.prototype, 'scrollWidth', {
|
|
333
407
|
configurable: true,
|
|
@@ -11,8 +11,7 @@ import {useIsTextTruncated} from '@shipfox/react-ui/hooks';
|
|
|
11
11
|
import {RelativeTime} from '@shipfox/react-ui/relative-time';
|
|
12
12
|
import {TimeTickerProvider} from '@shipfox/react-ui/time-ticker';
|
|
13
13
|
import {Tooltip, TooltipContent, TooltipTrigger} from '@shipfox/react-ui/tooltip';
|
|
14
|
-
import {Header, Text} from '@shipfox/react-ui/typography';
|
|
15
|
-
import {Link} from '@tanstack/react-router';
|
|
14
|
+
import {Code, Header, Text} from '@shipfox/react-ui/typography';
|
|
16
15
|
import {useId} from 'react';
|
|
17
16
|
import {WorkflowRunNumberLabel} from '#components/workflow-run-number-label.js';
|
|
18
17
|
import {
|
|
@@ -21,15 +20,20 @@ import {
|
|
|
21
20
|
WORKFLOW_RUN_STATUSES,
|
|
22
21
|
type WorkflowRunDetail,
|
|
23
22
|
type WorkflowRunRerunMode,
|
|
23
|
+
workflowRunBlockingJob,
|
|
24
|
+
workflowRunDetailDisplay,
|
|
24
25
|
} from '#core/workflow-run.js';
|
|
25
|
-
import {validateWorkflowRunsSearch, workflowRunListSearchParams} from '#routes/inputs.js';
|
|
26
26
|
import {WorkflowRunDurationLabel} from '../workflow-run-duration-label.js';
|
|
27
27
|
import {getWorkflowStatusVisual} from '../workflow-status/status-visuals.js';
|
|
28
28
|
import {WorkflowRunAttemptSwitcher} from './workflow-run-attempt-switcher.js';
|
|
29
29
|
|
|
30
30
|
const STATUS_BADGE_LABEL_WIDTH_CH = Math.max(
|
|
31
|
-
...WORKFLOW_RUN_STATUSES.map(
|
|
31
|
+
...[...WORKFLOW_RUN_STATUSES, 'queued' as const].map(
|
|
32
|
+
(status) => getWorkflowStatusVisual(status).label.length,
|
|
33
|
+
),
|
|
32
34
|
);
|
|
35
|
+
const RERUN_BUTTON_SURFACE_CLASS_NAME =
|
|
36
|
+
'bg-background-neutral-base hover:bg-background-neutral-hover active:bg-background-neutral-pressed';
|
|
33
37
|
|
|
34
38
|
type WorkflowRunAction = 'cancel' | 'rerun-all' | 'rerun-menu' | 'none';
|
|
35
39
|
|
|
@@ -55,14 +59,19 @@ export function WorkflowRunSummary({
|
|
|
55
59
|
latestAttempt,
|
|
56
60
|
}: WorkflowRunSummaryProps) {
|
|
57
61
|
const headingId = useId();
|
|
58
|
-
const
|
|
62
|
+
const display = workflowRunDetailDisplay(run);
|
|
63
|
+
const status = getWorkflowStatusVisual(display.status);
|
|
59
64
|
const action = workflowRunActionForRun(run);
|
|
60
65
|
const hasAction = canRenderWorkflowRunAction(action, onCancel, onRerun);
|
|
61
66
|
const attemptSwitcher =
|
|
62
67
|
latestAttempt && latestAttempt > 1 && workspaceSlug && projectSlug
|
|
63
68
|
? {workspaceSlug, projectSlug, latestAttempt}
|
|
64
69
|
: null;
|
|
65
|
-
const displayDuration =
|
|
70
|
+
const displayDuration = display.duration;
|
|
71
|
+
// Named only while the run is waiting and has more than one job: with a single job the rail
|
|
72
|
+
// and the graph already say which one it is.
|
|
73
|
+
const blockingJob =
|
|
74
|
+
display.status === 'queued' && run.jobs.length > 1 ? workflowRunBlockingJob(run.jobs) : null;
|
|
66
75
|
const {ref: headingTextRef, isTruncated: isHeadingTruncated} =
|
|
67
76
|
useIsTextTruncated<HTMLSpanElement>(run.name);
|
|
68
77
|
|
|
@@ -70,61 +79,32 @@ export function WorkflowRunSummary({
|
|
|
70
79
|
<TimeTickerProvider intervalMs={1000} reducedMotionIntervalMs={10_000}>
|
|
71
80
|
<section aria-labelledby={headingId} className="bg-background-neutral-background px-16 py-12">
|
|
72
81
|
<div className="grid min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-x-12 gap-y-8 overflow-hidden max-[480px]:grid-cols-1">
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
<Link
|
|
81
|
-
to="/w/$workspaceSlug/p/$projectSlug/runs"
|
|
82
|
-
params={{workspaceSlug, projectSlug}}
|
|
83
|
-
search={
|
|
84
|
-
((previous: Record<string, unknown>) =>
|
|
85
|
-
workflowRunListSearchParams(validateWorkflowRunsSearch(previous))) as never
|
|
86
|
-
}
|
|
87
|
-
className="rounded-4 text-xs font-medium text-foreground-neutral-subtle underline decoration-border-neutral-strong underline-offset-4 outline-none hover:text-foreground-neutral-base focus-visible:shadow-border-interactive-with-active"
|
|
88
|
-
>
|
|
89
|
-
Runs
|
|
90
|
-
</Link>
|
|
91
|
-
</li>
|
|
92
|
-
) : null}
|
|
93
|
-
{workspaceSlug && projectSlug ? (
|
|
94
|
-
<li aria-hidden="true" className="shrink-0 text-xs text-foreground-neutral-subtle">
|
|
95
|
-
/
|
|
96
|
-
</li>
|
|
97
|
-
) : null}
|
|
98
|
-
<li aria-current="page" className="flex min-w-0 items-center gap-8">
|
|
99
|
-
<Badge variant={status.badge} size="xs">
|
|
100
|
-
<span className="text-center" style={{width: `${STATUS_BADGE_LABEL_WIDTH_CH}ch`}}>
|
|
101
|
-
{status.label}
|
|
102
|
-
</span>
|
|
103
|
-
</Badge>
|
|
82
|
+
<div className="col-start-1 row-start-1 min-w-0 max-[480px]:col-start-auto max-[480px]:row-start-auto">
|
|
83
|
+
<div className="flex min-w-0 items-center gap-8">
|
|
84
|
+
<Badge variant={status.badge} size="xs">
|
|
85
|
+
<span className="text-center" style={{width: `${STATUS_BADGE_LABEL_WIDTH_CH}ch`}}>
|
|
86
|
+
{status.label}
|
|
87
|
+
</span>
|
|
88
|
+
</Badge>
|
|
104
89
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
) : null}
|
|
124
|
-
</Tooltip>
|
|
125
|
-
</li>
|
|
126
|
-
</ol>
|
|
127
|
-
</nav>
|
|
90
|
+
<Tooltip>
|
|
91
|
+
<TooltipTrigger asChild>
|
|
92
|
+
<Header id={headingId} variant="h3" className="min-w-0 truncate">
|
|
93
|
+
<span ref={headingTextRef} className="block min-w-0 truncate">
|
|
94
|
+
{run.name}
|
|
95
|
+
</span>
|
|
96
|
+
</Header>
|
|
97
|
+
</TooltipTrigger>
|
|
98
|
+
{isHeadingTruncated ? (
|
|
99
|
+
<TooltipContent>
|
|
100
|
+
<Text as="span" size="xs" className="max-w-[360px] break-words">
|
|
101
|
+
{run.name}
|
|
102
|
+
</Text>
|
|
103
|
+
</TooltipContent>
|
|
104
|
+
) : null}
|
|
105
|
+
</Tooltip>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
128
108
|
|
|
129
109
|
{hasAction ? (
|
|
130
110
|
<div className="col-start-2 row-start-1 flex min-w-max items-center gap-6 justify-self-end max-[480px]:col-start-auto max-[480px]:row-start-auto max-[480px]:justify-self-start">
|
|
@@ -204,6 +184,20 @@ export function WorkflowRunSummary({
|
|
|
204
184
|
/>
|
|
205
185
|
</>
|
|
206
186
|
) : null}
|
|
187
|
+
|
|
188
|
+
{blockingJob ? (
|
|
189
|
+
<>
|
|
190
|
+
<MetadataSeparator />
|
|
191
|
+
<span className="flex min-w-0 items-center gap-4">
|
|
192
|
+
<Text as="span" size="xs" className="shrink-0">
|
|
193
|
+
waiting on
|
|
194
|
+
</Text>
|
|
195
|
+
<Code as="span" variant="label" className="min-w-0 truncate">
|
|
196
|
+
{blockingJob.displayName}
|
|
197
|
+
</Code>
|
|
198
|
+
</span>
|
|
199
|
+
</>
|
|
200
|
+
) : null}
|
|
207
201
|
</div>
|
|
208
202
|
</div>
|
|
209
203
|
</section>
|
|
@@ -251,6 +245,7 @@ function WorkflowRunActionSlot({
|
|
|
251
245
|
type="button"
|
|
252
246
|
variant="secondary"
|
|
253
247
|
size="xs"
|
|
248
|
+
className={RERUN_BUTTON_SURFACE_CLASS_NAME}
|
|
254
249
|
isLoading={rerunPending}
|
|
255
250
|
disabled={rerunPending}
|
|
256
251
|
onClick={() => onRerun('all')}
|
|
@@ -269,6 +264,7 @@ function WorkflowRunActionSlot({
|
|
|
269
264
|
type="button"
|
|
270
265
|
variant="secondary"
|
|
271
266
|
size="xs"
|
|
267
|
+
className={RERUN_BUTTON_SURFACE_CLASS_NAME}
|
|
272
268
|
iconRight="arrowDownSLine"
|
|
273
269
|
isLoading={rerunPending}
|
|
274
270
|
disabled={rerunPending}
|
|
@@ -1,5 +1,2 @@
|
|
|
1
1
|
export {RunAnnotationSummaryLine} from './run-annotation-summary-line.js';
|
|
2
2
|
export {RunAnnotationsEmpty} from './run-annotations-empty.js';
|
|
3
|
-
export {RunJobsList} from './run-jobs-list.js';
|
|
4
|
-
export {RunTabCount} from './run-tab-count.js';
|
|
5
|
-
export {RunTabStrip} from './run-tab-strip.js';
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import {EmptyState} from '@shipfox/react-ui/empty-state';
|
|
2
2
|
|
|
3
|
-
export function RunAnnotationsEmpty() {
|
|
3
|
+
export function RunAnnotationsEmpty({jobName}: {jobName?: string | undefined} = {}) {
|
|
4
4
|
return (
|
|
5
5
|
<EmptyState
|
|
6
6
|
icon="fileDamageLine"
|
|
7
7
|
title="No annotations"
|
|
8
|
-
description=
|
|
8
|
+
description={
|
|
9
|
+
jobName
|
|
10
|
+
? `${jobName} has no annotations to show in this run.`
|
|
11
|
+
: 'This run has no annotations to show.'
|
|
12
|
+
}
|
|
9
13
|
/>
|
|
10
14
|
);
|
|
11
15
|
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import {act, screen, within} from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import {useState} from 'react';
|
|
4
|
+
import {
|
|
5
|
+
workflowJobDto,
|
|
6
|
+
workflowJobExecutionDto,
|
|
7
|
+
workflowRunDetail,
|
|
8
|
+
} from '#test/fixtures/workflow-run.js';
|
|
9
|
+
import {renderWithRouter} from '#test/render.js';
|
|
10
|
+
import {RunWorkspaceNav} from './run-workspace-nav.js';
|
|
11
|
+
|
|
12
|
+
const RUN_ID = '66666666-6666-4666-8666-666666666666';
|
|
13
|
+
const CURRENT_JOB_ID = '88888888-8888-4888-8888-888888888888';
|
|
14
|
+
const BUILD_LINK_PATTERN = /build/;
|
|
15
|
+
const DEPLOY_LINK_PATTERN = /deploy/;
|
|
16
|
+
const NOW = Date.parse('2026-06-26T12:00:00.000Z');
|
|
17
|
+
|
|
18
|
+
describe('RunWorkspaceNav', () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', {
|
|
21
|
+
configurable: true,
|
|
22
|
+
value: vi.fn(),
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
vi.useRealTimers();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('shows the complete run hierarchy and marks the current job', async () => {
|
|
31
|
+
const run = workflowRunDetail({
|
|
32
|
+
id: RUN_ID,
|
|
33
|
+
jobs: [
|
|
34
|
+
workflowJobDto({id: 'job-deploy', name: 'deploy', position: 2, status: 'failed'}),
|
|
35
|
+
workflowJobDto({id: CURRENT_JOB_ID, name: 'build', position: 1, status: 'running'}),
|
|
36
|
+
workflowJobDto({id: 'job-setup', name: 'setup', position: 0, status: 'succeeded'}),
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
renderWithRouter(
|
|
41
|
+
<RunWorkspaceNav
|
|
42
|
+
workspaceSlug="acme"
|
|
43
|
+
projectSlug="project"
|
|
44
|
+
run={run}
|
|
45
|
+
activeSection="summary"
|
|
46
|
+
currentJobId={CURRENT_JOB_ID}
|
|
47
|
+
jobSearch={{runAttempt: 2}}
|
|
48
|
+
/>,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
expect(await screen.findByRole('link', {name: 'Summary'})).toBeInTheDocument();
|
|
52
|
+
expect(screen.getByRole('heading', {name: 'Jobs'})).toBeInTheDocument();
|
|
53
|
+
expect(screen.getByRole('heading', {name: 'Run details'})).toBeInTheDocument();
|
|
54
|
+
expect(screen.getByRole('link', {name: 'Annotations'})).toBeInTheDocument();
|
|
55
|
+
expect(screen.getByRole('link', {name: 'Source'})).toBeInTheDocument();
|
|
56
|
+
|
|
57
|
+
const jobs = screen.getByRole('heading', {name: 'Jobs'}).closest('section');
|
|
58
|
+
if (!jobs) throw new Error('Missing jobs section');
|
|
59
|
+
expect(
|
|
60
|
+
within(jobs)
|
|
61
|
+
.getAllByRole('link')
|
|
62
|
+
.map((link) => link.textContent?.trim()),
|
|
63
|
+
).toEqual([
|
|
64
|
+
expect.stringContaining('setup'),
|
|
65
|
+
expect.stringContaining('build'),
|
|
66
|
+
expect.stringContaining('deploy'),
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
const current = within(jobs).getByRole('link', {name: BUILD_LINK_PATTERN});
|
|
70
|
+
expect(current).toHaveAttribute('aria-current', 'page');
|
|
71
|
+
expect(current).toHaveAttribute(
|
|
72
|
+
'href',
|
|
73
|
+
expect.stringContaining(`/runs/${RUN_ID}/jobs/${CURRENT_JOB_ID}`),
|
|
74
|
+
);
|
|
75
|
+
expect(current).not.toHaveAttribute('href', expect.stringContaining('tab='));
|
|
76
|
+
expect(current).toHaveAttribute('href', expect.stringContaining('runAttempt=%222%22'));
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('keeps the job index visible for a single-job run', async () => {
|
|
80
|
+
const run = workflowRunDetail({
|
|
81
|
+
id: RUN_ID,
|
|
82
|
+
jobs: [workflowJobDto({id: CURRENT_JOB_ID, name: 'build', status: 'succeeded'})],
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
renderWithRouter(
|
|
86
|
+
<RunWorkspaceNav
|
|
87
|
+
workspaceSlug="acme"
|
|
88
|
+
projectSlug="project"
|
|
89
|
+
run={run}
|
|
90
|
+
activeSection="summary"
|
|
91
|
+
/>,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(await screen.findByRole('heading', {name: 'Jobs'})).toBeInTheDocument();
|
|
95
|
+
expect(screen.getByRole('link', {name: BUILD_LINK_PATTERN})).toBeInTheDocument();
|
|
96
|
+
expect(screen.getByRole('link', {name: 'Summary'})).toHaveAttribute('aria-current', 'page');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('scrolls the current job into view when the mobile rail opens', async () => {
|
|
100
|
+
const user = userEvent.setup();
|
|
101
|
+
const scrollIntoView = vi.fn();
|
|
102
|
+
Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', {
|
|
103
|
+
configurable: true,
|
|
104
|
+
value: scrollIntoView,
|
|
105
|
+
});
|
|
106
|
+
const run = workflowRunDetail({
|
|
107
|
+
id: RUN_ID,
|
|
108
|
+
jobs: [workflowJobDto({id: CURRENT_JOB_ID, name: 'build', status: 'running'})],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
renderWithRouter(
|
|
112
|
+
<RunWorkspaceNav
|
|
113
|
+
workspaceSlug="acme"
|
|
114
|
+
projectSlug="project"
|
|
115
|
+
run={run}
|
|
116
|
+
activeSection="summary"
|
|
117
|
+
currentJobId={CURRENT_JOB_ID}
|
|
118
|
+
/>,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
await screen.findByRole('link', {name: BUILD_LINK_PATTERN});
|
|
122
|
+
await user.click(await screen.findByRole('button', {name: 'Toggle run navigation'}));
|
|
123
|
+
|
|
124
|
+
expect(scrollIntoView).toHaveBeenCalledTimes(2);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('closes the mobile rail after navigating to a job', async () => {
|
|
128
|
+
const user = userEvent.setup();
|
|
129
|
+
const run = workflowRunDetail({
|
|
130
|
+
id: RUN_ID,
|
|
131
|
+
jobs: [
|
|
132
|
+
workflowJobDto({id: CURRENT_JOB_ID, name: 'build', status: 'running'}),
|
|
133
|
+
workflowJobDto({id: 'job-deploy', name: 'deploy', position: 1, status: 'succeeded'}),
|
|
134
|
+
],
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
renderWithRouter(
|
|
138
|
+
<RunWorkspaceNav
|
|
139
|
+
workspaceSlug="acme"
|
|
140
|
+
projectSlug="project"
|
|
141
|
+
run={run}
|
|
142
|
+
activeSection="summary"
|
|
143
|
+
currentJobId={CURRENT_JOB_ID}
|
|
144
|
+
/>,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const trigger = await screen.findByRole('button', {name: 'Toggle run navigation'});
|
|
148
|
+
await user.click(trigger);
|
|
149
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
150
|
+
|
|
151
|
+
await user.click(screen.getByRole('link', {name: DEPLOY_LINK_PATTERN}));
|
|
152
|
+
|
|
153
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('scrolls once when the active job changes while the rail is open', async () => {
|
|
157
|
+
const user = userEvent.setup();
|
|
158
|
+
const scrollIntoView = vi.fn();
|
|
159
|
+
Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', {
|
|
160
|
+
configurable: true,
|
|
161
|
+
value: scrollIntoView,
|
|
162
|
+
});
|
|
163
|
+
const firstJob = workflowJobDto({id: CURRENT_JOB_ID, name: 'build', status: 'running'});
|
|
164
|
+
const secondJob = workflowJobDto({
|
|
165
|
+
id: 'job-deploy',
|
|
166
|
+
name: 'deploy',
|
|
167
|
+
position: 1,
|
|
168
|
+
status: 'running',
|
|
169
|
+
});
|
|
170
|
+
const initialRun = workflowRunDetail({id: RUN_ID, jobs: [firstJob, secondJob]});
|
|
171
|
+
renderWithRouter(<ActiveJobHarness run={initialRun} />);
|
|
172
|
+
|
|
173
|
+
await screen.findByRole('link', {name: BUILD_LINK_PATTERN});
|
|
174
|
+
await user.click(await screen.findByRole('button', {name: 'Toggle run navigation'}));
|
|
175
|
+
const callsBeforeJobChange = scrollIntoView.mock.calls.length;
|
|
176
|
+
|
|
177
|
+
await user.click(screen.getByRole('button', {name: 'Activate deploy'}));
|
|
178
|
+
|
|
179
|
+
expect(scrollIntoView).toHaveBeenCalledTimes(callsBeforeJobChange + 1);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('updates live job durations on the ticker cadence', async () => {
|
|
183
|
+
vi.useFakeTimers({shouldAdvanceTime: true});
|
|
184
|
+
vi.setSystemTime(NOW);
|
|
185
|
+
const run = workflowRunDetail({
|
|
186
|
+
id: RUN_ID,
|
|
187
|
+
jobs: [
|
|
188
|
+
workflowJobDto({
|
|
189
|
+
id: CURRENT_JOB_ID,
|
|
190
|
+
name: 'build',
|
|
191
|
+
status: 'running',
|
|
192
|
+
job_executions: [
|
|
193
|
+
workflowJobExecutionDto({
|
|
194
|
+
job_id: CURRENT_JOB_ID,
|
|
195
|
+
status: 'running',
|
|
196
|
+
queued_at: '2026-06-26T11:54:00.000Z',
|
|
197
|
+
started_at: '2026-06-26T11:57:46.000Z',
|
|
198
|
+
}),
|
|
199
|
+
],
|
|
200
|
+
}),
|
|
201
|
+
],
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
renderWithRouter(
|
|
205
|
+
<RunWorkspaceNav
|
|
206
|
+
workspaceSlug="acme"
|
|
207
|
+
projectSlug="project"
|
|
208
|
+
run={run}
|
|
209
|
+
activeSection="summary"
|
|
210
|
+
currentJobId={CURRENT_JOB_ID}
|
|
211
|
+
/>,
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const jobLink = await screen.findByRole('link', {name: BUILD_LINK_PATTERN});
|
|
215
|
+
expect(jobLink).toHaveTextContent('2m 14s');
|
|
216
|
+
|
|
217
|
+
act(() => {
|
|
218
|
+
vi.advanceTimersByTime(1000);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
expect(jobLink).toHaveTextContent('2m 15s');
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
function ActiveJobHarness({run}: {run: ReturnType<typeof workflowRunDetail>}) {
|
|
226
|
+
const [currentJobId, setCurrentJobId] = useState(CURRENT_JOB_ID);
|
|
227
|
+
|
|
228
|
+
return (
|
|
229
|
+
<>
|
|
230
|
+
<RunWorkspaceNav
|
|
231
|
+
workspaceSlug="acme"
|
|
232
|
+
projectSlug="project"
|
|
233
|
+
run={run}
|
|
234
|
+
activeSection="summary"
|
|
235
|
+
currentJobId={currentJobId}
|
|
236
|
+
/>
|
|
237
|
+
<button type="button" onClick={() => setCurrentJobId('job-deploy')}>
|
|
238
|
+
Activate deploy
|
|
239
|
+
</button>
|
|
240
|
+
</>
|
|
241
|
+
);
|
|
242
|
+
}
|