@shipfox/client-workflows 12.0.1 → 13.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 +3 -3
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +65 -0
- package/dist/components/job-graph/job-duration-format.d.ts +2 -2
- package/dist/components/job-graph/job-duration-format.d.ts.map +1 -1
- package/dist/components/job-graph/job-duration-format.js +4 -2
- package/dist/components/job-graph/job-duration-format.js.map +1 -1
- package/dist/components/job-graph/job-node.d.ts +1 -1
- package/dist/components/job-graph/job-node.d.ts.map +1 -1
- package/dist/components/job-graph/job-node.js +5 -4
- package/dist/components/job-graph/job-node.js.map +1 -1
- package/dist/components/step-list/step-list.d.ts +2 -2
- package/dist/components/step-list/step-list.d.ts.map +1 -1
- package/dist/components/step-list/step-list.js.map +1 -1
- package/dist/components/workflow-run-list/job-status-strip.d.ts +21 -0
- package/dist/components/workflow-run-list/job-status-strip.d.ts.map +1 -0
- package/dist/components/workflow-run-list/job-status-strip.js +185 -0
- package/dist/components/workflow-run-list/job-status-strip.js.map +1 -0
- package/dist/components/workflow-run-list/run-display.d.ts +27 -3
- package/dist/components/workflow-run-list/run-display.d.ts.map +1 -1
- package/dist/components/workflow-run-list/run-display.js +78 -7
- package/dist/components/workflow-run-list/run-display.js.map +1 -1
- package/dist/components/workflow-run-list/types.d.ts +23 -24
- package/dist/components/workflow-run-list/types.d.ts.map +1 -1
- package/dist/components/workflow-run-list/types.js.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-filter-menu.d.ts +22 -0
- package/dist/components/workflow-run-list/workflow-run-filter-menu.d.ts.map +1 -0
- package/dist/components/workflow-run-list/workflow-run-filter-menu.js +68 -0
- package/dist/components/workflow-run-list/workflow-run-filter-menu.js.map +1 -0
- package/dist/components/workflow-run-list/workflow-run-filters.d.ts +18 -0
- package/dist/components/workflow-run-list/workflow-run-filters.d.ts.map +1 -0
- package/dist/components/workflow-run-list/workflow-run-filters.js +236 -0
- package/dist/components/workflow-run-list/workflow-run-filters.js.map +1 -0
- package/dist/components/workflow-run-list/workflow-run-list-content.d.ts +8 -4
- package/dist/components/workflow-run-list/workflow-run-list-content.d.ts.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-list-content.js +40 -12
- package/dist/components/workflow-run-list/workflow-run-list-content.js.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-list-states.d.ts +29 -2
- package/dist/components/workflow-run-list/workflow-run-list-states.d.ts.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-list-states.js +103 -44
- package/dist/components/workflow-run-list/workflow-run-list-states.js.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-list-view.d.ts +1 -1
- package/dist/components/workflow-run-list/workflow-run-list-view.d.ts.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-list-view.js +60 -43
- package/dist/components/workflow-run-list/workflow-run-list-view.js.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-list.d.ts +1 -1
- package/dist/components/workflow-run-list/workflow-run-list.d.ts.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-list.js +20 -8
- package/dist/components/workflow-run-list/workflow-run-list.js.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-row.d.ts +19 -9
- package/dist/components/workflow-run-list/workflow-run-row.d.ts.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-row.js +179 -112
- package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
- package/dist/components/workflow-run-number-label.d.ts +15 -0
- package/dist/components/workflow-run-number-label.d.ts.map +1 -0
- package/dist/components/workflow-run-number-label.js +41 -0
- package/dist/components/workflow-run-number-label.js.map +1 -0
- package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.d.ts +3 -3
- package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.d.ts.map +1 -1
- package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js +8 -7
- package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js.map +1 -1
- package/dist/components/workflow-run-summary/workflow-run-summary.d.ts +3 -3
- package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
- package/dist/components/workflow-run-summary/workflow-run-summary.js +16 -7
- package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
- package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts +2 -2
- package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts.map +1 -1
- package/dist/components/workflow-run-view/agent-config-failure-callout.js +4 -4
- package/dist/components/workflow-run-view/agent-config-failure-callout.js.map +1 -1
- package/dist/components/workflow-run-view/job-card.d.ts +2 -2
- package/dist/components/workflow-run-view/job-card.d.ts.map +1 -1
- package/dist/components/workflow-run-view/job-card.js +51 -32
- package/dist/components/workflow-run-view/job-card.js.map +1 -1
- package/dist/components/workflow-run-view/job-execution-switcher.d.ts +1 -1
- package/dist/components/workflow-run-view/job-execution-switcher.d.ts.map +1 -1
- package/dist/components/workflow-run-view/job-execution-switcher.js +20 -8
- package/dist/components/workflow-run-view/job-execution-switcher.js.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-view.d.ts +3 -2
- package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-view.js +15 -11
- package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
- package/dist/components/workflow-status/status-visuals.d.ts +4 -4
- package/dist/components/workflow-status/status-visuals.d.ts.map +1 -1
- package/dist/components/workflow-status/status-visuals.js +7 -0
- package/dist/components/workflow-status/status-visuals.js.map +1 -1
- package/dist/components/workflow-status/workflow-status-icon.d.ts +3 -4
- package/dist/components/workflow-status/workflow-status-icon.d.ts.map +1 -1
- package/dist/components/workflow-status/workflow-status-icon.js +6 -4
- package/dist/components/workflow-status/workflow-status-icon.js.map +1 -1
- package/dist/core/entities/job-execution.d.ts +10 -0
- package/dist/core/entities/job-execution.d.ts.map +1 -1
- package/dist/core/entities/job-execution.js +11 -0
- package/dist/core/entities/job-execution.js.map +1 -1
- package/dist/core/entities/job.d.ts +4 -2
- package/dist/core/entities/job.d.ts.map +1 -1
- package/dist/core/entities/job.js +8 -1
- package/dist/core/entities/job.js.map +1 -1
- package/dist/core/entities/workflow-run.d.ts +52 -4
- package/dist/core/entities/workflow-run.d.ts.map +1 -1
- package/dist/core/entities/workflow-run.js +22 -3
- package/dist/core/entities/workflow-run.js.map +1 -1
- package/dist/core/workflow-run.d.ts +6 -6
- 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 +7 -7
- package/dist/feature.js +5 -5
- package/dist/feature.js.map +1 -1
- package/dist/hooks/api/workflow-run-mapper.d.ts +4 -3
- package/dist/hooks/api/workflow-run-mapper.d.ts.map +1 -1
- package/dist/hooks/api/workflow-run-mapper.js +31 -3
- package/dist/hooks/api/workflow-run-mapper.js.map +1 -1
- package/dist/hooks/api/workflow-runs.d.ts +4 -4
- package/dist/hooks/api/workflow-runs.d.ts.map +1 -1
- package/dist/hooks/api/workflow-runs.js +12 -4
- package/dist/hooks/api/workflow-runs.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/pages/project-workflows-page.js +46 -1
- package/dist/pages/project-workflows-page.js.map +1 -1
- package/dist/pages/workflow-run-detail-page.d.ts +11 -0
- package/dist/pages/workflow-run-detail-page.d.ts.map +1 -0
- package/dist/pages/workflow-run-detail-page.js +30 -0
- package/dist/pages/workflow-run-detail-page.js.map +1 -0
- package/dist/pages/workflow-run-list-page.d.ts +10 -0
- package/dist/pages/workflow-run-list-page.d.ts.map +1 -0
- package/dist/pages/workflow-run-list-page.js +43 -0
- package/dist/pages/workflow-run-list-page.js.map +1 -0
- package/dist/routes/inputs.d.ts +61 -5
- package/dist/routes/inputs.d.ts.map +1 -1
- package/dist/routes/inputs.js +150 -13
- package/dist/routes/inputs.js.map +1 -1
- package/dist/routes/project-route.d.ts +6 -0
- package/dist/routes/project-route.d.ts.map +1 -0
- package/dist/routes/project-route.js +25 -0
- package/dist/routes/project-route.js.map +1 -0
- package/dist/routes/run-detail.d.ts.map +1 -1
- package/dist/routes/run-detail.js +12 -7
- package/dist/routes/run-detail.js.map +1 -1
- package/dist/routes/runs.d.ts.map +1 -1
- package/dist/routes/runs.js +11 -6
- package/dist/routes/runs.js.map +1 -1
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/routes/workflows.js +6 -5
- package/dist/routes/workflows.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/src/components/job-graph/job-duration-format.ts +11 -2
- package/src/components/job-graph/job-graph-view.test.tsx +19 -8
- package/src/components/job-graph/job-graph.stories.tsx +23 -19
- package/src/components/job-graph/job-node.stories.tsx +10 -5
- package/src/components/job-graph/job-node.test.tsx +55 -10
- package/src/components/job-graph/job-node.tsx +16 -9
- package/src/components/step-list/step-list.stories.tsx +2 -3
- package/src/components/step-list/step-list.tsx +3 -2
- package/src/components/workflow-run-list/job-status-strip.tsx +189 -0
- package/src/components/workflow-run-list/run-display.test.ts +169 -30
- package/src/components/workflow-run-list/run-display.ts +116 -9
- package/src/components/workflow-run-list/types.ts +21 -17
- package/src/components/workflow-run-list/workflow-run-filter-menu.tsx +100 -0
- package/src/components/workflow-run-list/workflow-run-filters.tsx +243 -0
- package/src/components/workflow-run-list/workflow-run-list-content.tsx +44 -16
- package/src/components/workflow-run-list/workflow-run-list-states.tsx +128 -27
- package/src/components/workflow-run-list/workflow-run-list-view.test.tsx +441 -104
- package/src/components/workflow-run-list/workflow-run-list-view.tsx +73 -43
- package/src/components/workflow-run-list/workflow-run-list.stories.tsx +140 -20
- package/src/components/workflow-run-list/workflow-run-list.tsx +20 -10
- package/src/components/workflow-run-list/workflow-run-row.tsx +173 -106
- package/src/components/workflow-run-number-label.test.tsx +45 -0
- package/src/components/workflow-run-number-label.tsx +47 -0
- package/src/components/workflow-run-summary/workflow-run-attempt-switcher.test.tsx +7 -8
- package/src/components/workflow-run-summary/workflow-run-attempt-switcher.tsx +13 -12
- package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +6 -8
- package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +27 -2
- package/src/components/workflow-run-summary/workflow-run-summary.tsx +19 -9
- package/src/components/workflow-run-view/agent-config-failure-callout.stories.tsx +2 -3
- package/src/components/workflow-run-view/agent-config-failure-callout.tsx +4 -4
- package/src/components/workflow-run-view/job-card.stories.tsx +11 -10
- package/src/components/workflow-run-view/job-card.test.tsx +115 -0
- package/src/components/workflow-run-view/job-card.tsx +74 -35
- package/src/components/workflow-run-view/job-execution-switcher.tsx +31 -9
- package/src/components/workflow-run-view/workflow-run-view.test.tsx +88 -30
- package/src/components/workflow-run-view/workflow-run-view.tsx +24 -14
- package/src/components/workflow-status/status-visuals.test.ts +9 -0
- package/src/components/workflow-status/status-visuals.ts +6 -4
- package/src/components/workflow-status/workflow-status-icon.stories.tsx +10 -2
- package/src/components/workflow-status/workflow-status-icon.test.tsx +18 -0
- package/src/components/workflow-status/workflow-status-icon.tsx +17 -24
- package/src/core/entities/job-execution.ts +22 -0
- package/src/core/entities/job-status.test.ts +104 -0
- package/src/core/entities/job.ts +18 -3
- package/src/core/entities/workflow-run.ts +76 -7
- package/src/core/workflow-run.test.ts +24 -9
- package/src/core/workflow-run.ts +17 -2
- package/src/feature.ts +5 -5
- package/src/hooks/api/workflow-run-mapper.ts +30 -3
- package/src/hooks/api/workflow-runs.test.tsx +2 -0
- package/src/hooks/api/workflow-runs.ts +12 -7
- package/src/index.ts +7 -2
- package/src/page-harness-budget.test.ts +1 -1
- package/src/pages/project-workflows-page.test.tsx +59 -5
- package/src/pages/project-workflows-page.tsx +34 -1
- package/src/pages/workflow-run-detail-page.tsx +45 -0
- package/src/pages/workflow-run-list-page.tsx +65 -0
- package/src/pages/{workflow-run-page.test.tsx → workflow-run-pages.test.tsx} +211 -47
- package/src/routes/inputs.test.ts +193 -12
- package/src/routes/inputs.ts +170 -11
- package/src/routes/project-route.tsx +25 -0
- package/src/routes/run-detail.tsx +15 -8
- package/src/routes/runs.tsx +14 -7
- package/src/routes/workflows.tsx +5 -4
- package/test/fixtures/workflow-run.ts +76 -7
- package/test/pages.tsx +18 -8
- package/test/render.tsx +8 -2
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/components/workflow-run-list/workflow-run-list-header.d.ts +0 -10
- package/dist/components/workflow-run-list/workflow-run-list-header.d.ts.map +0 -1
- package/dist/components/workflow-run-list/workflow-run-list-header.js +0 -55
- package/dist/components/workflow-run-list/workflow-run-list-header.js.map +0 -1
- package/dist/pages/workflow-run-first-time-use.d.ts +0 -6
- package/dist/pages/workflow-run-first-time-use.d.ts.map +0 -1
- package/dist/pages/workflow-run-first-time-use.js +0 -17
- package/dist/pages/workflow-run-first-time-use.js.map +0 -1
- package/dist/pages/workflow-run-page.d.ts +0 -10
- package/dist/pages/workflow-run-page.d.ts.map +0 -1
- package/dist/pages/workflow-run-page.js +0 -90
- package/dist/pages/workflow-run-page.js.map +0 -1
- package/src/components/workflow-run-list/workflow-run-list-header.tsx +0 -53
- package/src/pages/workflow-run-first-time-use.tsx +0 -17
- package/src/pages/workflow-run-page.tsx +0 -94
|
@@ -8,7 +8,7 @@ const PAGE_HARNESS_ALLOWLIST = [
|
|
|
8
8
|
'src/components/workflow-run-summary/workflow-run-attempt-switcher.test.tsx',
|
|
9
9
|
'src/components/workflow-run-view/workflow-run-view.test.tsx',
|
|
10
10
|
'src/pages/project-workflows-page.test.tsx',
|
|
11
|
-
'src/pages/workflow-run-
|
|
11
|
+
'src/pages/workflow-run-pages.test.tsx',
|
|
12
12
|
];
|
|
13
13
|
|
|
14
14
|
const srcDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import {configureApiClient} from '@shipfox/client-api';
|
|
2
2
|
import {fireEvent, screen, waitFor} from '@testing-library/react';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
jsonResponse,
|
|
5
|
+
PROJECT_TEST_WID,
|
|
6
|
+
PROJECT_TEST_WSLUG,
|
|
7
|
+
renderProjectPage,
|
|
8
|
+
} from '#test/pages.js';
|
|
4
9
|
import {ProjectWorkflowsPage} from './project-workflows-page.js';
|
|
5
10
|
|
|
6
11
|
const PROJECT_ID = '44444444-4444-4444-8444-444444444444';
|
|
@@ -54,6 +59,13 @@ describe('ProjectWorkflowsPage', () => {
|
|
|
54
59
|
finished_at: null,
|
|
55
60
|
last_error_code: 'no-workflow-files',
|
|
56
61
|
last_error_message: 'No workflow files found',
|
|
62
|
+
warnings: [
|
|
63
|
+
{
|
|
64
|
+
code: 're-evaluating-command',
|
|
65
|
+
message: 'Workflow data is re-executed as shell code.',
|
|
66
|
+
path: 'jobs.build.steps.0.run',
|
|
67
|
+
},
|
|
68
|
+
],
|
|
57
69
|
},
|
|
58
70
|
}),
|
|
59
71
|
),
|
|
@@ -66,6 +78,47 @@ describe('ProjectWorkflowsPage', () => {
|
|
|
66
78
|
await screen.findByText('No workflow files found under .shipfox/workflows/.'),
|
|
67
79
|
).toBeInTheDocument();
|
|
68
80
|
expect(screen.getByText('Workflow sync failed')).toBeInTheDocument();
|
|
81
|
+
expect(screen.queryByText('Workflow definition warnings')).not.toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('shows definition warnings without rendering a sync failure', async () => {
|
|
85
|
+
configureApiClient({
|
|
86
|
+
fetchImpl: createProjectDetailFetch({
|
|
87
|
+
definitions: jsonResponse(
|
|
88
|
+
definitionsDto({
|
|
89
|
+
sync: {
|
|
90
|
+
ref: 'main',
|
|
91
|
+
status: 'succeeded',
|
|
92
|
+
last_sync_at: '2026-05-07T01:00:00.000Z',
|
|
93
|
+
started_at: '2026-05-07T00:59:55.000Z',
|
|
94
|
+
finished_at: '2026-05-07T01:00:00.000Z',
|
|
95
|
+
last_error_code: null,
|
|
96
|
+
last_error_message: null,
|
|
97
|
+
warnings: [
|
|
98
|
+
{
|
|
99
|
+
code: 're-evaluating-command',
|
|
100
|
+
message: 'Workflow data is re-executed as shell code.',
|
|
101
|
+
path: 'jobs.build.steps.0.run',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
code: 're-evaluating-command',
|
|
105
|
+
message: 'Workflow data is re-executed as shell code.',
|
|
106
|
+
path: 'jobs.build.steps.0.run',
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
}),
|
|
111
|
+
),
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
renderWorkflowsPage();
|
|
116
|
+
|
|
117
|
+
expect(await screen.findByText('Workflow definition warnings')).toBeInTheDocument();
|
|
118
|
+
expect(screen.getAllByText('Workflow data is re-executed as shell code.')).toHaveLength(2);
|
|
119
|
+
expect(screen.getAllByRole('listitem')).toHaveLength(2);
|
|
120
|
+
expect(screen.getAllByText('jobs.build.steps.0.run')).toHaveLength(2);
|
|
121
|
+
expect(screen.queryByText('Workflow sync failed')).not.toBeInTheDocument();
|
|
69
122
|
});
|
|
70
123
|
|
|
71
124
|
test('opens and closes the definition drawer by clicking the row', async () => {
|
|
@@ -123,10 +176,9 @@ describe('ProjectWorkflowsPage', () => {
|
|
|
123
176
|
});
|
|
124
177
|
|
|
125
178
|
function renderWorkflowsPage() {
|
|
126
|
-
return renderProjectPage(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
);
|
|
179
|
+
return renderProjectPage(`/w/${PROJECT_TEST_WSLUG}/p/project/workflows`, () => (
|
|
180
|
+
<ProjectWorkflowsPage projectId={PROJECT_ID} />
|
|
181
|
+
));
|
|
130
182
|
}
|
|
131
183
|
|
|
132
184
|
function createProjectDetailFetch({
|
|
@@ -174,6 +226,7 @@ function projectDto() {
|
|
|
174
226
|
id: PROJECT_ID,
|
|
175
227
|
workspace_id: PROJECT_TEST_WID,
|
|
176
228
|
name: 'Platform',
|
|
229
|
+
slug: 'platform',
|
|
177
230
|
source: {
|
|
178
231
|
connection_id: CONNECTION_ID,
|
|
179
232
|
external_repository_id: 'platform',
|
|
@@ -238,6 +291,7 @@ function baseDefinitionsDto() {
|
|
|
238
291
|
finished_at: '2026-05-07T01:00:00.000Z',
|
|
239
292
|
last_error_code: null,
|
|
240
293
|
last_error_message: null,
|
|
294
|
+
warnings: [],
|
|
241
295
|
},
|
|
242
296
|
};
|
|
243
297
|
}
|
|
@@ -103,6 +103,7 @@ function ProjectWorkflowsPageInner({projectId}: {projectId: string}) {
|
|
|
103
103
|
/>
|
|
104
104
|
|
|
105
105
|
<WorkflowSyncAlert sync={sync} />
|
|
106
|
+
<WorkflowSyncWarnings sync={sync} />
|
|
106
107
|
|
|
107
108
|
<WorkflowDefinitionsList
|
|
108
109
|
definitions={definitions}
|
|
@@ -214,7 +215,7 @@ function WorkflowDefinitionsList({
|
|
|
214
215
|
// The workflow-name cell holds a `<button>` so the row is
|
|
215
216
|
// keyboard-reachable (Tab focuses, Enter/Space activates
|
|
216
217
|
// via native button semantics). The TableRow itself is no
|
|
217
|
-
// longer clickable
|
|
218
|
+
// longer clickable: a row-level onClick would be invisible
|
|
218
219
|
// to keyboard users and require custom keydown handling.
|
|
219
220
|
// The `group` class on the row still drives the Run button
|
|
220
221
|
// reveal on hover or focus-within.
|
|
@@ -374,6 +375,38 @@ function WorkflowSyncAlert({sync}: {sync: DefinitionSyncSummary | null | undefin
|
|
|
374
375
|
);
|
|
375
376
|
}
|
|
376
377
|
|
|
378
|
+
function WorkflowSyncWarnings({sync}: {sync: DefinitionSyncSummary | null | undefined}) {
|
|
379
|
+
if (sync?.status !== 'succeeded' || sync.warnings.length === 0) return null;
|
|
380
|
+
|
|
381
|
+
const seenKeys = new Map<string, number>();
|
|
382
|
+
const warningItems = sync.warnings.map((warning) => {
|
|
383
|
+
const baseKey = `${warning.code}-${warning.path ?? 'workflow'}-${warning.message}`;
|
|
384
|
+
const occurrence = seenKeys.get(baseKey) ?? 0;
|
|
385
|
+
seenKeys.set(baseKey, occurrence + 1);
|
|
386
|
+
return {key: `${baseKey}-${occurrence}`, warning};
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
return (
|
|
390
|
+
<Callout role="status" type="warning">
|
|
391
|
+
<div className="flex flex-col gap-6">
|
|
392
|
+
<Text size="sm" bold>
|
|
393
|
+
Workflow definition warnings
|
|
394
|
+
</Text>
|
|
395
|
+
<ul className="flex flex-col gap-4">
|
|
396
|
+
{warningItems.map(({key, warning}) => (
|
|
397
|
+
<li key={key}>
|
|
398
|
+
<Text size="sm">{warning.message}</Text>
|
|
399
|
+
{warning.path ? (
|
|
400
|
+
<Code className="text-foreground-neutral-muted">{warning.path}</Code>
|
|
401
|
+
) : null}
|
|
402
|
+
</li>
|
|
403
|
+
))}
|
|
404
|
+
</ul>
|
|
405
|
+
</div>
|
|
406
|
+
</Callout>
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
377
410
|
function DefinitionSheet({
|
|
378
411
|
definition,
|
|
379
412
|
onOpenChange,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {useNavigate} from '@tanstack/react-router';
|
|
2
|
+
import {useCallback} from 'react';
|
|
3
|
+
import {WorkflowRunView} from '#components/workflow-run-view/index.js';
|
|
4
|
+
import type {WorkflowRunSelectionInput} from '#core/workflow-run-url-state.js';
|
|
5
|
+
import {type WorkflowRunsSearch, workflowRunSearchParams} from '#routes/inputs.js';
|
|
6
|
+
|
|
7
|
+
interface WorkflowRunDetailPageProps {
|
|
8
|
+
projectId: string;
|
|
9
|
+
workspaceSlug: string;
|
|
10
|
+
projectSlug: string;
|
|
11
|
+
workflowRunId?: string | undefined;
|
|
12
|
+
search?: WorkflowRunsSearch;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function WorkflowRunDetailPage({
|
|
16
|
+
projectId,
|
|
17
|
+
workspaceSlug,
|
|
18
|
+
projectSlug,
|
|
19
|
+
workflowRunId,
|
|
20
|
+
search = {},
|
|
21
|
+
}: WorkflowRunDetailPageProps) {
|
|
22
|
+
const navigate = useNavigate();
|
|
23
|
+
const selection: WorkflowRunSelectionInput = search;
|
|
24
|
+
const onSelectionChange = useCallback(
|
|
25
|
+
(nextSelection: WorkflowRunSelectionInput) => {
|
|
26
|
+
navigate({
|
|
27
|
+
search: workflowRunSearchParams(search, nextSelection) as never,
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
[navigate, search],
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="flex min-h-0 flex-1 overflow-hidden bg-background-neutral-base">
|
|
35
|
+
<WorkflowRunView
|
|
36
|
+
projectId={projectId}
|
|
37
|
+
workspaceSlug={workspaceSlug}
|
|
38
|
+
projectSlug={projectSlug}
|
|
39
|
+
workflowRunId={workflowRunId}
|
|
40
|
+
selection={selection}
|
|
41
|
+
onSelectionChange={onSelectionChange}
|
|
42
|
+
/>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {useNavigate} from '@tanstack/react-router';
|
|
2
|
+
import {useCallback} from 'react';
|
|
3
|
+
import {WorkflowRunList} from '#components/workflow-run-list/workflow-run-list.js';
|
|
4
|
+
import {
|
|
5
|
+
applyWorkflowRunFilterPatch,
|
|
6
|
+
clearWorkflowRunFilters,
|
|
7
|
+
type WorkflowRunFilterPatch,
|
|
8
|
+
type WorkflowRunsSearch,
|
|
9
|
+
workflowRunSearchParams,
|
|
10
|
+
} from '#routes/inputs.js';
|
|
11
|
+
|
|
12
|
+
interface WorkflowRunsPageProps {
|
|
13
|
+
projectId: string;
|
|
14
|
+
workspaceSlug: string;
|
|
15
|
+
projectSlug: string;
|
|
16
|
+
search?: WorkflowRunsSearch;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const EMPTY_SEARCH: WorkflowRunsSearch = {};
|
|
20
|
+
|
|
21
|
+
export function WorkflowRunsPage({
|
|
22
|
+
projectId,
|
|
23
|
+
workspaceSlug,
|
|
24
|
+
projectSlug,
|
|
25
|
+
search = EMPTY_SEARCH,
|
|
26
|
+
}: WorkflowRunsPageProps) {
|
|
27
|
+
const navigate = useNavigate();
|
|
28
|
+
|
|
29
|
+
// Filter changes replace history instead of pushing it, so Back leaves the list rather than
|
|
30
|
+
// walking every keystroke of a search box.
|
|
31
|
+
const commitSearch = useCallback(
|
|
32
|
+
(next: WorkflowRunsSearch) => {
|
|
33
|
+
navigate({
|
|
34
|
+
search: workflowRunSearchParams(next, {}) as never,
|
|
35
|
+
replace: true,
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
[navigate],
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const onFiltersChange = useCallback(
|
|
42
|
+
(patch: WorkflowRunFilterPatch) => commitSearch(applyWorkflowRunFilterPatch(search, patch)),
|
|
43
|
+
[commitSearch, search],
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const onClearFilters = useCallback(
|
|
47
|
+
() => commitSearch(clearWorkflowRunFilters(search)),
|
|
48
|
+
[commitSearch, search],
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="flex min-h-0 flex-1 overflow-hidden bg-background-neutral-base">
|
|
53
|
+
<div className="mx-auto flex min-h-0 w-full max-w-[1120px] flex-1 flex-col px-24 py-24">
|
|
54
|
+
<WorkflowRunList
|
|
55
|
+
projectId={projectId}
|
|
56
|
+
workspaceSlug={workspaceSlug}
|
|
57
|
+
projectSlug={projectSlug}
|
|
58
|
+
search={search}
|
|
59
|
+
onFiltersChange={onFiltersChange}
|
|
60
|
+
onClearFilters={onClearFilters}
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -14,13 +14,15 @@ import {
|
|
|
14
14
|
workflowStepAttemptDto,
|
|
15
15
|
workflowStepDto,
|
|
16
16
|
} from '#test/fixtures/workflow-run.js';
|
|
17
|
-
import {jsonResponse,
|
|
18
|
-
import {
|
|
17
|
+
import {jsonResponse, PROJECT_TEST_WSLUG, renderProjectPage} from '#test/pages.js';
|
|
18
|
+
import {WorkflowRunDetailPage} from './workflow-run-detail-page.js';
|
|
19
|
+
import {WorkflowRunsPage} from './workflow-run-list-page.js';
|
|
19
20
|
|
|
20
21
|
const PROJECT_ID = '44444444-4444-4444-8444-444444444444';
|
|
21
22
|
const DEFINITION_ID = '55555555-5555-4555-8555-555555555555';
|
|
22
23
|
const RUN_ID = '66666666-6666-4666-8666-666666666666';
|
|
23
24
|
const SECOND_RUN_ID = '66666666-6666-4666-8666-000000000002';
|
|
25
|
+
const OLDER_RUN_ID = '66666666-6666-4666-8666-000000000003';
|
|
24
26
|
const BUILD_JOB_ID = '77777777-7777-4777-8777-777777777777';
|
|
25
27
|
const BUILD_STEP_ID = '99999999-9999-4999-8999-000000000000';
|
|
26
28
|
const BUILD_ATTEMPT_ID = 'aaaaaaaa-aaaa-4aaa-8aaa-000000000000';
|
|
@@ -32,7 +34,12 @@ const DEPLOY_ATTEMPT_TWO_ID = 'aaaaaaaa-aaaa-4aaa-8aaa-000000000002';
|
|
|
32
34
|
const DEPLOY_EXECUTION_ONE_ID = 'bbbbbbbb-bbbb-4bbb-8bbb-000000000001';
|
|
33
35
|
const DEPLOY_EXECUTION_TWO_ID = 'bbbbbbbb-bbbb-4bbb-8bbb-000000000002';
|
|
34
36
|
const SMOKE_WEB_RE = /smoke-web/u;
|
|
35
|
-
const
|
|
37
|
+
const DEPLOY_WEB_RE = /deploy-web/u;
|
|
38
|
+
const OLDER_RUN_RE = /older-run/u;
|
|
39
|
+
const INTEGRATION_TESTS_RE = /integration-tests/u;
|
|
40
|
+
const BUILD_IMAGE_RE = /build-image/u;
|
|
41
|
+
const STATUS_FILTER_RE = /^Status\b.*filter$/u;
|
|
42
|
+
const EXECUTION_ONE_MENU_ITEM = /Execution #1: deploy review #1/u;
|
|
36
43
|
const RUN_DETAIL_PATH_RE = /^\/workflows\/runs\/([^/]+)$/u;
|
|
37
44
|
const RUN_OVERRIDES = {
|
|
38
45
|
id: RUN_ID,
|
|
@@ -47,47 +54,129 @@ const SECOND_RUN_OVERRIDES = {
|
|
|
47
54
|
id: SECOND_RUN_ID,
|
|
48
55
|
name: 'smoke-web',
|
|
49
56
|
} satisfies Partial<WorkflowRunResponseDto>;
|
|
57
|
+
const OLDER_RUN_OVERRIDES = {
|
|
58
|
+
...RUN_OVERRIDES,
|
|
59
|
+
id: OLDER_RUN_ID,
|
|
60
|
+
name: 'older-run',
|
|
61
|
+
} satisfies Partial<WorkflowRunResponseDto>;
|
|
50
62
|
|
|
51
|
-
describe('
|
|
52
|
-
test('
|
|
63
|
+
describe('WorkflowRunPages', () => {
|
|
64
|
+
test('renders the list route without mounting run detail', async () => {
|
|
53
65
|
configureApiClient({fetchImpl: vi.fn(() => new Promise<Response>(() => undefined))});
|
|
54
66
|
|
|
55
67
|
renderRunsPath();
|
|
56
68
|
|
|
57
|
-
|
|
58
|
-
expect(await screen.findByLabelText('Loading workflow run')).toBeInTheDocument();
|
|
59
|
-
// ...but the runs list itself stays mounted; it is never replaced by a page skeleton.
|
|
69
|
+
expect(await screen.findByLabelText('Loading runs')).toBeInTheDocument();
|
|
60
70
|
expect(screen.getByLabelText('Workflow runs')).toBeInTheDocument();
|
|
71
|
+
expect(screen.queryByLabelText('Loading workflow run')).not.toBeInTheDocument();
|
|
61
72
|
});
|
|
62
73
|
|
|
63
|
-
test('
|
|
74
|
+
test('keeps the list route and filters in place instead of redirecting to a run', async () => {
|
|
64
75
|
configureApiClient({fetchImpl: createRunsListFetch()});
|
|
65
76
|
|
|
66
|
-
const {router} = renderRunsPath(
|
|
67
|
-
`?job=${DEPLOY_JOB_ID}&step=${DEPLOY_STEP_ID}&stepAttempt=${DEPLOY_ATTEMPT_TWO_ID}&runAttempt=1`,
|
|
68
|
-
);
|
|
77
|
+
const {router} = renderRunsPath('?search=deploy-web&status=running');
|
|
69
78
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
expect(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
expect(await screen.findByRole('link', {name: DEPLOY_WEB_RE})).toBeInTheDocument();
|
|
80
|
+
expect(router.state.location.pathname).toBe(`/w/${PROJECT_TEST_WSLUG}/p/project/runs`);
|
|
81
|
+
expect(currentSearch(router)).toMatchObject({search: 'deploy-web', status: 'running'});
|
|
82
|
+
expect(screen.queryByLabelText('Loading workflow run')).not.toBeInTheDocument();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('clearing filters on the list route resets search and status in the URL', async () => {
|
|
86
|
+
const user = userEvent.setup();
|
|
87
|
+
configureApiClient({fetchImpl: createRunsListFetch()});
|
|
88
|
+
|
|
89
|
+
const {router} = renderRunsPath('?search=no-such-run&status=failed');
|
|
90
|
+
|
|
91
|
+
expect(await screen.findByText('No matching runs')).toBeInTheDocument();
|
|
92
|
+
|
|
93
|
+
await user.click(screen.getByRole('button', {name: 'Clear filters'}));
|
|
94
|
+
|
|
95
|
+
await waitFor(() => {
|
|
96
|
+
expect(currentSearch(router).search).toBeUndefined();
|
|
97
|
+
});
|
|
98
|
+
expect(currentSearch(router).status).toBeUndefined();
|
|
99
|
+
expect(await screen.findByRole('link', {name: DEPLOY_WEB_RE})).toBeInTheDocument();
|
|
78
100
|
});
|
|
79
101
|
|
|
80
|
-
test('
|
|
102
|
+
test('honors a deep link that repeats a filter key', async () => {
|
|
103
|
+
configureApiClient({fetchImpl: createMixedStatusRunsFetch()});
|
|
104
|
+
|
|
105
|
+
renderRunsPath('?status=failed&status=running');
|
|
106
|
+
|
|
107
|
+
expect(await screen.findByRole('link', {name: DEPLOY_WEB_RE})).toBeInTheDocument();
|
|
108
|
+
expect(await screen.findByRole('link', {name: INTEGRATION_TESTS_RE})).toBeInTheDocument();
|
|
109
|
+
expect(screen.queryByRole('link', {name: BUILD_IMAGE_RE})).not.toBeInTheDocument();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('writes a multi-select as repeated keys rather than one joined value', async () => {
|
|
113
|
+
const user = userEvent.setup();
|
|
114
|
+
configureApiClient({fetchImpl: createMixedStatusRunsFetch()});
|
|
115
|
+
|
|
116
|
+
const {router} = renderRunsPath('?status=failed');
|
|
117
|
+
|
|
118
|
+
await user.click(await screen.findByRole('button', {name: STATUS_FILTER_RE}));
|
|
119
|
+
await user.click(await screen.findByRole('menuitemcheckbox', {name: 'Running'}));
|
|
120
|
+
|
|
121
|
+
await waitFor(() => {
|
|
122
|
+
expect(currentSearch(router).status).toEqual(['failed', 'running']);
|
|
123
|
+
});
|
|
124
|
+
expect(router.state.location.searchStr).toBe('?status=failed&status=running');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('replaces history on a filter change so back leaves the list', async () => {
|
|
128
|
+
const user = userEvent.setup();
|
|
129
|
+
configureApiClient({fetchImpl: createMixedStatusRunsFetch()});
|
|
130
|
+
|
|
131
|
+
const {router} = renderRunsPath();
|
|
132
|
+
const initialLength = router.history.length;
|
|
133
|
+
|
|
134
|
+
await user.type(await screen.findByLabelText('Search runs'), 'deploy');
|
|
135
|
+
|
|
136
|
+
await waitFor(() => {
|
|
137
|
+
expect(currentSearch(router).search).toBe('deploy');
|
|
138
|
+
});
|
|
139
|
+
expect(router.history.length).toBe(initialLength);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('loads older pages before reporting that a search has no matches', async () => {
|
|
143
|
+
const user = userEvent.setup();
|
|
144
|
+
const fetchImpl = createPaginatedRunsFetch();
|
|
145
|
+
configureApiClient({fetchImpl});
|
|
146
|
+
|
|
147
|
+
renderRunsPath('?search=older-run');
|
|
148
|
+
|
|
149
|
+
expect(await screen.findByText('No matches in loaded history')).toBeInTheDocument();
|
|
150
|
+
await user.click(screen.getByRole('button', {name: 'Load more runs'}));
|
|
151
|
+
|
|
152
|
+
expect(await screen.findByRole('link', {name: OLDER_RUN_RE})).toBeInTheDocument();
|
|
153
|
+
expect(
|
|
154
|
+
fetchImpl.mock.calls.some((call) => {
|
|
155
|
+
const url = new URL(requestInputUrl(call[0]));
|
|
156
|
+
return url.searchParams.get('cursor') === 'cursor-2';
|
|
157
|
+
}),
|
|
158
|
+
).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('shows the list empty state without mounting run detail', async () => {
|
|
81
162
|
configureApiClient({fetchImpl: createEmptyRunsFetch()});
|
|
82
163
|
|
|
83
164
|
renderRunsPath();
|
|
84
165
|
|
|
85
|
-
expect(await screen.findByText('No
|
|
86
|
-
|
|
87
|
-
expect(screen.queryByLabelText('Workflow runs')).not.toBeInTheDocument();
|
|
166
|
+
expect(await screen.findByText('No runs yet')).toBeInTheDocument();
|
|
167
|
+
expect(screen.getByLabelText('Workflow runs')).toBeInTheDocument();
|
|
88
168
|
expect(screen.queryByLabelText('Loading workflow run')).not.toBeInTheDocument();
|
|
89
169
|
});
|
|
90
170
|
|
|
171
|
+
test('renders the detail route without mounting the run list', async () => {
|
|
172
|
+
configureApiClient({fetchImpl: createRunDetailFetch()});
|
|
173
|
+
|
|
174
|
+
renderRunPath();
|
|
175
|
+
|
|
176
|
+
expect(await screen.findByRole('button', {name: 'deploy, Running'})).toBeInTheDocument();
|
|
177
|
+
expect(screen.queryByLabelText('Workflow runs')).not.toBeInTheDocument();
|
|
178
|
+
});
|
|
179
|
+
|
|
91
180
|
test('restores a deep-linked job and exact attempt after data loads', async () => {
|
|
92
181
|
configureApiClient({fetchImpl: createRunDetailFetch()});
|
|
93
182
|
|
|
@@ -150,10 +239,14 @@ describe('WorkflowRunPage', () => {
|
|
|
150
239
|
|
|
151
240
|
await user.click(
|
|
152
241
|
await screen.findByRole('button', {
|
|
153
|
-
name: 'Switch job execution, currently execution 2',
|
|
242
|
+
name: 'Switch job execution, currently execution 2: deploy review #2',
|
|
243
|
+
}),
|
|
244
|
+
);
|
|
245
|
+
await user.click(
|
|
246
|
+
screen.getByRole('menuitem', {
|
|
247
|
+
name: EXECUTION_ONE_MENU_ITEM,
|
|
154
248
|
}),
|
|
155
249
|
);
|
|
156
|
-
await user.click(screen.getByRole('menuitem', {name: EXECUTION_ONE_MENU_ITEM_PATTERN}));
|
|
157
250
|
|
|
158
251
|
await waitFor(() => {
|
|
159
252
|
expect(currentSearch(router)).toMatchObject({
|
|
@@ -162,6 +255,7 @@ describe('WorkflowRunPage', () => {
|
|
|
162
255
|
});
|
|
163
256
|
});
|
|
164
257
|
expect(screen.getByRole('button', {name: 'deploy, Failed, attempt 1'})).toBeInTheDocument();
|
|
258
|
+
expect(screen.getAllByText('deploy review #1')).not.toHaveLength(0);
|
|
165
259
|
|
|
166
260
|
await user.click(screen.getByRole('button', {name: 'deploy, Failed, attempt 1'}));
|
|
167
261
|
|
|
@@ -229,7 +323,7 @@ describe('WorkflowRunPage', () => {
|
|
|
229
323
|
);
|
|
230
324
|
});
|
|
231
325
|
|
|
232
|
-
test('run
|
|
326
|
+
test('run list links navigate to detail and back restores list filters', async () => {
|
|
233
327
|
const user = userEvent.setup();
|
|
234
328
|
configureApiClient({
|
|
235
329
|
fetchImpl: createRunDetailFetch({
|
|
@@ -240,41 +334,62 @@ describe('WorkflowRunPage', () => {
|
|
|
240
334
|
},
|
|
241
335
|
}),
|
|
242
336
|
});
|
|
243
|
-
const {router} =
|
|
337
|
+
const {router} = renderRunsPath('?search=smoke&status=running');
|
|
244
338
|
|
|
245
339
|
await user.click(await screen.findByRole('link', {name: SMOKE_WEB_RE}));
|
|
246
340
|
|
|
247
341
|
await waitFor(() => {
|
|
248
|
-
expect(router.state.location.pathname).
|
|
342
|
+
expect(router.state.location.pathname).toBe(
|
|
343
|
+
`/w/${PROJECT_TEST_WSLUG}/p/project/runs/${SECOND_RUN_ID}`,
|
|
344
|
+
);
|
|
249
345
|
});
|
|
250
|
-
expect(
|
|
251
|
-
expect(currentSearch(router)
|
|
252
|
-
|
|
253
|
-
|
|
346
|
+
expect(screen.queryByLabelText('Workflow runs')).not.toBeInTheDocument();
|
|
347
|
+
expect(currentSearch(router)).toMatchObject({search: 'smoke', status: 'running'});
|
|
348
|
+
|
|
349
|
+
await act(() => {
|
|
350
|
+
router.history.back();
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
await waitFor(() => {
|
|
354
|
+
expect(router.state.location.pathname).toBe(`/w/${PROJECT_TEST_WSLUG}/p/project/runs`);
|
|
355
|
+
});
|
|
356
|
+
expect(currentSearch(router)).toMatchObject({search: 'smoke', status: 'running'});
|
|
357
|
+
expect(screen.getByLabelText('Workflow runs')).toBeInTheDocument();
|
|
358
|
+
expect(screen.getByRole('link', {name: SMOKE_WEB_RE})).toBeInTheDocument();
|
|
254
359
|
});
|
|
255
360
|
});
|
|
256
361
|
|
|
257
362
|
function renderRunsPath(search = '') {
|
|
258
363
|
return renderProjectPage(
|
|
259
|
-
`/
|
|
260
|
-
({workflowRunId, search}) =>
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
364
|
+
`/w/${PROJECT_TEST_WSLUG}/p/project/runs${search}`,
|
|
365
|
+
({workflowRunId, search}) =>
|
|
366
|
+
workflowRunId ? (
|
|
367
|
+
<WorkflowRunDetailPage
|
|
368
|
+
projectId={PROJECT_ID}
|
|
369
|
+
workspaceSlug={PROJECT_TEST_WSLUG}
|
|
370
|
+
projectSlug="project"
|
|
371
|
+
workflowRunId={workflowRunId}
|
|
372
|
+
search={search}
|
|
373
|
+
/>
|
|
374
|
+
) : (
|
|
375
|
+
<WorkflowRunsPage
|
|
376
|
+
projectId={PROJECT_ID}
|
|
377
|
+
workspaceSlug={PROJECT_TEST_WSLUG}
|
|
378
|
+
projectSlug="project"
|
|
379
|
+
search={search}
|
|
380
|
+
/>
|
|
381
|
+
),
|
|
268
382
|
);
|
|
269
383
|
}
|
|
270
384
|
|
|
271
385
|
function renderRunPath(search = '') {
|
|
272
386
|
return renderProjectPage(
|
|
273
|
-
`/
|
|
387
|
+
`/w/${PROJECT_TEST_WSLUG}/p/project/runs/${RUN_ID}${search}`,
|
|
274
388
|
({workflowRunId, search}) => (
|
|
275
|
-
<
|
|
276
|
-
workspaceId={PROJECT_TEST_WID}
|
|
389
|
+
<WorkflowRunDetailPage
|
|
277
390
|
projectId={PROJECT_ID}
|
|
391
|
+
workspaceSlug={PROJECT_TEST_WSLUG}
|
|
392
|
+
projectSlug="project"
|
|
278
393
|
workflowRunId={workflowRunId}
|
|
279
394
|
search={search}
|
|
280
395
|
/>
|
|
@@ -303,6 +418,55 @@ function createRunsListFetch() {
|
|
|
303
418
|
});
|
|
304
419
|
}
|
|
305
420
|
|
|
421
|
+
function createMixedStatusRunsFetch() {
|
|
422
|
+
const runs = [
|
|
423
|
+
workflowRunDto({...RUN_OVERRIDES, status: 'running'}),
|
|
424
|
+
workflowRunDto({
|
|
425
|
+
...RUN_OVERRIDES,
|
|
426
|
+
id: SECOND_RUN_ID,
|
|
427
|
+
name: 'integration-tests',
|
|
428
|
+
workflow_name: 'integration-tests',
|
|
429
|
+
status: 'failed',
|
|
430
|
+
}),
|
|
431
|
+
workflowRunDto({
|
|
432
|
+
...RUN_OVERRIDES,
|
|
433
|
+
id: OLDER_RUN_ID,
|
|
434
|
+
name: 'build-image',
|
|
435
|
+
workflow_name: 'build-image',
|
|
436
|
+
status: 'succeeded',
|
|
437
|
+
}),
|
|
438
|
+
];
|
|
439
|
+
|
|
440
|
+
return vi.fn((input: RequestInfo | URL) => {
|
|
441
|
+
const url = new URL(requestInputUrl(input));
|
|
442
|
+
|
|
443
|
+
if (url.pathname === '/workflows/runs') {
|
|
444
|
+
return Promise.resolve(jsonResponse({runs, next_cursor: null, filtered_total_count: 3}));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return Promise.resolve(jsonResponse({code: 'not-found'}, {status: 404}));
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function createPaginatedRunsFetch() {
|
|
452
|
+
return vi.fn((input: RequestInfo | URL) => {
|
|
453
|
+
const url = new URL(requestInputUrl(input));
|
|
454
|
+
|
|
455
|
+
if (url.pathname === '/workflows/runs') {
|
|
456
|
+
const isSecondPage = url.searchParams.get('cursor') === 'cursor-2';
|
|
457
|
+
return Promise.resolve(
|
|
458
|
+
jsonResponse({
|
|
459
|
+
runs: [workflowRunDto(isSecondPage ? OLDER_RUN_OVERRIDES : RUN_OVERRIDES)],
|
|
460
|
+
next_cursor: isSecondPage ? null : 'cursor-2',
|
|
461
|
+
filtered_total_count: isSecondPage ? null : 2,
|
|
462
|
+
}),
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return Promise.resolve(jsonResponse({code: 'not-found'}, {status: 404}));
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
306
470
|
function createRunDetailFetch({
|
|
307
471
|
runs = [workflowRunDto(RUN_OVERRIDES)],
|
|
308
472
|
details = {[RUN_ID]: defaultRunDetailDto()},
|
|
@@ -464,7 +628,7 @@ function retryRunDetailDto(): WorkflowRunDetailResponseDto {
|
|
|
464
628
|
id: DEPLOY_EXECUTION_ONE_ID,
|
|
465
629
|
job_id: DEPLOY_JOB_ID,
|
|
466
630
|
sequence: 1,
|
|
467
|
-
name: 'deploy',
|
|
631
|
+
name: 'deploy review #1',
|
|
468
632
|
status: 'failed',
|
|
469
633
|
started_at: '2026-05-07T01:01:00.000Z',
|
|
470
634
|
finished_at: '2026-05-07T01:02:00.000Z',
|
|
@@ -488,7 +652,7 @@ function retryRunDetailDto(): WorkflowRunDetailResponseDto {
|
|
|
488
652
|
id: DEPLOY_EXECUTION_TWO_ID,
|
|
489
653
|
job_id: DEPLOY_JOB_ID,
|
|
490
654
|
sequence: 2,
|
|
491
|
-
name: 'deploy',
|
|
655
|
+
name: 'deploy review #2',
|
|
492
656
|
status: 'running',
|
|
493
657
|
started_at: '2026-05-07T01:03:00.000Z',
|
|
494
658
|
steps: [
|