@shipfox/client-workflows 20.0.0 → 22.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 (118) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +51 -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 +232 -44
  5. package/dist/components/job-detail/job-detail-view.js.map +1 -1
  6. package/dist/components/job-detail/job-empty-states.js +2 -2
  7. package/dist/components/job-detail/job-empty-states.js.map +1 -1
  8. package/dist/components/job-detail/step-attempt-log-panel.d.ts +7 -1
  9. package/dist/components/job-detail/step-attempt-log-panel.d.ts.map +1 -1
  10. package/dist/components/job-detail/step-attempt-log-panel.js +37 -10
  11. package/dist/components/job-detail/step-attempt-log-panel.js.map +1 -1
  12. package/dist/components/job-graph/job-graph-content.js +2 -2
  13. package/dist/components/job-graph/job-graph-content.js.map +1 -1
  14. package/dist/components/step-list/step-list-model.d.ts +1 -0
  15. package/dist/components/step-list/step-list-model.d.ts.map +1 -1
  16. package/dist/components/step-list/step-list-model.js +1 -1
  17. package/dist/components/step-list/step-list-model.js.map +1 -1
  18. package/dist/components/step-list/step-list.d.ts +2 -1
  19. package/dist/components/step-list/step-list.d.ts.map +1 -1
  20. package/dist/components/step-list/step-list.js +25 -9
  21. package/dist/components/step-list/step-list.js.map +1 -1
  22. package/dist/components/workflow-run-list/job-status-strip.d.ts +9 -1
  23. package/dist/components/workflow-run-list/job-status-strip.d.ts.map +1 -1
  24. package/dist/components/workflow-run-list/job-status-strip.js +22 -11
  25. package/dist/components/workflow-run-list/job-status-strip.js.map +1 -1
  26. package/dist/components/workflow-run-list/workflow-run-filters.js +1 -1
  27. package/dist/components/workflow-run-list/workflow-run-filters.js.map +1 -1
  28. package/dist/components/workflow-run-list/workflow-run-list-content.d.ts.map +1 -1
  29. package/dist/components/workflow-run-list/workflow-run-list-content.js +13 -7
  30. package/dist/components/workflow-run-list/workflow-run-list-content.js.map +1 -1
  31. package/dist/components/workflow-run-list/workflow-run-list-states.d.ts.map +1 -1
  32. package/dist/components/workflow-run-list/workflow-run-list-states.js +43 -38
  33. package/dist/components/workflow-run-list/workflow-run-list-states.js.map +1 -1
  34. package/dist/components/workflow-run-list/workflow-run-list-view.d.ts.map +1 -1
  35. package/dist/components/workflow-run-list/workflow-run-list-view.js +34 -22
  36. package/dist/components/workflow-run-list/workflow-run-list-view.js.map +1 -1
  37. package/dist/components/workflow-run-list/workflow-run-row.d.ts +3 -3
  38. package/dist/components/workflow-run-list/workflow-run-row.d.ts.map +1 -1
  39. package/dist/components/workflow-run-list/workflow-run-row.js +25 -18
  40. package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
  41. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
  42. package/dist/components/workflow-run-summary/workflow-run-summary.js +4 -4
  43. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
  44. package/dist/components/workflow-run-view/run-workspace-nav.d.ts.map +1 -1
  45. package/dist/components/workflow-run-view/run-workspace-nav.js +20 -7
  46. package/dist/components/workflow-run-view/run-workspace-nav.js.map +1 -1
  47. package/dist/components/workflow-run-view/workflow-run-states.d.ts.map +1 -1
  48. package/dist/components/workflow-run-view/workflow-run-states.js +1 -1
  49. package/dist/components/workflow-run-view/workflow-run-states.js.map +1 -1
  50. package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
  51. package/dist/components/workflow-run-view/workflow-run-view.js +103 -78
  52. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
  53. package/dist/components/workflow-source-panel/workflow-source-content.js +1 -1
  54. package/dist/components/workflow-source-panel/workflow-source-content.js.map +1 -1
  55. package/dist/pages/project-workflows-page.d.ts.map +1 -1
  56. package/dist/pages/project-workflows-page.js +171 -158
  57. package/dist/pages/project-workflows-page.js.map +1 -1
  58. package/dist/pages/workflow-job-detail-page.d.ts.map +1 -1
  59. package/dist/pages/workflow-job-detail-page.js +3 -2
  60. package/dist/pages/workflow-job-detail-page.js.map +1 -1
  61. package/dist/pages/workflow-run-detail-page.js +2 -1
  62. package/dist/pages/workflow-run-detail-page.js.map +1 -1
  63. package/dist/pages/workflow-run-list-page.d.ts.map +1 -1
  64. package/dist/pages/workflow-run-list-page.js +9 -11
  65. package/dist/pages/workflow-run-list-page.js.map +1 -1
  66. package/dist/routes/job-detail.d.ts +1 -1
  67. package/dist/routes/job-detail.js +1 -1
  68. package/dist/routes/job-detail.js.map +1 -1
  69. package/dist/routes/run-detail.d.ts +1 -1
  70. package/dist/routes/run-detail.js +1 -1
  71. package/dist/routes/run-detail.js.map +1 -1
  72. package/dist/routes/runs.d.ts +1 -1
  73. package/dist/routes/runs.js +1 -1
  74. package/dist/routes/runs.js.map +1 -1
  75. package/dist/routes/workflows.d.ts +3 -0
  76. package/dist/routes/workflows.js +3 -0
  77. package/dist/routes/workflows.js.map +1 -1
  78. package/dist/tsconfig.test.tsbuildinfo +1 -1
  79. package/package.json +8 -8
  80. package/src/components/job-detail/job-detail-view.tsx +264 -49
  81. package/src/components/job-detail/job-empty-states.tsx +2 -2
  82. package/src/components/job-detail/step-attempt-log-panel.test.tsx +30 -12
  83. package/src/components/job-detail/step-attempt-log-panel.tsx +68 -10
  84. package/src/components/job-graph/job-graph-content.tsx +2 -2
  85. package/src/components/job-graph/job-graph.stories.tsx +1 -1
  86. package/src/components/job-graph/job-node.stories.tsx +1 -1
  87. package/src/components/step-list/step-list-model.ts +1 -1
  88. package/src/components/step-list/step-list.test.tsx +29 -1
  89. package/src/components/step-list/step-list.tsx +29 -11
  90. package/src/components/workflow-run-list/job-status-strip.tsx +25 -12
  91. package/src/components/workflow-run-list/workflow-run-filters.tsx +1 -1
  92. package/src/components/workflow-run-list/workflow-run-list-content.tsx +7 -2
  93. package/src/components/workflow-run-list/workflow-run-list-states.tsx +42 -41
  94. package/src/components/workflow-run-list/workflow-run-list-view.test.tsx +53 -4
  95. package/src/components/workflow-run-list/workflow-run-list-view.tsx +28 -22
  96. package/src/components/workflow-run-list/workflow-run-list.stories.tsx +9 -20
  97. package/src/components/workflow-run-list/workflow-run-row.tsx +26 -19
  98. package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +3 -3
  99. package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +11 -2
  100. package/src/components/workflow-run-summary/workflow-run-summary.tsx +4 -7
  101. package/src/components/workflow-run-view/run-workspace-nav.test.tsx +40 -2
  102. package/src/components/workflow-run-view/run-workspace-nav.tsx +28 -9
  103. package/src/components/workflow-run-view/workflow-run-states.tsx +1 -4
  104. package/src/components/workflow-run-view/workflow-run-view.stories.tsx +142 -0
  105. package/src/components/workflow-run-view/workflow-run-view.test.tsx +42 -8
  106. package/src/components/workflow-run-view/workflow-run-view.tsx +112 -96
  107. package/src/components/workflow-source-panel/workflow-source-content.tsx +2 -2
  108. package/src/pages/project-workflows-page.tsx +135 -125
  109. package/src/pages/workflow-job-detail-page.test.tsx +32 -0
  110. package/src/pages/workflow-job-detail-page.tsx +5 -1
  111. package/src/pages/workflow-run-detail-page.tsx +1 -1
  112. package/src/pages/workflow-run-list-page.tsx +9 -11
  113. package/src/pages/workflow-run-pages.test.tsx +26 -0
  114. package/src/routes/job-detail.tsx +1 -1
  115. package/src/routes/run-detail.tsx +1 -1
  116. package/src/routes/runs.tsx +1 -1
  117. package/src/routes/workflows.tsx +1 -0
  118. package/tsconfig.build.tsbuildinfo +1 -1
@@ -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,
@@ -170,152 +171,161 @@ function WorkflowDefinitionsList({
170
171
  }) {
171
172
  if (isPending) {
172
173
  return (
173
- <div className="flex flex-col gap-inline">
174
- <Skeleton className="h-40 w-full" />
175
- <Skeleton className="h-40 w-full" />
176
- <Skeleton className="h-40 w-full" />
177
- </div>
174
+ <Panel role="status" aria-label="Loading workflows" className="divide-y">
175
+ <Skeleton className="h-44 w-full rounded-none" />
176
+ <Skeleton className="h-44 w-full rounded-none" />
177
+ <Skeleton className="h-44 w-full rounded-none" />
178
+ </Panel>
178
179
  );
179
180
  }
180
181
 
181
182
  if (isError && definitions.length === 0) {
182
183
  return (
183
- <LoadErrorState
184
- title="Couldn't load workflows"
185
- description="Definitions could not be loaded. Source metadata remains visible."
186
- onRetry={onRetry}
187
- retryLabel="Retry loading workflows"
188
- />
184
+ <Panel>
185
+ <LoadErrorState
186
+ title="Couldn't load workflows"
187
+ description="Definitions could not be loaded. Source metadata remains visible."
188
+ onRetry={onRetry}
189
+ retryLabel="Retry loading workflows"
190
+ variant="panel"
191
+ />
192
+ </Panel>
189
193
  );
190
194
  }
191
195
 
192
196
  if (definitions.length === 0) {
193
- return <WorkflowEmptyState sync={sync} />;
197
+ return (
198
+ <Panel>
199
+ <WorkflowEmptyState sync={sync} />
200
+ </Panel>
201
+ );
194
202
  }
195
203
 
196
204
  return (
197
205
  <>
198
- <div className="hidden rounded-8 border border-border-neutral-base md:block">
199
- <Table>
200
- <TableHeader>
201
- <TableRow>
202
- <TableHead className="w-40"></TableHead>
203
- <TableHead>Workflow</TableHead>
204
- <TableHead className="w-180">Updated</TableHead>
205
- <TableHead className="w-80 text-right"></TableHead>
206
- </TableRow>
207
- </TableHeader>
208
- <TableBody>
209
- {definitions.map((definition) => {
210
- const runErrorMessage =
211
- runError?.definitionId === definition.id ? runError.message : null;
212
- const isRunning = runningDefinitionId === definition.id;
206
+ <Panel>
207
+ <div className="hidden md:block">
208
+ <Table>
209
+ <TableHeader>
210
+ <TableRow>
211
+ <TableHead className="w-40"></TableHead>
212
+ <TableHead>Workflow</TableHead>
213
+ <TableHead className="w-180">Updated</TableHead>
214
+ <TableHead className="w-80 text-right"></TableHead>
215
+ </TableRow>
216
+ </TableHeader>
217
+ <TableBody>
218
+ {definitions.map((definition) => {
219
+ const runErrorMessage =
220
+ runError?.definitionId === definition.id ? runError.message : null;
221
+ const isRunning = runningDefinitionId === definition.id;
213
222
 
214
- return (
215
- // The workflow-name cell holds a `<button>` so the row is
216
- // keyboard-reachable (Tab focuses, Enter/Space activates
217
- // via native button semantics). The TableRow itself is no
218
- // longer clickable: a row-level onClick would be invisible
219
- // to keyboard users and require custom keydown handling.
220
- // The `group` class on the row still drives the Run button
221
- // reveal on hover or focus-within.
222
- <TableRow key={definition.id} className="group">
223
- <TableCell>
224
- <Icon
225
- name={sourceIcon(definition.source)}
226
- className="size-16 text-foreground-neutral-muted"
227
- aria-hidden="true"
228
- />
229
- </TableCell>
230
- <TableCell className="max-w-260">
231
- <div className="flex min-w-0 flex-col gap-tight">
232
- <button
233
- type="button"
234
- onClick={() => onOpenDefinition(definition)}
235
- className="flex min-w-0 flex-col gap-tight text-left outline-none focus-visible:shadow-border-interactive-with-active rounded-4"
236
- >
237
- <Text size="sm" bold className="truncate">
238
- {definition.name}
239
- </Text>
240
- <Code className="truncate text-foreground-neutral-muted">
241
- {definition.configPath ?? 'Manual definition'}
242
- </Code>
243
- </button>
244
- {runErrorMessage ? (
245
- <Text size="xs" className="text-tag-error-text">
246
- {runErrorMessage}
247
- </Text>
248
- ) : null}
249
- </div>
250
- </TableCell>
251
- <TableCell className="text-foreground-neutral-muted">
252
- <RelativeTime value={definition.updatedAt} />
253
- </TableCell>
254
- <TableCell>
255
- {definition.manualTrigger ? (
256
- <div className="flex justify-end opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100">
257
- <Button size="xs" isLoading={isRunning} onClick={() => onRun(definition)}>
258
- Run
259
- </Button>
223
+ return (
224
+ // The workflow-name cell holds a `<button>` so the row is
225
+ // keyboard-reachable (Tab focuses, Enter/Space activates
226
+ // via native button semantics). The TableRow itself is no
227
+ // longer clickable: a row-level onClick would be invisible
228
+ // to keyboard users and require custom keydown handling.
229
+ // The `group` class on the row still drives the Run button
230
+ // reveal on hover or focus-within.
231
+ <TableRow key={definition.id} className="group">
232
+ <TableCell>
233
+ <Icon
234
+ name={sourceIcon(definition.source)}
235
+ className="size-16 text-foreground-neutral-muted"
236
+ aria-hidden="true"
237
+ />
238
+ </TableCell>
239
+ <TableCell className="max-w-260">
240
+ <div className="flex min-w-0 flex-col gap-tight">
241
+ <button
242
+ type="button"
243
+ onClick={() => onOpenDefinition(definition)}
244
+ className="flex min-w-0 flex-col gap-tight rounded-4 text-left outline-none focus-visible:shadow-border-interactive-with-active"
245
+ >
246
+ <Text size="sm" bold className="truncate">
247
+ {definition.name}
248
+ </Text>
249
+ <Code className="truncate text-foreground-neutral-muted">
250
+ {definition.configPath ?? 'Manual definition'}
251
+ </Code>
252
+ </button>
253
+ {runErrorMessage ? (
254
+ <Text size="xs" className="text-tag-error-text">
255
+ {runErrorMessage}
256
+ </Text>
257
+ ) : null}
260
258
  </div>
261
- ) : null}
262
- </TableCell>
263
- </TableRow>
264
- );
265
- })}
266
- </TableBody>
267
- </Table>
268
- </div>
259
+ </TableCell>
260
+ <TableCell className="text-foreground-neutral-muted">
261
+ <RelativeTime value={definition.updatedAt} />
262
+ </TableCell>
263
+ <TableCell>
264
+ {definition.manualTrigger ? (
265
+ <div className="flex justify-end opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100">
266
+ <Button size="xs" isLoading={isRunning} onClick={() => onRun(definition)}>
267
+ Run
268
+ </Button>
269
+ </div>
270
+ ) : null}
271
+ </TableCell>
272
+ </TableRow>
273
+ );
274
+ })}
275
+ </TableBody>
276
+ </Table>
277
+ </div>
269
278
 
270
- <div className="flex flex-col rounded-8 border border-border-neutral-base md:hidden">
271
- {definitions.map((definition) => {
272
- const runErrorMessage =
273
- runError?.definitionId === definition.id ? runError.message : null;
274
- const isRunning = runningDefinitionId === definition.id;
279
+ <div className="flex flex-col md:hidden">
280
+ {definitions.map((definition) => {
281
+ const runErrorMessage =
282
+ runError?.definitionId === definition.id ? runError.message : null;
283
+ const isRunning = runningDefinitionId === definition.id;
275
284
 
276
- return (
277
- <div
278
- key={definition.id}
279
- className="flex flex-col gap-inline border-b border-border-neutral-base p-panel-compact last:border-b-0"
280
- >
281
- <button
282
- type="button"
283
- className="flex min-w-0 items-start gap-inline text-left"
284
- onClick={() => onOpenDefinition(definition)}
285
+ return (
286
+ <div
287
+ key={definition.id}
288
+ className="flex flex-col gap-inline border-b border-border-neutral-base p-panel-compact last:border-b-0"
285
289
  >
286
- <Icon
287
- name={sourceIcon(definition.source)}
288
- className="size-16 shrink-0 text-foreground-neutral-muted"
289
- aria-hidden="true"
290
- />
291
- <div className="flex min-w-0 flex-col gap-tight">
292
- <Text size="sm" bold className="break-words">
293
- {definition.name}
290
+ <button
291
+ type="button"
292
+ className="flex min-w-0 items-start gap-inline text-left"
293
+ onClick={() => onOpenDefinition(definition)}
294
+ >
295
+ <Icon
296
+ name={sourceIcon(definition.source)}
297
+ className="size-16 shrink-0 text-foreground-neutral-muted"
298
+ aria-hidden="true"
299
+ />
300
+ <div className="flex min-w-0 flex-col gap-tight">
301
+ <Text size="sm" bold className="break-words">
302
+ {definition.name}
303
+ </Text>
304
+ <Code className="break-words text-foreground-neutral-muted">
305
+ {definition.configPath ?? 'Manual definition'}
306
+ </Code>
307
+ </div>
308
+ </button>
309
+ <div className="flex items-center justify-between gap-inline">
310
+ <Text size="xs" className="text-foreground-neutral-muted">
311
+ Updated <RelativeTime value={definition.updatedAt} />
294
312
  </Text>
295
- <Code className="break-words text-foreground-neutral-muted">
296
- {definition.configPath ?? 'Manual definition'}
297
- </Code>
313
+ {definition.manualTrigger ? (
314
+ <Button size="sm" isLoading={isRunning} onClick={() => onRun(definition)}>
315
+ Run
316
+ </Button>
317
+ ) : null}
298
318
  </div>
299
- </button>
300
- <div className="flex items-center justify-between gap-inline">
301
- <Text size="xs" className="text-foreground-neutral-muted">
302
- Updated <RelativeTime value={definition.updatedAt} />
303
- </Text>
304
- {definition.manualTrigger ? (
305
- <Button size="sm" isLoading={isRunning} onClick={() => onRun(definition)}>
306
- Run
307
- </Button>
319
+ {runErrorMessage ? (
320
+ <Text size="xs" className="text-tag-error-text">
321
+ {runErrorMessage}
322
+ </Text>
308
323
  ) : null}
309
324
  </div>
310
- {runErrorMessage ? (
311
- <Text size="xs" className="text-tag-error-text">
312
- {runErrorMessage}
313
- </Text>
314
- ) : null}
315
- </div>
316
- );
317
- })}
318
- </div>
325
+ );
326
+ })}
327
+ </div>
328
+ </Panel>
319
329
 
320
330
  {isFetchNextPageError ? (
321
331
  <Callout role="alert" type="error">
@@ -355,7 +365,7 @@ function WorkflowEmptyState({sync}: {sync: DefinitionSyncSummary | null}) {
355
365
  ? 'No workflow definitions found.'
356
366
  : 'Workflow sync has not reported yet.';
357
367
 
358
- return <EmptyState icon="flowChart" title="No workflows" description={message} />;
368
+ return <EmptyState icon="flowChart" title="No workflows" description={message} variant="panel" />;
359
369
  }
360
370
 
361
371
  function WorkflowSyncAlert({sync}: {sync: DefinitionSyncSummary | null | undefined}) {
@@ -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>