@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.
Files changed (105) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +29 -0
  3. package/dist/components/job-detail/job-detail-view.d.ts.map +1 -1
  4. package/dist/components/job-detail/job-detail-view.js +231 -42
  5. package/dist/components/job-detail/job-detail-view.js.map +1 -1
  6. package/dist/components/job-detail/step-attempt-log-panel.d.ts +7 -1
  7. package/dist/components/job-detail/step-attempt-log-panel.d.ts.map +1 -1
  8. package/dist/components/job-detail/step-attempt-log-panel.js +37 -10
  9. package/dist/components/job-detail/step-attempt-log-panel.js.map +1 -1
  10. package/dist/components/step-list/step-list-model.d.ts +1 -0
  11. package/dist/components/step-list/step-list-model.d.ts.map +1 -1
  12. package/dist/components/step-list/step-list-model.js +1 -1
  13. package/dist/components/step-list/step-list-model.js.map +1 -1
  14. package/dist/components/step-list/step-list.d.ts +2 -1
  15. package/dist/components/step-list/step-list.d.ts.map +1 -1
  16. package/dist/components/step-list/step-list.js +24 -7
  17. package/dist/components/step-list/step-list.js.map +1 -1
  18. package/dist/components/workflow-run-list/job-status-strip.d.ts +9 -1
  19. package/dist/components/workflow-run-list/job-status-strip.d.ts.map +1 -1
  20. package/dist/components/workflow-run-list/job-status-strip.js +22 -11
  21. package/dist/components/workflow-run-list/job-status-strip.js.map +1 -1
  22. package/dist/components/workflow-run-list/workflow-run-filters.js +1 -1
  23. package/dist/components/workflow-run-list/workflow-run-filters.js.map +1 -1
  24. package/dist/components/workflow-run-list/workflow-run-list-states.js +2 -2
  25. package/dist/components/workflow-run-list/workflow-run-list-states.js.map +1 -1
  26. package/dist/components/workflow-run-list/workflow-run-list-view.d.ts.map +1 -1
  27. package/dist/components/workflow-run-list/workflow-run-list-view.js +34 -22
  28. package/dist/components/workflow-run-list/workflow-run-list-view.js.map +1 -1
  29. package/dist/components/workflow-run-list/workflow-run-row.d.ts +3 -3
  30. package/dist/components/workflow-run-list/workflow-run-row.d.ts.map +1 -1
  31. package/dist/components/workflow-run-list/workflow-run-row.js +13 -10
  32. package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
  33. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
  34. package/dist/components/workflow-run-summary/workflow-run-summary.js +1 -1
  35. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
  36. package/dist/components/workflow-run-view/run-workspace-nav.js +1 -1
  37. package/dist/components/workflow-run-view/run-workspace-nav.js.map +1 -1
  38. package/dist/components/workflow-run-view/workflow-run-states.d.ts.map +1 -1
  39. package/dist/components/workflow-run-view/workflow-run-states.js +1 -1
  40. package/dist/components/workflow-run-view/workflow-run-states.js.map +1 -1
  41. package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
  42. package/dist/components/workflow-run-view/workflow-run-view.js +43 -36
  43. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
  44. package/dist/components/workflow-source-panel/workflow-source-content.js +1 -1
  45. package/dist/components/workflow-source-panel/workflow-source-content.js.map +1 -1
  46. package/dist/pages/project-workflows-page.d.ts.map +1 -1
  47. package/dist/pages/project-workflows-page.js +3 -2
  48. package/dist/pages/project-workflows-page.js.map +1 -1
  49. package/dist/pages/workflow-job-detail-page.d.ts.map +1 -1
  50. package/dist/pages/workflow-job-detail-page.js +3 -2
  51. package/dist/pages/workflow-job-detail-page.js.map +1 -1
  52. package/dist/pages/workflow-run-detail-page.js +2 -1
  53. package/dist/pages/workflow-run-detail-page.js.map +1 -1
  54. package/dist/pages/workflow-run-list-page.d.ts.map +1 -1
  55. package/dist/pages/workflow-run-list-page.js +9 -11
  56. package/dist/pages/workflow-run-list-page.js.map +1 -1
  57. package/dist/routes/job-detail.d.ts +1 -1
  58. package/dist/routes/job-detail.js +1 -1
  59. package/dist/routes/job-detail.js.map +1 -1
  60. package/dist/routes/run-detail.d.ts +1 -1
  61. package/dist/routes/run-detail.js +1 -1
  62. package/dist/routes/run-detail.js.map +1 -1
  63. package/dist/routes/runs.d.ts +1 -1
  64. package/dist/routes/runs.js +1 -1
  65. package/dist/routes/runs.js.map +1 -1
  66. package/dist/routes/workflows.d.ts +3 -0
  67. package/dist/routes/workflows.js +3 -0
  68. package/dist/routes/workflows.js.map +1 -1
  69. package/dist/tsconfig.test.tsbuildinfo +1 -1
  70. package/package.json +7 -7
  71. package/src/components/job-detail/job-detail-view.tsx +263 -47
  72. package/src/components/job-detail/step-attempt-log-panel.test.tsx +30 -12
  73. package/src/components/job-detail/step-attempt-log-panel.tsx +68 -10
  74. package/src/components/job-graph/job-graph.stories.tsx +1 -1
  75. package/src/components/job-graph/job-node.stories.tsx +1 -1
  76. package/src/components/step-list/step-list-model.ts +1 -1
  77. package/src/components/step-list/step-list.test.tsx +29 -1
  78. package/src/components/step-list/step-list.tsx +28 -9
  79. package/src/components/workflow-run-list/job-status-strip.tsx +25 -12
  80. package/src/components/workflow-run-list/workflow-run-filters.tsx +1 -1
  81. package/src/components/workflow-run-list/workflow-run-list-states.tsx +2 -2
  82. package/src/components/workflow-run-list/workflow-run-list-view.test.tsx +53 -4
  83. package/src/components/workflow-run-list/workflow-run-list-view.tsx +28 -22
  84. package/src/components/workflow-run-list/workflow-run-list.stories.tsx +9 -20
  85. package/src/components/workflow-run-list/workflow-run-row.tsx +17 -13
  86. package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +2 -2
  87. package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +2 -1
  88. package/src/components/workflow-run-summary/workflow-run-summary.tsx +1 -4
  89. package/src/components/workflow-run-view/run-workspace-nav.tsx +1 -1
  90. package/src/components/workflow-run-view/workflow-run-states.tsx +1 -4
  91. package/src/components/workflow-run-view/workflow-run-view.stories.tsx +140 -0
  92. package/src/components/workflow-run-view/workflow-run-view.test.tsx +38 -7
  93. package/src/components/workflow-run-view/workflow-run-view.tsx +45 -37
  94. package/src/components/workflow-source-panel/workflow-source-content.tsx +2 -2
  95. package/src/pages/project-workflows-page.tsx +3 -2
  96. package/src/pages/workflow-job-detail-page.test.tsx +32 -0
  97. package/src/pages/workflow-job-detail-page.tsx +5 -1
  98. package/src/pages/workflow-run-detail-page.tsx +1 -1
  99. package/src/pages/workflow-run-list-page.tsx +9 -11
  100. package/src/pages/workflow-run-pages.test.tsx +26 -0
  101. package/src/routes/job-detail.tsx +1 -1
  102. package/src/routes/run-detail.tsx +1 -1
  103. package/src/routes/runs.tsx +1 -1
  104. package/src/routes/workflows.tsx +1 -0
  105. 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
- expect(
53
- screen
54
- .getByRole('navigation', {name: 'Run workspace'})
55
- .closest('[data-run-workspace-layout]'),
56
- ).toHaveClass('border-t', 'border-border-neutral-base', 'min-[768px]:flex-row');
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 min-[768px]:flex-row"
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-content
330
- className="flex min-h-0 min-w-0 flex-1 flex-col bg-background-neutral-base"
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 && jobContent ? (
333
- jobContent
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
- selection={selection}
343
- selectedJobId={selection?.jobId}
344
- onSelectGraphJob={selectGraphJob}
345
- onSelectAnnotationJob={selectAnnotationJob}
346
- onClearAnnotationFilters={clearAnnotationFilters}
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
- <div className="min-h-0 flex-1 overflow-auto p-panel">{loadingOrError}</div>
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 max-w-[1120px] flex-col gap-group px-frame">
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 max-w-[1120px] flex-col px-frame">
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 max-w-[1120px] flex-col gap-group px-frame">
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 bg-background-neutral-background p-panel-compact min-[768px]:block"
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 [&>div]:dark:bg-background-contrast-base',
67
- '[&_code]:!text-sm [&_code]:!text-foreground-neutral-on-inverted [&_.line]:!text-sm [&_.line]:before:!text-sm [&_.line]:before:!text-foreground-neutral-muted',
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
- <div className="hidden rounded-8 border border-border-neutral-base md:block">
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
- </div>
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 className="flex min-h-0 flex-1 overflow-hidden bg-background-neutral-background">
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 bg-background-neutral-background">
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 bg-background-neutral-base">
53
- <div className="mx-auto flex min-h-0 w-full max-w-[1120px] flex-1 flex-col px-frame py-frame">
54
- <WorkflowRunList
55
- projectId={projectId}
56
- workspaceSlug={workspaceSlug}
57
- projectSlug={projectSlug}
58
- search={search}
59
- onFiltersChange={onFiltersChange}
60
- onClearFilters={onClearFilters}
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: {layout: 'full-bleed'},
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: {layout: 'full-bleed'},
7
+ staticData: {frame: 'data'},
8
8
  validateSearch: validateWorkflowRunsSearch,
9
9
  component: () => {
10
10
  const {workspaceSlug, projectSlug, workflowRunId} = useRouteParams(workflowRouteParams);
@@ -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: {layout: 'full-bleed'},
7
+ staticData: {frame: 'data'},
8
8
  validateSearch: validateWorkflowRunsSearch,
9
9
  component: () => {
10
10
  const {workspaceSlug, projectSlug} = useRouteParams(workflowRouteParams);
@@ -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>