@shipfox/client-workflows 21.0.0 → 22.0.1

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 (57) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +33 -0
  3. package/dist/components/job-detail/job-detail-view.js +1 -2
  4. package/dist/components/job-detail/job-detail-view.js.map +1 -1
  5. package/dist/components/job-detail/job-empty-states.js +2 -2
  6. package/dist/components/job-detail/job-empty-states.js.map +1 -1
  7. package/dist/components/job-detail/job-execution-time-text.d.ts.map +1 -1
  8. package/dist/components/job-detail/job-execution-time-text.js +2 -15
  9. package/dist/components/job-detail/job-execution-time-text.js.map +1 -1
  10. package/dist/components/job-graph/job-duration-format.js +1 -1
  11. package/dist/components/job-graph/job-duration-format.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.js +1 -2
  15. package/dist/components/step-list/step-list.js.map +1 -1
  16. package/dist/components/workflow-run-list/workflow-run-list-content.d.ts.map +1 -1
  17. package/dist/components/workflow-run-list/workflow-run-list-content.js +13 -7
  18. package/dist/components/workflow-run-list/workflow-run-list-content.js.map +1 -1
  19. package/dist/components/workflow-run-list/workflow-run-list-states.d.ts.map +1 -1
  20. package/dist/components/workflow-run-list/workflow-run-list-states.js +42 -37
  21. package/dist/components/workflow-run-list/workflow-run-list-states.js.map +1 -1
  22. package/dist/components/workflow-run-list/workflow-run-row.d.ts.map +1 -1
  23. package/dist/components/workflow-run-list/workflow-run-row.js +14 -10
  24. package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
  25. package/dist/components/workflow-run-summary/workflow-run-summary.js +3 -3
  26. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
  27. package/dist/components/workflow-run-view/run-workspace-nav.d.ts.map +1 -1
  28. package/dist/components/workflow-run-view/run-workspace-nav.js +20 -7
  29. package/dist/components/workflow-run-view/run-workspace-nav.js.map +1 -1
  30. package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
  31. package/dist/components/workflow-run-view/workflow-run-view.js +68 -50
  32. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
  33. package/dist/pages/project-workflows-page.js +181 -172
  34. package/dist/pages/project-workflows-page.js.map +1 -1
  35. package/dist/tsconfig.test.tsbuildinfo +1 -1
  36. package/package.json +8 -8
  37. package/src/components/job-detail/job-detail-view.tsx +1 -2
  38. package/src/components/job-detail/job-empty-states.tsx +2 -2
  39. package/src/components/job-detail/job-execution-time-text.test.ts +46 -0
  40. package/src/components/job-detail/job-execution-time-text.tsx +2 -23
  41. package/src/components/job-graph/job-duration-format.ts +1 -1
  42. package/src/components/job-graph/job-graph-content.tsx +2 -2
  43. package/src/components/step-list/step-list.tsx +1 -2
  44. package/src/components/workflow-run-list/workflow-run-list-content.tsx +7 -2
  45. package/src/components/workflow-run-list/workflow-run-list-states.tsx +41 -40
  46. package/src/components/workflow-run-list/workflow-run-row.tsx +11 -8
  47. package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +1 -1
  48. package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +9 -1
  49. package/src/components/workflow-run-summary/workflow-run-summary.tsx +3 -3
  50. package/src/components/workflow-run-view/run-workspace-nav.test.tsx +40 -2
  51. package/src/components/workflow-run-view/run-workspace-nav.tsx +28 -9
  52. package/src/components/workflow-run-view/workflow-run-view.stories.tsx +3 -1
  53. package/src/components/workflow-run-view/workflow-run-view.test.tsx +11 -8
  54. package/src/components/workflow-run-view/workflow-run-view.tsx +78 -70
  55. package/src/pages/project-workflows-page.test.tsx +14 -4
  56. package/src/pages/project-workflows-page.tsx +138 -132
  57. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,6 +1,7 @@
1
1
  import {ApiError} from '@shipfox/client-api';
2
2
  import {QueryLoadError} from '@shipfox/client-ui';
3
3
  import {EmptyState} from '@shipfox/react-ui/empty-state';
4
+ import {Panel, PanelBody, PanelHeader} from '@shipfox/react-ui/panel';
4
5
  import {RelativeTimeProvider} from '@shipfox/react-ui/relative-time';
5
6
  import {
6
7
  Select,
@@ -11,7 +12,6 @@ import {
11
12
  } from '@shipfox/react-ui/select';
12
13
  import {toast} from '@shipfox/react-ui/toast';
13
14
  import {Text} from '@shipfox/react-ui/typography';
14
- import {cn} from '@shipfox/react-ui/utils';
15
15
  import {useNavigate} from '@tanstack/react-router';
16
16
  import {type ReactNode, useEffect, useMemo} from 'react';
17
17
  import {buildRunAnnotationList, type RunAnnotationSummary} from '#core/run-annotation.js';
@@ -54,7 +54,6 @@ import {
54
54
  } from './workflow-run-states.js';
55
55
 
56
56
  type RunWorkspaceSection = Exclude<WorkflowRunTab, 'jobs'>;
57
- const RUN_WORKSPACE_FRAME_CLASS_NAME = 'mx-auto w-full max-w-[calc(240px_+_1120px)]';
58
57
 
59
58
  export interface WorkflowRunViewProps {
60
59
  projectId: string;
@@ -159,7 +158,6 @@ function RunViewContent({
159
158
  const activeSection = runWorkspaceSection(tab);
160
159
  const cancelMutation = useCancelWorkflowRunMutation(runData);
161
160
  const sourceSnapshot = runData?.sourceSnapshot ?? null;
162
- const hasJobContent = Boolean(jobContent);
163
161
  const resolvedSelection =
164
162
  runData && selection ? resolveWorkflowRunSelection({run: runData, selection}) : undefined;
165
163
  const highlightedLineRange = resolvedSelection?.step?.sourceLocation ?? null;
@@ -316,10 +314,7 @@ function RunViewContent({
316
314
  >
317
315
  <div
318
316
  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
- )}
317
+ className="flex min-h-0 min-w-0 w-full flex-1 flex-col min-[768px]:flex-row"
323
318
  >
324
319
  {runData && workspaceSlug && projectSlug ? (
325
320
  <RunWorkspaceNav
@@ -403,16 +398,20 @@ function RunSectionContent({
403
398
  aria-label="All jobs summary"
404
399
  className="min-h-0 flex-1 overflow-auto pb-panel pt-[16px]"
405
400
  >
406
- <div className="mx-auto flex w-full flex-col gap-group px-frame">
401
+ <div className="flex w-full flex-col gap-group">
407
402
  <Text as="h2" className="sr-only">
408
403
  All jobs summary
409
404
  </Text>
410
- <JobGraph
411
- run={run}
412
- selectedJobId={selectedJobId}
413
- onSelectedJobChange={onSelectGraphJob}
414
- className="min-h-160 overflow-hidden"
415
- />
405
+ <Panel className="min-h-160">
406
+ <PanelBody className="min-h-160 bg-background-components-base p-0">
407
+ <JobGraph
408
+ run={run}
409
+ selectedJobId={selectedJobId}
410
+ onSelectedJobChange={onSelectGraphJob}
411
+ className="min-h-160 overflow-hidden"
412
+ />
413
+ </PanelBody>
414
+ </Panel>
416
415
  </div>
417
416
  </section>
418
417
  );
@@ -439,28 +438,33 @@ function RunSectionContent({
439
438
  aria-label="Workflow source"
440
439
  className="min-h-0 flex-1 overflow-auto pb-panel pt-[16px]"
441
440
  >
442
- <div className="mx-auto flex min-h-full w-full flex-col px-frame">
441
+ <div className="flex min-h-full w-full flex-col">
443
442
  <Text as="h2" className="sr-only">
444
443
  Workflow source
445
444
  </Text>
446
- {sourceSnapshot ? (
447
- <WorkflowSourceContent
448
- source={sourceSnapshot}
449
- highlightedLineRange={highlightedLineRange}
450
- scrollHighlightedIntoView
451
- />
452
- ) : (
453
- <EmptyState
454
- className="min-h-160 w-full"
455
- icon="fileDamageLine"
456
- title="Source snapshot unavailable"
457
- description={
458
- run.isTemporary
459
- ? 'Temporary runs do not capture workflow source.'
460
- : 'This run was created before workflow source snapshots were captured.'
461
- }
462
- />
463
- )}
445
+ <Panel className="min-h-160 flex-1">
446
+ <PanelBody className="min-h-160 flex-1 p-0">
447
+ {sourceSnapshot ? (
448
+ <WorkflowSourceContent
449
+ source={sourceSnapshot}
450
+ highlightedLineRange={highlightedLineRange}
451
+ scrollHighlightedIntoView
452
+ />
453
+ ) : (
454
+ <EmptyState
455
+ variant="panel"
456
+ className="min-h-160"
457
+ icon="fileDamageLine"
458
+ title="Source snapshot unavailable"
459
+ description={
460
+ run.isTemporary
461
+ ? 'Temporary runs do not capture workflow source.'
462
+ : 'This run was created before workflow source snapshots were captured.'
463
+ }
464
+ />
465
+ )}
466
+ </PanelBody>
467
+ </Panel>
464
468
  </div>
465
469
  </section>
466
470
  );
@@ -534,45 +538,49 @@ function RunAnnotationsSection({
534
538
  aria-label="Run annotations"
535
539
  className="min-h-0 flex-1 overflow-auto pb-panel pt-[16px]"
536
540
  >
537
- <div className="mx-auto flex w-full flex-col gap-group px-frame">
541
+ <div className="flex w-full flex-col">
538
542
  <Text as="h2" className="sr-only">
539
543
  Annotations
540
544
  </Text>
541
- <div className="flex flex-wrap items-center justify-between gap-inline">
542
- <RunAnnotationSummaryLine
543
- summary={annotationSummary}
544
- workspaceSlug={workspaceSlug}
545
- projectSlug={projectSlug}
546
- workflowRunId={run.id}
547
- search={selection}
548
- />
549
- <AnnotationJobFilter
550
- jobs={run.jobs}
551
- selectedJobId={selectedJob?.id}
552
- onSelect={onSelectAnnotationJob}
553
- />
554
- </div>
555
- <RunAnnotationList
556
- // Remounting on a filter or run-attempt change resets the render window, so the next
557
- // list never inherits a "show more" position from different data.
558
- key={`${run.id}:${run.runAttempt.attempt}:${severity ?? 'all'}:${selectedJob?.id ?? 'all'}`}
559
- query={annotations.query}
560
- entries={entries}
561
- derivedAnnotations={derivedAnnotations}
562
- workspaceSlug={workspaceSlug}
563
- projectSlug={projectSlug}
564
- workflowRunId={run.id}
565
- runAttempt={run.runAttempt.attempt}
566
- // A run with no annotations at all offers no filter to clear, whatever the URL says.
567
- filtered={Boolean(
568
- (severity || selectedJob || selection?.annotation) &&
569
- ((annotationSummary?.total ?? 0) > 0 || hasSynthesizedJobAnnotations),
570
- )}
571
- filteredJobName={selectedJob?.displayName}
572
- filteredSeverity={severity}
573
- onClearFilters={onClearAnnotationFilters}
574
- selectedAnnotationId={selection?.annotation}
575
- />
545
+ <Panel>
546
+ <PanelHeader className="flex-wrap">
547
+ <RunAnnotationSummaryLine
548
+ summary={annotationSummary}
549
+ workspaceSlug={workspaceSlug}
550
+ projectSlug={projectSlug}
551
+ workflowRunId={run.id}
552
+ search={selection}
553
+ />
554
+ <AnnotationJobFilter
555
+ jobs={run.jobs}
556
+ selectedJobId={selectedJob?.id}
557
+ onSelect={onSelectAnnotationJob}
558
+ />
559
+ </PanelHeader>
560
+ <PanelBody className="gap-group p-panel">
561
+ <RunAnnotationList
562
+ // Remounting on a filter or run-attempt change resets the render window, so the next
563
+ // list never inherits a "show more" position from different data.
564
+ key={`${run.id}:${run.runAttempt.attempt}:${severity ?? 'all'}:${selectedJob?.id ?? 'all'}`}
565
+ query={annotations.query}
566
+ entries={entries}
567
+ derivedAnnotations={derivedAnnotations}
568
+ workspaceSlug={workspaceSlug}
569
+ projectSlug={projectSlug}
570
+ workflowRunId={run.id}
571
+ runAttempt={run.runAttempt.attempt}
572
+ // A run with no annotations at all offers no filter to clear, whatever the URL says.
573
+ filtered={Boolean(
574
+ (severity || selectedJob || selection?.annotation) &&
575
+ ((annotationSummary?.total ?? 0) > 0 || hasSynthesizedJobAnnotations),
576
+ )}
577
+ filteredJobName={selectedJob?.displayName}
578
+ filteredSeverity={severity}
579
+ onClearFilters={onClearAnnotationFilters}
580
+ selectedAnnotationId={selection?.annotation}
581
+ />
582
+ </PanelBody>
583
+ </Panel>
576
584
  </div>
577
585
  </section>
578
586
  );
@@ -674,7 +682,7 @@ function RunWorkspaceNavSkeleton() {
674
682
  return (
675
683
  <aside
676
684
  aria-label="Loading run navigation"
677
- className="hidden w-240 shrink-0 border-r border-border-neutral-base p-panel-compact min-[768px]:block"
685
+ className="hidden w-240 shrink-0 p-panel-compact min-[768px]:block"
678
686
  >
679
687
  <div className="flex flex-col gap-group">
680
688
  <div className="h-32 rounded-4 bg-background-components-subtle" />
@@ -12,15 +12,25 @@ const PROJECT_ID = '44444444-4444-4444-8444-444444444444';
12
12
  const CONNECTION_ID = '33333333-3333-4333-8333-333333333333';
13
13
 
14
14
  describe('ProjectWorkflowsPage', () => {
15
- test('renders workflow definitions and the source strip', async () => {
15
+ test('renders workflow definitions and their panel regions', async () => {
16
16
  configureApiClient({fetchImpl: createProjectDetailFetch()});
17
17
 
18
18
  renderWorkflowsPage();
19
19
 
20
- expect(await screen.findByRole('heading', {name: 'Workflows'})).toBeInTheDocument();
21
- expect(screen.getAllByText('Deploy production')[0]).toBeInTheDocument();
20
+ expect((await screen.findAllByText('Deploy production'))[0]).toBeInTheDocument();
21
+ expect(screen.getByRole('heading', {name: 'Workflows'})).toHaveClass('sr-only');
22
+ expect(
23
+ screen.queryByText('Synced workflow definitions for this project source.'),
24
+ ).not.toBeInTheDocument();
22
25
  expect(screen.getAllByText('.shipfox/workflows/deploy.yml')[0]).toBeInTheDocument();
23
- expect(screen.getByRole('region', {name: 'Project source'})).toBeInTheDocument();
26
+ const sourcePanel = screen
27
+ .getByRole('region', {name: 'Project source'})
28
+ .closest('[data-slot="panel"]');
29
+ const definitionsPanel = screen.getByRole('region', {name: 'Workflow definitions'});
30
+ expect(sourcePanel).toBeInTheDocument();
31
+ expect(definitionsPanel).toBeInTheDocument();
32
+ expect(sourcePanel).not.toBe(definitionsPanel);
33
+ expect(screen.getByRole('table').closest('[data-slot="panel"]')).toBe(definitionsPanel);
24
34
  // Source strip resolves connection display_name from the integrations
25
35
  // workspace cache; external_repository_id renders as a Code chip.
26
36
  expect(await screen.findByText('Acme GitHub')).toBeInTheDocument();
@@ -68,6 +68,10 @@ function ProjectWorkflowsPageInner({projectId}: {projectId: string}) {
68
68
 
69
69
  return (
70
70
  <div className="flex w-full flex-col gap-section">
71
+ <Header variant="h1" className="sr-only">
72
+ Workflows
73
+ </Header>
74
+
71
75
  {projectQuery.isPending ? (
72
76
  <div className="flex flex-col gap-cluster">
73
77
  <Skeleton className="h-28 w-1/3" />
@@ -89,13 +93,6 @@ function ProjectWorkflowsPageInner({projectId}: {projectId: string}) {
89
93
 
90
94
  {projectQuery.data ? (
91
95
  <>
92
- <header className="flex flex-col gap-tight">
93
- <Header variant="h2">Workflows</Header>
94
- <Text size="sm" className="text-foreground-neutral-muted">
95
- Synced workflow definitions for this project source.
96
- </Text>
97
- </header>
98
-
99
96
  <SourceStrip
100
97
  connectionId={projectQuery.data.source.connectionId}
101
98
  externalRepositoryId={projectQuery.data.source.externalRepositoryId}
@@ -171,152 +168,161 @@ function WorkflowDefinitionsList({
171
168
  }) {
172
169
  if (isPending) {
173
170
  return (
174
- <div className="flex flex-col gap-inline">
175
- <Skeleton className="h-40 w-full" />
176
- <Skeleton className="h-40 w-full" />
177
- <Skeleton className="h-40 w-full" />
178
- </div>
171
+ <Panel role="status" aria-label="Loading workflows" className="divide-y">
172
+ <Skeleton className="h-44 w-full rounded-none" />
173
+ <Skeleton className="h-44 w-full rounded-none" />
174
+ <Skeleton className="h-44 w-full rounded-none" />
175
+ </Panel>
179
176
  );
180
177
  }
181
178
 
182
179
  if (isError && definitions.length === 0) {
183
180
  return (
184
- <LoadErrorState
185
- title="Couldn't load workflows"
186
- description="Definitions could not be loaded. Source metadata remains visible."
187
- onRetry={onRetry}
188
- retryLabel="Retry loading workflows"
189
- />
181
+ <Panel role="region" aria-label="Workflow definitions">
182
+ <LoadErrorState
183
+ title="Couldn't load workflows"
184
+ description="Definitions could not be loaded. Source metadata remains visible."
185
+ onRetry={onRetry}
186
+ retryLabel="Retry loading workflows"
187
+ variant="panel"
188
+ />
189
+ </Panel>
190
190
  );
191
191
  }
192
192
 
193
193
  if (definitions.length === 0) {
194
- return <WorkflowEmptyState sync={sync} />;
194
+ return (
195
+ <Panel role="region" aria-label="Workflow definitions">
196
+ <WorkflowEmptyState sync={sync} />
197
+ </Panel>
198
+ );
195
199
  }
196
200
 
197
201
  return (
198
202
  <>
199
- <Panel className="hidden md:flex">
200
- <Table>
201
- <TableHeader>
202
- <TableRow>
203
- <TableHead className="w-40"></TableHead>
204
- <TableHead>Workflow</TableHead>
205
- <TableHead className="w-180">Updated</TableHead>
206
- <TableHead className="w-80 text-right"></TableHead>
207
- </TableRow>
208
- </TableHeader>
209
- <TableBody>
210
- {definitions.map((definition) => {
211
- const runErrorMessage =
212
- runError?.definitionId === definition.id ? runError.message : null;
213
- const isRunning = runningDefinitionId === definition.id;
203
+ <Panel role="region" aria-label="Workflow definitions">
204
+ <div className="hidden md:block">
205
+ <Table>
206
+ <TableHeader>
207
+ <TableRow>
208
+ <TableHead className="w-40"></TableHead>
209
+ <TableHead>Workflow</TableHead>
210
+ <TableHead className="w-180">Updated</TableHead>
211
+ <TableHead className="w-80 text-right"></TableHead>
212
+ </TableRow>
213
+ </TableHeader>
214
+ <TableBody>
215
+ {definitions.map((definition) => {
216
+ const runErrorMessage =
217
+ runError?.definitionId === definition.id ? runError.message : null;
218
+ const isRunning = runningDefinitionId === definition.id;
214
219
 
215
- return (
216
- // The workflow-name cell holds a `<button>` so the row is
217
- // keyboard-reachable (Tab focuses, Enter/Space activates
218
- // via native button semantics). The TableRow itself is no
219
- // longer clickable: a row-level onClick would be invisible
220
- // to keyboard users and require custom keydown handling.
221
- // The `group` class on the row still drives the Run button
222
- // reveal on hover or focus-within.
223
- <TableRow key={definition.id} className="group">
224
- <TableCell>
225
- <Icon
226
- name={sourceIcon(definition.source)}
227
- className="size-16 text-foreground-neutral-muted"
228
- aria-hidden="true"
229
- />
230
- </TableCell>
231
- <TableCell className="max-w-260">
232
- <div className="flex min-w-0 flex-col gap-tight">
233
- <button
234
- type="button"
235
- onClick={() => onOpenDefinition(definition)}
236
- className="flex min-w-0 flex-col gap-tight text-left outline-none focus-visible:shadow-border-interactive-with-active rounded-4"
237
- >
238
- <Text size="sm" bold className="truncate">
239
- {definition.name}
240
- </Text>
241
- <Code className="truncate text-foreground-neutral-muted">
242
- {definition.configPath ?? 'Manual definition'}
243
- </Code>
244
- </button>
245
- {runErrorMessage ? (
246
- <Text size="xs" className="text-tag-error-text">
247
- {runErrorMessage}
248
- </Text>
249
- ) : null}
250
- </div>
251
- </TableCell>
252
- <TableCell className="text-foreground-neutral-muted">
253
- <RelativeTime value={definition.updatedAt} />
254
- </TableCell>
255
- <TableCell>
256
- {definition.manualTrigger ? (
257
- <div className="flex justify-end opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100">
258
- <Button size="xs" isLoading={isRunning} onClick={() => onRun(definition)}>
259
- Run
260
- </Button>
220
+ return (
221
+ // The workflow-name cell holds a `<button>` so the row is
222
+ // keyboard-reachable (Tab focuses, Enter/Space activates
223
+ // via native button semantics). The TableRow itself is no
224
+ // longer clickable: a row-level onClick would be invisible
225
+ // to keyboard users and require custom keydown handling.
226
+ // The `group` class on the row still drives the Run button
227
+ // reveal on hover or focus-within.
228
+ <TableRow key={definition.id} className="group">
229
+ <TableCell>
230
+ <Icon
231
+ name={sourceIcon(definition.source)}
232
+ className="size-16 text-foreground-neutral-muted"
233
+ aria-hidden="true"
234
+ />
235
+ </TableCell>
236
+ <TableCell className="max-w-260">
237
+ <div className="flex min-w-0 flex-col gap-tight">
238
+ <button
239
+ type="button"
240
+ onClick={() => onOpenDefinition(definition)}
241
+ className="flex min-w-0 flex-col gap-tight rounded-4 text-left outline-none focus-visible:shadow-border-interactive-with-active"
242
+ >
243
+ <Text size="sm" bold className="truncate">
244
+ {definition.name}
245
+ </Text>
246
+ <Code className="truncate text-foreground-neutral-muted">
247
+ {definition.configPath ?? 'Manual definition'}
248
+ </Code>
249
+ </button>
250
+ {runErrorMessage ? (
251
+ <Text size="xs" className="text-tag-error-text">
252
+ {runErrorMessage}
253
+ </Text>
254
+ ) : null}
261
255
  </div>
262
- ) : null}
263
- </TableCell>
264
- </TableRow>
265
- );
266
- })}
267
- </TableBody>
268
- </Table>
269
- </Panel>
256
+ </TableCell>
257
+ <TableCell className="text-foreground-neutral-muted">
258
+ <RelativeTime value={definition.updatedAt} />
259
+ </TableCell>
260
+ <TableCell>
261
+ {definition.manualTrigger ? (
262
+ <div className="flex justify-end opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100">
263
+ <Button size="xs" isLoading={isRunning} onClick={() => onRun(definition)}>
264
+ Run
265
+ </Button>
266
+ </div>
267
+ ) : null}
268
+ </TableCell>
269
+ </TableRow>
270
+ );
271
+ })}
272
+ </TableBody>
273
+ </Table>
274
+ </div>
270
275
 
271
- <div className="flex flex-col rounded-8 border border-border-neutral-base md:hidden">
272
- {definitions.map((definition) => {
273
- const runErrorMessage =
274
- runError?.definitionId === definition.id ? runError.message : null;
275
- const isRunning = runningDefinitionId === definition.id;
276
+ <div className="flex flex-col md:hidden">
277
+ {definitions.map((definition) => {
278
+ const runErrorMessage =
279
+ runError?.definitionId === definition.id ? runError.message : null;
280
+ const isRunning = runningDefinitionId === definition.id;
276
281
 
277
- return (
278
- <div
279
- key={definition.id}
280
- className="flex flex-col gap-inline border-b border-border-neutral-base p-panel-compact last:border-b-0"
281
- >
282
- <button
283
- type="button"
284
- className="flex min-w-0 items-start gap-inline text-left"
285
- onClick={() => onOpenDefinition(definition)}
282
+ return (
283
+ <div
284
+ key={definition.id}
285
+ className="flex flex-col gap-inline border-b border-border-neutral-base p-panel-compact last:border-b-0"
286
286
  >
287
- <Icon
288
- name={sourceIcon(definition.source)}
289
- className="size-16 shrink-0 text-foreground-neutral-muted"
290
- aria-hidden="true"
291
- />
292
- <div className="flex min-w-0 flex-col gap-tight">
293
- <Text size="sm" bold className="break-words">
294
- {definition.name}
287
+ <button
288
+ type="button"
289
+ className="flex min-w-0 items-start gap-inline text-left"
290
+ onClick={() => onOpenDefinition(definition)}
291
+ >
292
+ <Icon
293
+ name={sourceIcon(definition.source)}
294
+ className="size-16 shrink-0 text-foreground-neutral-muted"
295
+ aria-hidden="true"
296
+ />
297
+ <div className="flex min-w-0 flex-col gap-tight">
298
+ <Text size="sm" bold className="break-words">
299
+ {definition.name}
300
+ </Text>
301
+ <Code className="break-words text-foreground-neutral-muted">
302
+ {definition.configPath ?? 'Manual definition'}
303
+ </Code>
304
+ </div>
305
+ </button>
306
+ <div className="flex items-center justify-between gap-inline">
307
+ <Text size="xs" className="text-foreground-neutral-muted">
308
+ Updated <RelativeTime value={definition.updatedAt} />
295
309
  </Text>
296
- <Code className="break-words text-foreground-neutral-muted">
297
- {definition.configPath ?? 'Manual definition'}
298
- </Code>
310
+ {definition.manualTrigger ? (
311
+ <Button size="sm" isLoading={isRunning} onClick={() => onRun(definition)}>
312
+ Run
313
+ </Button>
314
+ ) : null}
299
315
  </div>
300
- </button>
301
- <div className="flex items-center justify-between gap-inline">
302
- <Text size="xs" className="text-foreground-neutral-muted">
303
- Updated <RelativeTime value={definition.updatedAt} />
304
- </Text>
305
- {definition.manualTrigger ? (
306
- <Button size="sm" isLoading={isRunning} onClick={() => onRun(definition)}>
307
- Run
308
- </Button>
316
+ {runErrorMessage ? (
317
+ <Text size="xs" className="text-tag-error-text">
318
+ {runErrorMessage}
319
+ </Text>
309
320
  ) : null}
310
321
  </div>
311
- {runErrorMessage ? (
312
- <Text size="xs" className="text-tag-error-text">
313
- {runErrorMessage}
314
- </Text>
315
- ) : null}
316
- </div>
317
- );
318
- })}
319
- </div>
322
+ );
323
+ })}
324
+ </div>
325
+ </Panel>
320
326
 
321
327
  {isFetchNextPageError ? (
322
328
  <Callout role="alert" type="error">
@@ -356,7 +362,7 @@ function WorkflowEmptyState({sync}: {sync: DefinitionSyncSummary | null}) {
356
362
  ? 'No workflow definitions found.'
357
363
  : 'Workflow sync has not reported yet.';
358
364
 
359
- return <EmptyState icon="flowChart" title="No workflows" description={message} />;
365
+ return <EmptyState icon="flowChart" title="No workflows" description={message} variant="panel" />;
360
366
  }
361
367
 
362
368
  function WorkflowSyncAlert({sync}: {sync: DefinitionSyncSummary | null | undefined}) {