@shipfox/client-workflows 20.0.0 → 21.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +29 -0
- package/dist/components/job-detail/job-detail-view.d.ts.map +1 -1
- package/dist/components/job-detail/job-detail-view.js +231 -42
- package/dist/components/job-detail/job-detail-view.js.map +1 -1
- package/dist/components/job-detail/step-attempt-log-panel.d.ts +7 -1
- package/dist/components/job-detail/step-attempt-log-panel.d.ts.map +1 -1
- package/dist/components/job-detail/step-attempt-log-panel.js +37 -10
- package/dist/components/job-detail/step-attempt-log-panel.js.map +1 -1
- package/dist/components/step-list/step-list-model.d.ts +1 -0
- package/dist/components/step-list/step-list-model.d.ts.map +1 -1
- package/dist/components/step-list/step-list-model.js +1 -1
- package/dist/components/step-list/step-list-model.js.map +1 -1
- package/dist/components/step-list/step-list.d.ts +2 -1
- package/dist/components/step-list/step-list.d.ts.map +1 -1
- package/dist/components/step-list/step-list.js +24 -7
- package/dist/components/step-list/step-list.js.map +1 -1
- package/dist/components/workflow-run-list/job-status-strip.d.ts +9 -1
- package/dist/components/workflow-run-list/job-status-strip.d.ts.map +1 -1
- package/dist/components/workflow-run-list/job-status-strip.js +22 -11
- package/dist/components/workflow-run-list/job-status-strip.js.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-filters.js +1 -1
- package/dist/components/workflow-run-list/workflow-run-filters.js.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-list-states.js +2 -2
- 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.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-list-view.js +34 -22
- package/dist/components/workflow-run-list/workflow-run-list-view.js.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-row.d.ts +3 -3
- package/dist/components/workflow-run-list/workflow-run-row.d.ts.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-row.js +13 -10
- 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 +1 -1
- package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
- package/dist/components/workflow-run-view/run-workspace-nav.js +1 -1
- package/dist/components/workflow-run-view/run-workspace-nav.js.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-states.d.ts.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-states.js +1 -1
- package/dist/components/workflow-run-view/workflow-run-states.js.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-view.js +43 -36
- package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
- package/dist/components/workflow-source-panel/workflow-source-content.js +1 -1
- package/dist/components/workflow-source-panel/workflow-source-content.js.map +1 -1
- package/dist/pages/project-workflows-page.d.ts.map +1 -1
- package/dist/pages/project-workflows-page.js +3 -2
- package/dist/pages/project-workflows-page.js.map +1 -1
- package/dist/pages/workflow-job-detail-page.d.ts.map +1 -1
- package/dist/pages/workflow-job-detail-page.js +3 -2
- package/dist/pages/workflow-job-detail-page.js.map +1 -1
- package/dist/pages/workflow-run-detail-page.js +2 -1
- package/dist/pages/workflow-run-detail-page.js.map +1 -1
- package/dist/pages/workflow-run-list-page.d.ts.map +1 -1
- package/dist/pages/workflow-run-list-page.js +9 -11
- package/dist/pages/workflow-run-list-page.js.map +1 -1
- package/dist/routes/job-detail.d.ts +1 -1
- package/dist/routes/job-detail.js +1 -1
- package/dist/routes/job-detail.js.map +1 -1
- package/dist/routes/run-detail.d.ts +1 -1
- package/dist/routes/run-detail.js +1 -1
- package/dist/routes/run-detail.js.map +1 -1
- package/dist/routes/runs.d.ts +1 -1
- package/dist/routes/runs.js +1 -1
- package/dist/routes/runs.js.map +1 -1
- package/dist/routes/workflows.d.ts +3 -0
- package/dist/routes/workflows.js +3 -0
- package/dist/routes/workflows.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/src/components/job-detail/job-detail-view.tsx +263 -47
- package/src/components/job-detail/step-attempt-log-panel.test.tsx +30 -12
- package/src/components/job-detail/step-attempt-log-panel.tsx +68 -10
- package/src/components/job-graph/job-graph.stories.tsx +1 -1
- package/src/components/job-graph/job-node.stories.tsx +1 -1
- package/src/components/step-list/step-list-model.ts +1 -1
- package/src/components/step-list/step-list.test.tsx +29 -1
- package/src/components/step-list/step-list.tsx +28 -9
- package/src/components/workflow-run-list/job-status-strip.tsx +25 -12
- package/src/components/workflow-run-list/workflow-run-filters.tsx +1 -1
- package/src/components/workflow-run-list/workflow-run-list-states.tsx +2 -2
- package/src/components/workflow-run-list/workflow-run-list-view.test.tsx +53 -4
- package/src/components/workflow-run-list/workflow-run-list-view.tsx +28 -22
- package/src/components/workflow-run-list/workflow-run-list.stories.tsx +9 -20
- package/src/components/workflow-run-list/workflow-run-row.tsx +17 -13
- package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +2 -2
- package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +2 -1
- package/src/components/workflow-run-summary/workflow-run-summary.tsx +1 -4
- package/src/components/workflow-run-view/run-workspace-nav.tsx +1 -1
- package/src/components/workflow-run-view/workflow-run-states.tsx +1 -4
- package/src/components/workflow-run-view/workflow-run-view.stories.tsx +140 -0
- package/src/components/workflow-run-view/workflow-run-view.test.tsx +38 -7
- package/src/components/workflow-run-view/workflow-run-view.tsx +45 -37
- package/src/components/workflow-source-panel/workflow-source-content.tsx +2 -2
- package/src/pages/project-workflows-page.tsx +3 -2
- package/src/pages/workflow-job-detail-page.test.tsx +32 -0
- package/src/pages/workflow-job-detail-page.tsx +5 -1
- package/src/pages/workflow-run-detail-page.tsx +1 -1
- package/src/pages/workflow-run-list-page.tsx +9 -11
- package/src/pages/workflow-run-pages.test.tsx +26 -0
- package/src/routes/job-detail.tsx +1 -1
- package/src/routes/run-detail.tsx +1 -1
- package/src/routes/runs.tsx +1 -1
- package/src/routes/workflows.tsx +1 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type {WorkflowRunDetailResponseDto} from '@shipfox/api-workflows-dto';
|
|
2
|
+
import {configureApiClient} from '@shipfox/client-api';
|
|
3
|
+
import type {Decorator, Meta, StoryObj} from '@storybook/react';
|
|
4
|
+
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
|
|
5
|
+
import {type ReactNode, useEffect, useState} from 'react';
|
|
6
|
+
import {
|
|
7
|
+
runAttemptsResponseDto,
|
|
8
|
+
workflowJobDto,
|
|
9
|
+
workflowRunDetailDto,
|
|
10
|
+
} from '#test/fixtures/workflow-run.js';
|
|
11
|
+
import {WorkflowRunView} from './workflow-run-view.js';
|
|
12
|
+
|
|
13
|
+
const PROJECT_ID = '22222222-2222-4222-8222-222222222222';
|
|
14
|
+
const RUN_ID = '11111111-1111-4111-8111-111111111111';
|
|
15
|
+
const RUN_STARTED_AT = '2026-06-26T11:57:00.000Z';
|
|
16
|
+
const RUN_FINISHED_AT = '2026-06-26T11:59:00.000Z';
|
|
17
|
+
|
|
18
|
+
const RUN_RESPONSE: WorkflowRunDetailResponseDto = workflowRunDetailDto({
|
|
19
|
+
id: RUN_ID,
|
|
20
|
+
project_id: PROJECT_ID,
|
|
21
|
+
name: 'deploy-web',
|
|
22
|
+
status: 'succeeded',
|
|
23
|
+
started_at: RUN_STARTED_AT,
|
|
24
|
+
finished_at: RUN_FINISHED_AT,
|
|
25
|
+
has_started_job_execution: true,
|
|
26
|
+
jobs: [
|
|
27
|
+
workflowJobDto({
|
|
28
|
+
key: 'build',
|
|
29
|
+
name: 'build',
|
|
30
|
+
status: 'succeeded',
|
|
31
|
+
position: 0,
|
|
32
|
+
}),
|
|
33
|
+
workflowJobDto({
|
|
34
|
+
key: 'deploy',
|
|
35
|
+
name: 'deploy',
|
|
36
|
+
status: 'succeeded',
|
|
37
|
+
position: 1,
|
|
38
|
+
dependencies: ['build'],
|
|
39
|
+
}),
|
|
40
|
+
workflowJobDto({
|
|
41
|
+
key: 'notify',
|
|
42
|
+
name: 'notify',
|
|
43
|
+
status: 'succeeded',
|
|
44
|
+
position: 2,
|
|
45
|
+
dependencies: ['deploy'],
|
|
46
|
+
}),
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
const RUN_ATTEMPTS_RESPONSE = runAttemptsResponseDto({attempts: []});
|
|
50
|
+
|
|
51
|
+
const withRunApi: Decorator = (Story) => (
|
|
52
|
+
<RunWorkspaceStoryProviders>
|
|
53
|
+
<Story />
|
|
54
|
+
</RunWorkspaceStoryProviders>
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const meta = {
|
|
58
|
+
title: 'Workflows/WorkflowRunView',
|
|
59
|
+
component: WorkflowRunView,
|
|
60
|
+
parameters: {
|
|
61
|
+
layout: 'fullscreen',
|
|
62
|
+
viewport: {
|
|
63
|
+
defaultViewport: 'wide',
|
|
64
|
+
options: {
|
|
65
|
+
wide: {
|
|
66
|
+
name: 'Wide',
|
|
67
|
+
styles: {width: '1440px', height: '720px'},
|
|
68
|
+
type: 'desktop',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
globals: {
|
|
73
|
+
viewport: {value: 'wide'},
|
|
74
|
+
},
|
|
75
|
+
argos: {
|
|
76
|
+
modes: {dark: {theme: 'dark'}},
|
|
77
|
+
fitToContent: false,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
decorators: [withRunApi],
|
|
81
|
+
args: {
|
|
82
|
+
projectId: PROJECT_ID,
|
|
83
|
+
workspaceSlug: 'acme',
|
|
84
|
+
projectSlug: 'platform',
|
|
85
|
+
workflowRunId: RUN_ID,
|
|
86
|
+
},
|
|
87
|
+
} satisfies Meta<typeof WorkflowRunView>;
|
|
88
|
+
|
|
89
|
+
export default meta;
|
|
90
|
+
type Story = StoryObj<typeof meta>;
|
|
91
|
+
|
|
92
|
+
export const WideViewport: Story = {};
|
|
93
|
+
|
|
94
|
+
function RunWorkspaceStoryProviders({children}: {children: ReactNode}) {
|
|
95
|
+
const [queryClient] = useState(
|
|
96
|
+
() => new QueryClient({defaultOptions: {queries: {retry: false}}}),
|
|
97
|
+
);
|
|
98
|
+
const [configured, setConfigured] = useState(false);
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
configureApiClient({
|
|
102
|
+
baseUrl: 'https://api.example.test',
|
|
103
|
+
fetchImpl: (input) => {
|
|
104
|
+
const url =
|
|
105
|
+
input instanceof Request ? input.url : input instanceof URL ? input.href : String(input);
|
|
106
|
+
const path = new URL(url, 'https://api.example.test').pathname;
|
|
107
|
+
const response =
|
|
108
|
+
path === '/annotations'
|
|
109
|
+
? {
|
|
110
|
+
body: {annotations: [], has_more: false, next_cursor: null},
|
|
111
|
+
status: 200,
|
|
112
|
+
}
|
|
113
|
+
: path === `/workflows/runs/${RUN_ID}/attempts`
|
|
114
|
+
? {body: RUN_ATTEMPTS_RESPONSE, status: 200}
|
|
115
|
+
: path === `/workflows/runs/${RUN_ID}`
|
|
116
|
+
? {body: RUN_RESPONSE, status: 200}
|
|
117
|
+
: {body: {code: 'not-found'}, status: 404};
|
|
118
|
+
return new Response(JSON.stringify(response.body), {
|
|
119
|
+
status: response.status,
|
|
120
|
+
headers: {'content-type': 'application/json'},
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
setConfigured(true);
|
|
125
|
+
|
|
126
|
+
return () => {
|
|
127
|
+
configureApiClient({baseUrl: '', fetchImpl: undefined});
|
|
128
|
+
};
|
|
129
|
+
}, []);
|
|
130
|
+
|
|
131
|
+
if (!configured) return null;
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<QueryClientProvider client={queryClient}>
|
|
135
|
+
<div className="flex h-[720px] min-w-[1440px] w-[1440px] bg-background-subtle-base">
|
|
136
|
+
{children}
|
|
137
|
+
</div>
|
|
138
|
+
</QueryClientProvider>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
@@ -49,23 +49,54 @@ describe('WorkflowRunView', () => {
|
|
|
49
49
|
const summary = await screen.findByRole('region', {name: 'deploy-web'});
|
|
50
50
|
expect(within(summary).getByRole('heading', {name: 'deploy-web'})).toBeInTheDocument();
|
|
51
51
|
expect(screen.getByRole('navigation', {name: 'Run workspace'})).toBeInTheDocument();
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
).toHaveClass('
|
|
52
|
+
const workspaceLayout = screen
|
|
53
|
+
.getByRole('navigation', {name: 'Run workspace'})
|
|
54
|
+
.closest('[data-run-workspace-layout]');
|
|
55
|
+
expect(workspaceLayout).toHaveClass('border-t', 'border-border-neutral-base');
|
|
56
|
+
expect(workspaceLayout).not.toHaveClass('max-w-[1360px]');
|
|
57
|
+
expect(workspaceLayout?.querySelector('[data-run-workspace-frame]')).toHaveClass(
|
|
58
|
+
'mx-auto',
|
|
59
|
+
'w-full',
|
|
60
|
+
'max-w-[calc(240px_+_1120px)]',
|
|
61
|
+
'min-[768px]:flex-row',
|
|
62
|
+
);
|
|
57
63
|
expect(screen.getByRole('link', {name: 'Summary'})).toHaveAttribute('aria-current', 'page');
|
|
58
64
|
expect(screen.getByRole('heading', {name: 'Jobs'})).toBeInTheDocument();
|
|
59
65
|
expect(screen.getByRole('heading', {name: 'Run details'})).toBeInTheDocument();
|
|
60
66
|
expect(screen.getByRole('region', {name: 'All jobs summary'})).toBeInTheDocument();
|
|
61
67
|
expect(screen.getByRole('region', {name: 'Workflow jobs'})).toBeInTheDocument();
|
|
62
|
-
expect(container.querySelector('[data-run-workspace-content]')).toHaveClass(
|
|
68
|
+
expect(container.querySelector('[data-run-workspace-content]')).toHaveClass('flex-1');
|
|
69
|
+
expect(container.querySelector('[data-run-workspace-content]')).not.toHaveClass(
|
|
63
70
|
'bg-background-neutral-base',
|
|
64
|
-
'flex-1',
|
|
65
71
|
);
|
|
66
72
|
expect(screen.queryByRole('tab', {name: 'Jobs'})).not.toBeInTheDocument();
|
|
67
73
|
});
|
|
68
74
|
|
|
75
|
+
test.each([
|
|
76
|
+
{tab: undefined, region: 'All jobs summary'},
|
|
77
|
+
{tab: 'source', region: 'Workflow source'},
|
|
78
|
+
{tab: 'annotations', region: 'Run annotations'},
|
|
79
|
+
] as const)('does not cap the $region section at 1120px', async ({tab, region}) => {
|
|
80
|
+
configureRunFetch();
|
|
81
|
+
|
|
82
|
+
renderView({tab});
|
|
83
|
+
|
|
84
|
+
const section = await screen.findByRole('region', {name: region});
|
|
85
|
+
expect(section.firstElementChild).not.toHaveClass('max-w-[1120px]');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('keeps dedicated job content on the full-width data surface', async () => {
|
|
89
|
+
configureRunFetch();
|
|
90
|
+
|
|
91
|
+
renderView({jobContent: <div>Job logs</div>});
|
|
92
|
+
|
|
93
|
+
const jobContent = await screen.findByText('Job logs');
|
|
94
|
+
const workspaceFrame = jobContent.closest('[data-run-workspace-frame]');
|
|
95
|
+
|
|
96
|
+
expect(workspaceFrame).toHaveClass('w-full', 'min-[768px]:flex-row');
|
|
97
|
+
expect(workspaceFrame).not.toHaveClass('max-w-[calc(240px_+_1120px)]');
|
|
98
|
+
});
|
|
99
|
+
|
|
69
100
|
test('treats the removed Jobs tab URL as the graph Summary', async () => {
|
|
70
101
|
configureRunFetch();
|
|
71
102
|
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from '@shipfox/react-ui/select';
|
|
12
12
|
import {toast} from '@shipfox/react-ui/toast';
|
|
13
13
|
import {Text} from '@shipfox/react-ui/typography';
|
|
14
|
+
import {cn} from '@shipfox/react-ui/utils';
|
|
14
15
|
import {useNavigate} from '@tanstack/react-router';
|
|
15
16
|
import {type ReactNode, useEffect, useMemo} from 'react';
|
|
16
17
|
import {buildRunAnnotationList, type RunAnnotationSummary} from '#core/run-annotation.js';
|
|
@@ -53,6 +54,7 @@ import {
|
|
|
53
54
|
} from './workflow-run-states.js';
|
|
54
55
|
|
|
55
56
|
type RunWorkspaceSection = Exclude<WorkflowRunTab, 'jobs'>;
|
|
57
|
+
const RUN_WORKSPACE_FRAME_CLASS_NAME = 'mx-auto w-full max-w-[calc(240px_+_1120px)]';
|
|
56
58
|
|
|
57
59
|
export interface WorkflowRunViewProps {
|
|
58
60
|
projectId: string;
|
|
@@ -157,6 +159,7 @@ function RunViewContent({
|
|
|
157
159
|
const activeSection = runWorkspaceSection(tab);
|
|
158
160
|
const cancelMutation = useCancelWorkflowRunMutation(runData);
|
|
159
161
|
const sourceSnapshot = runData?.sourceSnapshot ?? null;
|
|
162
|
+
const hasJobContent = Boolean(jobContent);
|
|
160
163
|
const resolvedSelection =
|
|
161
164
|
runData && selection ? resolveWorkflowRunSelection({run: runData, selection}) : undefined;
|
|
162
165
|
const highlightedLineRange = resolvedSelection?.step?.sourceLocation ?? null;
|
|
@@ -309,47 +312,52 @@ function RunViewContent({
|
|
|
309
312
|
|
|
310
313
|
<div
|
|
311
314
|
data-run-workspace-layout
|
|
312
|
-
className="flex min-h-0 min-w-0 flex-1 flex-col border-t border-border-neutral-base
|
|
315
|
+
className="flex min-h-0 min-w-0 flex-1 flex-col border-t border-border-neutral-base"
|
|
313
316
|
>
|
|
314
|
-
{runData && workspaceSlug && projectSlug ? (
|
|
315
|
-
<RunWorkspaceNav
|
|
316
|
-
workspaceSlug={workspaceSlug}
|
|
317
|
-
projectSlug={projectSlug}
|
|
318
|
-
run={runData}
|
|
319
|
-
activeSection={activeSection}
|
|
320
|
-
currentJobId={activeJobId}
|
|
321
|
-
jobSearch={jobSearch}
|
|
322
|
-
annotationSummary={annotationSummary}
|
|
323
|
-
/>
|
|
324
|
-
) : (
|
|
325
|
-
<RunWorkspaceNavSkeleton />
|
|
326
|
-
)}
|
|
327
|
-
|
|
328
317
|
<div
|
|
329
|
-
data-run-workspace-
|
|
330
|
-
className=
|
|
318
|
+
data-run-workspace-frame
|
|
319
|
+
className={cn(
|
|
320
|
+
'flex min-h-0 min-w-0 w-full flex-1 flex-col min-[768px]:flex-row',
|
|
321
|
+
!hasJobContent && RUN_WORKSPACE_FRAME_CLASS_NAME,
|
|
322
|
+
)}
|
|
331
323
|
>
|
|
332
|
-
{runData &&
|
|
333
|
-
|
|
334
|
-
) : runData ? (
|
|
335
|
-
<RunSectionContent
|
|
336
|
-
section={activeSection}
|
|
337
|
-
run={runData}
|
|
338
|
-
annotations={annotations}
|
|
339
|
-
annotationSummary={annotationSummary}
|
|
324
|
+
{runData && workspaceSlug && projectSlug ? (
|
|
325
|
+
<RunWorkspaceNav
|
|
340
326
|
workspaceSlug={workspaceSlug}
|
|
341
327
|
projectSlug={projectSlug}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
sourceSnapshot={sourceSnapshot}
|
|
348
|
-
highlightedLineRange={highlightedLineRange}
|
|
328
|
+
run={runData}
|
|
329
|
+
activeSection={activeSection}
|
|
330
|
+
currentJobId={activeJobId}
|
|
331
|
+
jobSearch={jobSearch}
|
|
332
|
+
annotationSummary={annotationSummary}
|
|
349
333
|
/>
|
|
350
334
|
) : (
|
|
351
|
-
<
|
|
335
|
+
<RunWorkspaceNavSkeleton />
|
|
352
336
|
)}
|
|
337
|
+
|
|
338
|
+
<div data-run-workspace-content className="flex min-h-0 min-w-0 flex-1 flex-col">
|
|
339
|
+
{runData && jobContent ? (
|
|
340
|
+
jobContent
|
|
341
|
+
) : runData ? (
|
|
342
|
+
<RunSectionContent
|
|
343
|
+
section={activeSection}
|
|
344
|
+
run={runData}
|
|
345
|
+
annotations={annotations}
|
|
346
|
+
annotationSummary={annotationSummary}
|
|
347
|
+
workspaceSlug={workspaceSlug}
|
|
348
|
+
projectSlug={projectSlug}
|
|
349
|
+
selection={selection}
|
|
350
|
+
selectedJobId={selection?.jobId}
|
|
351
|
+
onSelectGraphJob={selectGraphJob}
|
|
352
|
+
onSelectAnnotationJob={selectAnnotationJob}
|
|
353
|
+
onClearAnnotationFilters={clearAnnotationFilters}
|
|
354
|
+
sourceSnapshot={sourceSnapshot}
|
|
355
|
+
highlightedLineRange={highlightedLineRange}
|
|
356
|
+
/>
|
|
357
|
+
) : (
|
|
358
|
+
<div className="min-h-0 flex-1 overflow-auto p-panel">{loadingOrError}</div>
|
|
359
|
+
)}
|
|
360
|
+
</div>
|
|
353
361
|
</div>
|
|
354
362
|
</div>
|
|
355
363
|
</div>
|
|
@@ -395,7 +403,7 @@ function RunSectionContent({
|
|
|
395
403
|
aria-label="All jobs summary"
|
|
396
404
|
className="min-h-0 flex-1 overflow-auto pb-panel pt-[16px]"
|
|
397
405
|
>
|
|
398
|
-
<div className="mx-auto flex w-full
|
|
406
|
+
<div className="mx-auto flex w-full flex-col gap-group px-frame">
|
|
399
407
|
<Text as="h2" className="sr-only">
|
|
400
408
|
All jobs summary
|
|
401
409
|
</Text>
|
|
@@ -431,7 +439,7 @@ function RunSectionContent({
|
|
|
431
439
|
aria-label="Workflow source"
|
|
432
440
|
className="min-h-0 flex-1 overflow-auto pb-panel pt-[16px]"
|
|
433
441
|
>
|
|
434
|
-
<div className="mx-auto flex min-h-full w-full
|
|
442
|
+
<div className="mx-auto flex min-h-full w-full flex-col px-frame">
|
|
435
443
|
<Text as="h2" className="sr-only">
|
|
436
444
|
Workflow source
|
|
437
445
|
</Text>
|
|
@@ -526,7 +534,7 @@ function RunAnnotationsSection({
|
|
|
526
534
|
aria-label="Run annotations"
|
|
527
535
|
className="min-h-0 flex-1 overflow-auto pb-panel pt-[16px]"
|
|
528
536
|
>
|
|
529
|
-
<div className="mx-auto flex w-full
|
|
537
|
+
<div className="mx-auto flex w-full flex-col gap-group px-frame">
|
|
530
538
|
<Text as="h2" className="sr-only">
|
|
531
539
|
Annotations
|
|
532
540
|
</Text>
|
|
@@ -666,7 +674,7 @@ function RunWorkspaceNavSkeleton() {
|
|
|
666
674
|
return (
|
|
667
675
|
<aside
|
|
668
676
|
aria-label="Loading run navigation"
|
|
669
|
-
className="hidden w-240 shrink-0 border-r border-border-neutral-base
|
|
677
|
+
className="hidden w-240 shrink-0 border-r border-border-neutral-base p-panel-compact min-[768px]:block"
|
|
670
678
|
>
|
|
671
679
|
<div className="flex flex-col gap-group">
|
|
672
680
|
<div className="h-32 rounded-4 bg-background-components-subtle" />
|
|
@@ -63,8 +63,8 @@ export function WorkflowSourceContent({
|
|
|
63
63
|
value={item.filename}
|
|
64
64
|
className={cn(
|
|
65
65
|
'min-h-full px-0 pb-0',
|
|
66
|
-
'[&>div]:rounded-none [&>div]:border-0 [&>div]:bg-background-contrast-base
|
|
67
|
-
'[&_code]:!text-sm [&_code]:!text-foreground-
|
|
66
|
+
'[&>div]:rounded-none [&>div]:border-0 [&>div]:bg-background-contrast-base',
|
|
67
|
+
'[&_code]:!text-sm [&_code]:!text-foreground-contrast-primary [&_.line]:!text-sm [&_.line]:before:!text-sm [&_.line]:before:!text-foreground-contrast-secondary',
|
|
68
68
|
)}
|
|
69
69
|
>
|
|
70
70
|
<CodeBlockContent
|
|
@@ -12,6 +12,7 @@ import {Callout} from '@shipfox/react-ui/callout';
|
|
|
12
12
|
import {EmptyState} from '@shipfox/react-ui/empty-state';
|
|
13
13
|
import {Icon, type IconName} from '@shipfox/react-ui/icon';
|
|
14
14
|
import {LoadErrorState} from '@shipfox/react-ui/load-error-state';
|
|
15
|
+
import {Panel} from '@shipfox/react-ui/panel';
|
|
15
16
|
import {RelativeTime, RelativeTimeProvider} from '@shipfox/react-ui/relative-time';
|
|
16
17
|
import {
|
|
17
18
|
Sheet,
|
|
@@ -195,7 +196,7 @@ function WorkflowDefinitionsList({
|
|
|
195
196
|
|
|
196
197
|
return (
|
|
197
198
|
<>
|
|
198
|
-
<
|
|
199
|
+
<Panel className="hidden md:flex">
|
|
199
200
|
<Table>
|
|
200
201
|
<TableHeader>
|
|
201
202
|
<TableRow>
|
|
@@ -265,7 +266,7 @@ function WorkflowDefinitionsList({
|
|
|
265
266
|
})}
|
|
266
267
|
</TableBody>
|
|
267
268
|
</Table>
|
|
268
|
-
</
|
|
269
|
+
</Panel>
|
|
269
270
|
|
|
270
271
|
<div className="flex flex-col rounded-8 border border-border-neutral-base md:hidden">
|
|
271
272
|
{definitions.map((definition) => {
|
|
@@ -41,6 +41,19 @@ describe('WorkflowJobDetailPage', () => {
|
|
|
41
41
|
jobAnnotations.value = [];
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
test('lets the job detail route inherit its shell canvas', async () => {
|
|
45
|
+
configureApiClient({fetchImpl: vi.fn(() => new Promise<Response>(() => undefined))});
|
|
46
|
+
|
|
47
|
+
const {container} = renderJobPath();
|
|
48
|
+
|
|
49
|
+
expect(await screen.findByRole('region', {name: 'Loading workflow run'})).toBeInTheDocument();
|
|
50
|
+
|
|
51
|
+
const pageRoot = container.querySelector('[data-workflow-page-root="job-detail"]');
|
|
52
|
+
|
|
53
|
+
expect(pageRoot).not.toBeNull();
|
|
54
|
+
expect(pageRoot).not.toHaveClass('bg-background-subtle-base');
|
|
55
|
+
});
|
|
56
|
+
|
|
44
57
|
test('links the job to its annotations without rendering one', async () => {
|
|
45
58
|
jobAnnotations.value = [
|
|
46
59
|
{
|
|
@@ -88,6 +101,7 @@ describe('WorkflowJobDetailPage', () => {
|
|
|
88
101
|
});
|
|
89
102
|
|
|
90
103
|
test('resolves an exact job execution and step attempt from a deep link', async () => {
|
|
104
|
+
const user = userEvent.setup();
|
|
91
105
|
configureApiClient({fetchImpl: vi.fn(jobDetailFetch)});
|
|
92
106
|
|
|
93
107
|
renderJobPath(
|
|
@@ -104,6 +118,24 @@ describe('WorkflowJobDetailPage', () => {
|
|
|
104
118
|
await screen.findByRole('region', {name: 'tests output, attempt 2'}),
|
|
105
119
|
).toBeInTheDocument();
|
|
106
120
|
expect(screen.getByRole('region', {name: 'release logs'})).toBeInTheDocument();
|
|
121
|
+
expect(screen.getByRole('textbox', {name: 'Search logs'})).toBeInTheDocument();
|
|
122
|
+
expect(screen.getByRole('button', {name: 'Refresh logs'})).toBeInTheDocument();
|
|
123
|
+
expect(screen.getByRole('button', {name: 'Log settings'})).toBeInTheDocument();
|
|
124
|
+
const testsAttempt = screen.getByRole('button', {name: 'tests, Succeeded, attempt 2'});
|
|
125
|
+
await user.click(testsAttempt);
|
|
126
|
+
await waitFor(() =>
|
|
127
|
+
expect(
|
|
128
|
+
screen.queryByRole('region', {name: 'tests output, attempt 2'}),
|
|
129
|
+
).not.toBeInTheDocument(),
|
|
130
|
+
);
|
|
131
|
+
expect(screen.getByRole('textbox', {name: 'Search logs'})).toBeDisabled();
|
|
132
|
+
expect(screen.getByRole('button', {name: 'Refresh logs'})).toBeDisabled();
|
|
133
|
+
|
|
134
|
+
await user.click(testsAttempt);
|
|
135
|
+
expect(
|
|
136
|
+
await screen.findByRole('region', {name: 'tests output, attempt 2'}),
|
|
137
|
+
).toBeInTheDocument();
|
|
138
|
+
expect(screen.getByRole('textbox', {name: 'Search logs'})).not.toBeDisabled();
|
|
107
139
|
expect(screen.queryByRole('tab')).not.toBeInTheDocument();
|
|
108
140
|
expect(screen.getByRole('img', {name: 'Job status: Succeeded'})).toBeInTheDocument();
|
|
109
141
|
expect(screen.getByRole('heading', {name: 'deploy-web'})).toBeInTheDocument();
|
|
@@ -56,7 +56,10 @@ export function WorkflowJobDetailPage({
|
|
|
56
56
|
[navigate],
|
|
57
57
|
);
|
|
58
58
|
return (
|
|
59
|
-
<div
|
|
59
|
+
<div
|
|
60
|
+
data-workflow-page-root="job-detail"
|
|
61
|
+
className="flex min-h-0 min-w-0 flex-1 overflow-hidden"
|
|
62
|
+
>
|
|
60
63
|
<WorkflowRunView
|
|
61
64
|
projectId={projectId}
|
|
62
65
|
workspaceSlug={workspaceSlug}
|
|
@@ -67,6 +70,7 @@ export function WorkflowJobDetailPage({
|
|
|
67
70
|
jobSearch={search}
|
|
68
71
|
jobContent={
|
|
69
72
|
<JobDetailView
|
|
73
|
+
key={jobId}
|
|
70
74
|
workspaceSlug={workspaceSlug}
|
|
71
75
|
projectSlug={projectSlug}
|
|
72
76
|
workflowRunId={workflowRunId}
|
|
@@ -17,7 +17,7 @@ export function WorkflowRunDetailPage({
|
|
|
17
17
|
search = {},
|
|
18
18
|
}: WorkflowRunDetailPageProps) {
|
|
19
19
|
return (
|
|
20
|
-
<div className="flex min-h-0 flex-1 overflow-hidden
|
|
20
|
+
<div data-workflow-page-root="run-detail" className="flex min-h-0 flex-1 overflow-hidden">
|
|
21
21
|
<WorkflowRunView
|
|
22
22
|
projectId={projectId}
|
|
23
23
|
workspaceSlug={workspaceSlug}
|
|
@@ -49,17 +49,15 @@ export function WorkflowRunsPage({
|
|
|
49
49
|
);
|
|
50
50
|
|
|
51
51
|
return (
|
|
52
|
-
<div className="flex min-h-0 flex-1 overflow-hidden
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
/>
|
|
62
|
-
</div>
|
|
52
|
+
<div data-workflow-page-root="runs" className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
53
|
+
<WorkflowRunList
|
|
54
|
+
projectId={projectId}
|
|
55
|
+
workspaceSlug={workspaceSlug}
|
|
56
|
+
projectSlug={projectSlug}
|
|
57
|
+
search={search}
|
|
58
|
+
onFiltersChange={onFiltersChange}
|
|
59
|
+
onClearFilters={onClearFilters}
|
|
60
|
+
/>
|
|
63
61
|
</div>
|
|
64
62
|
);
|
|
65
63
|
}
|
|
@@ -72,6 +72,19 @@ describe('WorkflowRunPages', () => {
|
|
|
72
72
|
expect(screen.queryByLabelText('Loading workflow run')).not.toBeInTheDocument();
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
+
test('lets the list route inherit its shell canvas and width', async () => {
|
|
76
|
+
configureApiClient({fetchImpl: vi.fn(() => new Promise<Response>(() => undefined))});
|
|
77
|
+
|
|
78
|
+
const {container} = renderRunsPath();
|
|
79
|
+
|
|
80
|
+
await screen.findByLabelText('Workflow runs');
|
|
81
|
+
const pageRoot = container.querySelector('[data-workflow-page-root="runs"]');
|
|
82
|
+
|
|
83
|
+
expect(pageRoot).not.toBeNull();
|
|
84
|
+
expect(pageRoot).not.toHaveClass('bg-background-neutral-base');
|
|
85
|
+
expect(pageRoot).not.toHaveClass('max-w-[1120px]');
|
|
86
|
+
});
|
|
87
|
+
|
|
75
88
|
test('keeps the list route and filters in place instead of redirecting to a run', async () => {
|
|
76
89
|
configureApiClient({fetchImpl: createRunsListFetch()});
|
|
77
90
|
|
|
@@ -184,6 +197,19 @@ describe('WorkflowRunPages', () => {
|
|
|
184
197
|
expect(currentSearch(router).tab).toBeUndefined();
|
|
185
198
|
});
|
|
186
199
|
|
|
200
|
+
test('lets the run detail route inherit its shell canvas', async () => {
|
|
201
|
+
configureApiClient({fetchImpl: vi.fn(() => new Promise<Response>(() => undefined))});
|
|
202
|
+
|
|
203
|
+
const {container} = renderRunPath();
|
|
204
|
+
|
|
205
|
+
expect(await screen.findByRole('region', {name: 'Loading workflow run'})).toBeInTheDocument();
|
|
206
|
+
|
|
207
|
+
const pageRoot = container.querySelector('[data-workflow-page-root="run-detail"]');
|
|
208
|
+
|
|
209
|
+
expect(pageRoot).not.toBeNull();
|
|
210
|
+
expect(pageRoot).not.toHaveClass('bg-background-subtle-base');
|
|
211
|
+
});
|
|
212
|
+
|
|
187
213
|
test('maps the removed Jobs tab URL to the graph Summary', async () => {
|
|
188
214
|
configureApiClient({fetchImpl: createRunDetailFetch()});
|
|
189
215
|
|
|
@@ -4,7 +4,7 @@ import {validateWorkflowJobSearch, workflowJobRouteParams} from './inputs.js';
|
|
|
4
4
|
import {ProjectRoute} from './project-route.js';
|
|
5
5
|
|
|
6
6
|
export default defineRoute({
|
|
7
|
-
staticData: {
|
|
7
|
+
staticData: {frame: 'data'},
|
|
8
8
|
validateSearch: validateWorkflowJobSearch,
|
|
9
9
|
component: () => {
|
|
10
10
|
const {workspaceSlug, projectSlug, workflowRunId, jobId} =
|
|
@@ -4,7 +4,7 @@ import {validateWorkflowRunsSearch, workflowRouteParams} from './inputs.js';
|
|
|
4
4
|
import {ProjectRoute} from './project-route.js';
|
|
5
5
|
|
|
6
6
|
export default defineRoute({
|
|
7
|
-
staticData: {
|
|
7
|
+
staticData: {frame: 'data'},
|
|
8
8
|
validateSearch: validateWorkflowRunsSearch,
|
|
9
9
|
component: () => {
|
|
10
10
|
const {workspaceSlug, projectSlug, workflowRunId} = useRouteParams(workflowRouteParams);
|
package/src/routes/runs.tsx
CHANGED
|
@@ -4,7 +4,7 @@ import {validateWorkflowRunsSearch, workflowRouteParams} from './inputs.js';
|
|
|
4
4
|
import {ProjectRoute} from './project-route.js';
|
|
5
5
|
|
|
6
6
|
export default defineRoute({
|
|
7
|
-
staticData: {
|
|
7
|
+
staticData: {frame: 'data'},
|
|
8
8
|
validateSearch: validateWorkflowRunsSearch,
|
|
9
9
|
component: () => {
|
|
10
10
|
const {workspaceSlug, projectSlug} = useRouteParams(workflowRouteParams);
|
package/src/routes/workflows.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import {ProjectWorkflowsPage} from '#pages/project-workflows-page.js';
|
|
|
3
3
|
import {ProjectRoute} from './project-route.js';
|
|
4
4
|
|
|
5
5
|
export default defineRoute({
|
|
6
|
+
staticData: {frame: 'data'},
|
|
6
7
|
component: () => {
|
|
7
8
|
return (
|
|
8
9
|
<ProjectRoute>{(project) => <ProjectWorkflowsPage projectId={project.id} />}</ProjectRoute>
|