@shipfox/client-workflows 16.0.0 → 18.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 (182) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +32 -0
  3. package/dist/components/identifier/identifier.js +2 -2
  4. package/dist/components/identifier/identifier.js.map +1 -1
  5. package/dist/components/job-detail/agent-config-failure-callout.d.ts.map +1 -1
  6. package/dist/components/job-detail/agent-config-failure-callout.js +1 -1
  7. package/dist/components/job-detail/agent-config-failure-callout.js.map +1 -1
  8. package/dist/components/job-detail/job-context-panel.d.ts +6 -0
  9. package/dist/components/job-detail/job-context-panel.d.ts.map +1 -0
  10. package/dist/components/job-detail/job-context-panel.js +228 -0
  11. package/dist/components/job-detail/job-context-panel.js.map +1 -0
  12. package/dist/components/job-detail/job-detail-header.d.ts +3 -1
  13. package/dist/components/job-detail/job-detail-header.d.ts.map +1 -1
  14. package/dist/components/job-detail/job-detail-header.js +54 -48
  15. package/dist/components/job-detail/job-detail-header.js.map +1 -1
  16. package/dist/components/job-detail/job-detail-view.d.ts +1 -1
  17. package/dist/components/job-detail/job-detail-view.d.ts.map +1 -1
  18. package/dist/components/job-detail/job-detail-view.js +100 -57
  19. package/dist/components/job-detail/job-detail-view.js.map +1 -1
  20. package/dist/components/job-detail/job-empty-states.d.ts +4 -0
  21. package/dist/components/job-detail/job-empty-states.d.ts.map +1 -1
  22. package/dist/components/job-detail/job-empty-states.js +60 -11
  23. package/dist/components/job-detail/job-empty-states.js.map +1 -1
  24. package/dist/components/job-detail/job-execution-switcher.js +4 -4
  25. package/dist/components/job-detail/job-execution-switcher.js.map +1 -1
  26. package/dist/components/job-detail/json-code.d.ts +14 -0
  27. package/dist/components/job-detail/json-code.d.ts.map +1 -0
  28. package/dist/components/job-detail/json-code.js +93 -0
  29. package/dist/components/job-detail/json-code.js.map +1 -0
  30. package/dist/components/job-detail/step-attempt-log-panel.d.ts.map +1 -1
  31. package/dist/components/job-detail/step-attempt-log-panel.js +9 -6
  32. package/dist/components/job-detail/step-attempt-log-panel.js.map +1 -1
  33. package/dist/components/job-detail/step-troubleshooting.d.ts +18 -0
  34. package/dist/components/job-detail/step-troubleshooting.d.ts.map +1 -0
  35. package/dist/components/job-detail/step-troubleshooting.js +497 -0
  36. package/dist/components/job-detail/step-troubleshooting.js.map +1 -0
  37. package/dist/components/job-graph/job-graph-content.d.ts.map +1 -1
  38. package/dist/components/job-graph/job-graph-content.js +5 -4
  39. package/dist/components/job-graph/job-graph-content.js.map +1 -1
  40. package/dist/components/job-graph/job-node.js +3 -3
  41. package/dist/components/job-graph/job-node.js.map +1 -1
  42. package/dist/components/step-list/index.d.ts +1 -0
  43. package/dist/components/step-list/index.d.ts.map +1 -1
  44. package/dist/components/step-list/index.js.map +1 -1
  45. package/dist/components/step-list/step-list-model.d.ts.map +1 -1
  46. package/dist/components/step-list/step-list-model.js +6 -0
  47. package/dist/components/step-list/step-list-model.js.map +1 -1
  48. package/dist/components/step-list/step-list.d.ts +5 -1
  49. package/dist/components/step-list/step-list.d.ts.map +1 -1
  50. package/dist/components/step-list/step-list.js +117 -82
  51. package/dist/components/step-list/step-list.js.map +1 -1
  52. package/dist/components/workflow-run-duration-label.js +1 -1
  53. package/dist/components/workflow-run-duration-label.js.map +1 -1
  54. package/dist/components/workflow-run-list/job-status-strip.js +6 -6
  55. package/dist/components/workflow-run-list/job-status-strip.js.map +1 -1
  56. package/dist/components/workflow-run-list/workflow-run-filter-menu.js +1 -1
  57. package/dist/components/workflow-run-list/workflow-run-filter-menu.js.map +1 -1
  58. package/dist/components/workflow-run-list/workflow-run-filters.js +4 -4
  59. package/dist/components/workflow-run-list/workflow-run-filters.js.map +1 -1
  60. package/dist/components/workflow-run-list/workflow-run-list-states.js +6 -6
  61. package/dist/components/workflow-run-list/workflow-run-list-states.js.map +1 -1
  62. package/dist/components/workflow-run-list/workflow-run-list-view.js +1 -1
  63. package/dist/components/workflow-run-list/workflow-run-list-view.js.map +1 -1
  64. package/dist/components/workflow-run-list/workflow-run-row.js +7 -7
  65. package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
  66. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js +1 -1
  67. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js.map +1 -1
  68. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts +0 -1
  69. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
  70. package/dist/components/workflow-run-summary/workflow-run-summary.js +8 -13
  71. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
  72. package/dist/components/workflow-run-tabs/index.d.ts +1 -1
  73. package/dist/components/workflow-run-tabs/index.d.ts.map +1 -1
  74. package/dist/components/workflow-run-tabs/index.js.map +1 -1
  75. package/dist/components/workflow-run-tabs/run-annotation-count-chip.js +2 -2
  76. package/dist/components/workflow-run-tabs/run-annotation-count-chip.js.map +1 -1
  77. package/dist/components/workflow-run-tabs/run-annotation-list.d.ts +9 -2
  78. package/dist/components/workflow-run-tabs/run-annotation-list.d.ts.map +1 -1
  79. package/dist/components/workflow-run-tabs/run-annotation-list.js +28 -19
  80. package/dist/components/workflow-run-tabs/run-annotation-list.js.map +1 -1
  81. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts.map +1 -1
  82. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js +5 -5
  83. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js.map +1 -1
  84. package/dist/components/workflow-run-view/run-workspace-nav.d.ts.map +1 -1
  85. package/dist/components/workflow-run-view/run-workspace-nav.js +11 -10
  86. package/dist/components/workflow-run-view/run-workspace-nav.js.map +1 -1
  87. package/dist/components/workflow-run-view/workflow-run-states.js +6 -6
  88. package/dist/components/workflow-run-view/workflow-run-states.js.map +1 -1
  89. package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
  90. package/dist/components/workflow-run-view/workflow-run-view.js +72 -12
  91. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
  92. package/dist/core/entities/job-execution.d.ts +9 -0
  93. package/dist/core/entities/job-execution.d.ts.map +1 -1
  94. package/dist/core/entities/job-execution.js.map +1 -1
  95. package/dist/core/entities/job.d.ts +10 -1
  96. package/dist/core/entities/job.d.ts.map +1 -1
  97. package/dist/core/entities/job.js.map +1 -1
  98. package/dist/core/entities/step-attempt.d.ts +29 -0
  99. package/dist/core/entities/step-attempt.d.ts.map +1 -1
  100. package/dist/core/entities/step-attempt.js.map +1 -1
  101. package/dist/core/entities/step.d.ts +3 -1
  102. package/dist/core/entities/step.d.ts.map +1 -1
  103. package/dist/core/entities/step.js.map +1 -1
  104. package/dist/core/run-annotation.d.ts +6 -0
  105. package/dist/core/run-annotation.d.ts.map +1 -1
  106. package/dist/core/run-annotation.js.map +1 -1
  107. package/dist/core/workflow-run.d.ts +1 -1
  108. package/dist/core/workflow-run.d.ts.map +1 -1
  109. package/dist/core/workflow-run.js.map +1 -1
  110. package/dist/hooks/api/annotations.d.ts +63 -0
  111. package/dist/hooks/api/annotations.d.ts.map +1 -0
  112. package/dist/hooks/api/annotations.js +145 -0
  113. package/dist/hooks/api/annotations.js.map +1 -0
  114. package/dist/hooks/api/step-attempt-detail.d.ts +14 -0
  115. package/dist/hooks/api/step-attempt-detail.d.ts.map +1 -0
  116. package/dist/hooks/api/step-attempt-detail.js +43 -0
  117. package/dist/hooks/api/step-attempt-detail.js.map +1 -0
  118. package/dist/hooks/api/workflow-run-mapper.d.ts +9 -2
  119. package/dist/hooks/api/workflow-run-mapper.d.ts.map +1 -1
  120. package/dist/hooks/api/workflow-run-mapper.js +48 -0
  121. package/dist/hooks/api/workflow-run-mapper.js.map +1 -1
  122. package/dist/pages/project-workflows-page.js +19 -19
  123. package/dist/pages/project-workflows-page.js.map +1 -1
  124. package/dist/pages/workflow-run-list-page.js +1 -1
  125. package/dist/pages/workflow-run-list-page.js.map +1 -1
  126. package/dist/tsconfig.test.tsbuildinfo +1 -1
  127. package/package.json +10 -10
  128. package/src/components/identifier/identifier.tsx +2 -2
  129. package/src/components/job-detail/agent-config-failure-callout.tsx +5 -1
  130. package/src/components/job-detail/job-context-panel.test.tsx +72 -0
  131. package/src/components/job-detail/job-context-panel.tsx +241 -0
  132. package/src/components/job-detail/job-detail-header.tsx +52 -46
  133. package/src/components/job-detail/job-detail-view.stories.tsx +555 -53
  134. package/src/components/job-detail/job-detail-view.tsx +105 -59
  135. package/src/components/job-detail/job-empty-states.test.ts +105 -1
  136. package/src/components/job-detail/job-empty-states.tsx +77 -10
  137. package/src/components/job-detail/job-execution-switcher.tsx +5 -5
  138. package/src/components/job-detail/json-code.tsx +120 -0
  139. package/src/components/job-detail/step-attempt-log-panel.test.tsx +1 -0
  140. package/src/components/job-detail/step-attempt-log-panel.tsx +17 -6
  141. package/src/components/job-detail/step-troubleshooting.stories.tsx +120 -0
  142. package/src/components/job-detail/step-troubleshooting.test.tsx +337 -0
  143. package/src/components/job-detail/step-troubleshooting.tsx +541 -0
  144. package/src/components/job-graph/job-graph-content.tsx +4 -3
  145. package/src/components/job-graph/job-node.tsx +3 -3
  146. package/src/components/step-list/index.ts +1 -0
  147. package/src/components/step-list/step-list-model.ts +6 -0
  148. package/src/components/step-list/step-list.test.tsx +75 -2
  149. package/src/components/step-list/step-list.tsx +73 -20
  150. package/src/components/workflow-run-duration-label.tsx +1 -1
  151. package/src/components/workflow-run-list/job-status-strip.tsx +6 -6
  152. package/src/components/workflow-run-list/workflow-run-filter-menu.tsx +1 -1
  153. package/src/components/workflow-run-list/workflow-run-filters.tsx +4 -4
  154. package/src/components/workflow-run-list/workflow-run-list-states.tsx +6 -6
  155. package/src/components/workflow-run-list/workflow-run-list-view.tsx +1 -1
  156. package/src/components/workflow-run-list/workflow-run-row.tsx +7 -7
  157. package/src/components/workflow-run-summary/workflow-run-attempt-switcher.tsx +1 -1
  158. package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +1 -1
  159. package/src/components/workflow-run-summary/workflow-run-summary.tsx +11 -11
  160. package/src/components/workflow-run-tabs/index.ts +5 -1
  161. package/src/components/workflow-run-tabs/run-annotation-count-chip.tsx +2 -2
  162. package/src/components/workflow-run-tabs/run-annotation-list.tsx +25 -10
  163. package/src/components/workflow-run-tabs/run-annotation-summary-line.tsx +5 -5
  164. package/src/components/workflow-run-view/run-workspace-nav.tsx +18 -11
  165. package/src/components/workflow-run-view/workflow-run-states.tsx +6 -6
  166. package/src/components/workflow-run-view/workflow-run-view.tsx +114 -14
  167. package/src/core/entities/job-execution.ts +9 -0
  168. package/src/core/entities/job.ts +12 -1
  169. package/src/core/entities/step-attempt.ts +33 -0
  170. package/src/core/entities/step.ts +3 -1
  171. package/src/core/run-annotation.ts +6 -0
  172. package/src/core/workflow-run.ts +4 -0
  173. package/src/hooks/api/annotations.test.ts +84 -0
  174. package/src/hooks/api/annotations.ts +240 -0
  175. package/src/hooks/api/step-attempt-detail.ts +67 -0
  176. package/src/hooks/api/workflow-run-mapper.ts +47 -0
  177. package/src/hooks/api/workflow-runs.test.tsx +61 -1
  178. package/src/pages/project-workflows-page.tsx +19 -19
  179. package/src/pages/workflow-job-detail-page.test.tsx +52 -1
  180. package/src/pages/workflow-run-list-page.tsx +1 -1
  181. package/test/fixtures/workflow-run.ts +7 -0
  182. package/tsconfig.build.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/client-workflows",
3
3
  "license": "MIT",
4
- "version": "16.0.0",
4
+ "version": "18.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -27,16 +27,16 @@
27
27
  "dependencies": {
28
28
  "@swc/helpers": "^0.5.17",
29
29
  "date-fns": "^4.1.0",
30
- "@shipfox/annotations-dto": "12.0.0",
31
- "@shipfox/api-definitions-dto": "12.2.0",
32
- "@shipfox/api-workflows-dto": "12.2.0",
33
- "@shipfox/client-logs": "16.0.0",
30
+ "@shipfox/annotations-dto": "12.3.0",
31
+ "@shipfox/api-definitions-dto": "12.3.0",
32
+ "@shipfox/api-workflows-dto": "12.5.0",
34
33
  "@shipfox/client-api": "6.0.1",
35
- "@shipfox/client-shell": "16.0.0",
36
- "@shipfox/react-ui": "1.1.0",
37
- "@shipfox/client-ui": "16.0.0",
38
- "@shipfox/client-triggers": "16.0.0",
39
- "@shipfox/client-projects": "16.0.0"
34
+ "@shipfox/client-logs": "17.0.0",
35
+ "@shipfox/client-projects": "17.0.0",
36
+ "@shipfox/client-shell": "17.0.0",
37
+ "@shipfox/client-triggers": "17.0.0",
38
+ "@shipfox/client-ui": "17.0.0",
39
+ "@shipfox/react-ui": "1.2.0"
40
40
  },
41
41
  "peerDependencies": {
42
42
  "@tanstack/react-query": "^5.101.0",
@@ -112,7 +112,7 @@ export function Identifier({
112
112
  onClick={() => {
113
113
  void handleCopy();
114
114
  }}
115
- className="inline-flex h-20 min-w-0 shrink-0 items-center gap-4 rounded-4 px-2 text-foreground-neutral-muted outline-none transition-colors hover:bg-background-components-hover hover:text-foreground-neutral-base focus-visible:shadow-border-interactive-with-active"
115
+ className="inline-flex h-20 min-w-0 shrink-0 items-center gap-tight rounded-4 px-[var(--pad-menu-surface)] text-foreground-neutral-muted outline-none transition-colors hover:bg-background-components-hover hover:text-foreground-neutral-base focus-visible:shadow-border-interactive-with-active"
116
116
  >
117
117
  <Icon name="copy" aria-hidden="true" className="size-12 shrink-0" />
118
118
  <Code as="span" variant="label" className="truncate">
@@ -167,7 +167,7 @@ function CopyFeedbackPortal({
167
167
  : `translate(-50%, ${COPY_FEEDBACK_OFFSET_PX}px)`,
168
168
  }}
169
169
  className={cn(
170
- 'pointer-events-none fixed z-50 whitespace-nowrap rounded-8 bg-background-components-base px-8 py-4 text-xs font-display font-medium leading-20 shadow-tooltip',
170
+ 'pointer-events-none fixed z-50 whitespace-nowrap rounded-8 bg-background-components-base px-tight py-[4px] text-xs font-display font-medium leading-20 shadow-tooltip',
171
171
  kind === 'failed' ? 'text-foreground-highlight-error' : 'text-foreground-neutral-base',
172
172
  )}
173
173
  >
@@ -21,7 +21,11 @@ export function AgentConfigFailureCallout({
21
21
  const copy = agentConfigFailureCopy(config, error);
22
22
 
23
23
  return (
24
- <Alert variant="warning" animated={false} className="px-10 py-8">
24
+ <Alert
25
+ variant="warning"
26
+ animated={false}
27
+ className="rounded-none border-x-0 border-t-0 border-b border-tag-warning-border bg-transparent px-0 py-row"
28
+ >
25
29
  <AlertContent>
26
30
  <AlertTitle>{copy.title}</AlertTitle>
27
31
  <AlertDescription>{copy.description}</AlertDescription>
@@ -0,0 +1,72 @@
1
+ import {render, screen, within} from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import {workflowJob, workflowJobExecutionDto} from '#test/fixtures/workflow-run.js';
4
+ import {JobContextPanel} from './job-context-panel.js';
5
+
6
+ describe('JobContextPanel', () => {
7
+ test('opens job details in a labelled sheet from the info action', async () => {
8
+ const user = userEvent.setup();
9
+ const job = workflowJob({
10
+ name: 'build',
11
+ runner: ['runner-linux-x64'],
12
+ job_executions: [
13
+ workflowJobExecutionDto({
14
+ status: 'succeeded',
15
+ outputs: {failed_tests: 4},
16
+ }),
17
+ ],
18
+ });
19
+ const execution = job.jobExecutions[0];
20
+ if (!execution) throw new Error('Test fixture is missing a job execution.');
21
+
22
+ render(<JobContextPanel job={job} execution={execution} />);
23
+
24
+ expect(screen.queryByRole('dialog', {name: 'build'})).not.toBeInTheDocument();
25
+ await user.click(screen.getByRole('button', {name: 'Inspect job details'}));
26
+
27
+ const dialog = await screen.findByRole('dialog', {name: 'build'});
28
+ expect(within(dialog).getByText('Runner')).toBeInTheDocument();
29
+ expect(within(dialog).getAllByText('Outputs')).not.toHaveLength(0);
30
+ });
31
+
32
+ test('separates execution-name traces from job condition traces', async () => {
33
+ const user = userEvent.setup();
34
+ const job = workflowJob({
35
+ name: 'build',
36
+ evaluation_trace: [
37
+ {
38
+ expression: 'inputs.environment == "production"',
39
+ roots: ['inputs.environment'],
40
+ fill_target: 'job-activation',
41
+ evaluated_at: '2026-08-05T12:00:00.000Z',
42
+ field: 'job.if',
43
+ value: 'true',
44
+ },
45
+ ],
46
+ job_executions: [
47
+ workflowJobExecutionDto({
48
+ status: 'succeeded',
49
+ evaluation_trace: [
50
+ {
51
+ expression: 'inputs.environment',
52
+ roots: ['inputs.environment'],
53
+ fill_target: 'execution-creation',
54
+ evaluated_at: '2026-08-05T12:00:00.000Z',
55
+ field: 'job.execution_name',
56
+ value: 'production',
57
+ },
58
+ ],
59
+ }),
60
+ ],
61
+ });
62
+ const execution = job.jobExecutions[0];
63
+ if (!execution) throw new Error('Test fixture is missing a job execution.');
64
+
65
+ render(<JobContextPanel job={job} execution={execution} />);
66
+ await user.click(screen.getByRole('button', {name: 'Inspect job details'}));
67
+
68
+ const dialog = await screen.findByRole('dialog', {name: 'build'});
69
+ expect(within(dialog).getByText('Execution name evaluation (1)')).toBeInTheDocument();
70
+ expect(within(dialog).getByText('Condition evaluation (1)')).toBeInTheDocument();
71
+ });
72
+ });
@@ -0,0 +1,241 @@
1
+ import {Icon} from '@shipfox/react-ui/icon';
2
+ import {
3
+ Sheet,
4
+ SheetBody,
5
+ SheetContent,
6
+ SheetDescription,
7
+ SheetHeader,
8
+ SheetTitle,
9
+ } from '@shipfox/react-ui/sheet';
10
+ import {Tooltip, TooltipContent, TooltipTrigger} from '@shipfox/react-ui/tooltip';
11
+ import {Code, Text} from '@shipfox/react-ui/typography';
12
+ import {useState} from 'react';
13
+ import type {EvaluationTraceEntry, Job, JobExecution} from '#core/workflow-run.js';
14
+ import {formatJobExecutionTime} from './job-execution-time-text.js';
15
+ import {JsonCode} from './json-code.js';
16
+ import {EvaluationTrace} from './step-troubleshooting.js';
17
+
18
+ export function JobContextPanel({job, execution}: {job: Job; execution: JobExecution}) {
19
+ const runner = execution.runner?.length ? execution.runner : job.runner;
20
+ const outputs = execution.outputs ?? job.outputs;
21
+ const trace = [...(job.evaluationTrace ?? []), ...(execution.evaluationTrace ?? [])];
22
+ const statusReason = execution.statusReason ?? job.statusReason;
23
+ const hasTiming = Boolean(execution.queueTime || execution.runTime);
24
+ const hasContext = Boolean(
25
+ runner?.length ||
26
+ outputs ||
27
+ execution.triggerEvents.length ||
28
+ job.success ||
29
+ statusReason ||
30
+ execution.statusReasonMessage ||
31
+ trace?.length ||
32
+ hasTiming,
33
+ );
34
+
35
+ if (!hasContext) return null;
36
+
37
+ return (
38
+ <JobContextSheet
39
+ job={job}
40
+ execution={execution}
41
+ runner={runner}
42
+ outputs={outputs}
43
+ trace={trace}
44
+ statusReason={statusReason}
45
+ />
46
+ );
47
+ }
48
+
49
+ function JobContextSheet({
50
+ job,
51
+ execution,
52
+ runner,
53
+ outputs,
54
+ trace,
55
+ statusReason,
56
+ }: {
57
+ job: Job;
58
+ execution: JobExecution;
59
+ runner: string[] | null;
60
+ outputs: Record<string, unknown> | null;
61
+ trace: EvaluationTraceEntry[];
62
+ statusReason: JobExecution['statusReason'];
63
+ }) {
64
+ const [open, setOpen] = useState(false);
65
+ const {conditionTrace, executionNameTrace} = splitJobEvaluationTrace(trace);
66
+
67
+ return (
68
+ <Sheet open={open} onOpenChange={setOpen}>
69
+ <Tooltip>
70
+ <TooltipTrigger asChild>
71
+ <button
72
+ type="button"
73
+ aria-label="Inspect job details"
74
+ onClick={() => setOpen(true)}
75
+ className="flex size-28 shrink-0 items-center justify-center rounded-4 bg-transparent text-foreground-neutral-muted outline-none transition-colors hover:bg-transparent hover:text-foreground-neutral-base active:bg-transparent focus-visible:shadow-button-neutral-focus"
76
+ >
77
+ <Icon name="informationLine" size={14} aria-hidden="true" />
78
+ </button>
79
+ </TooltipTrigger>
80
+ <TooltipContent>Inspect job details</TooltipContent>
81
+ </Tooltip>
82
+ <SheetContent side="right" className="w-full sm:max-w-[560px]">
83
+ <SheetHeader>
84
+ <SheetTitle>{job.displayName}</SheetTitle>
85
+ <SheetDescription>
86
+ Execution #{execution.sequence} · {execution.displayName}
87
+ </SheetDescription>
88
+ </SheetHeader>
89
+ <SheetBody className="gap-section">
90
+ <div className="grid w-full min-w-0 gap-group min-[640px]:grid-cols-2">
91
+ {runner?.length ? <ContextList label="Runner" values={runner} mono /> : null}
92
+ {execution.queueTime || execution.runTime ? (
93
+ <div className="flex min-w-0 flex-col gap-tight">
94
+ <Text size="xs" bold className="text-foreground-neutral-base">
95
+ Timing
96
+ </Text>
97
+ <div className="flex flex-wrap gap-inline text-xs text-foreground-neutral-muted">
98
+ {execution.queueTime ? (
99
+ <span>Queue {formatJobExecutionTime(execution.queueTime)}</span>
100
+ ) : null}
101
+ {execution.runTime ? (
102
+ <span>Run {formatJobExecutionTime(execution.runTime)}</span>
103
+ ) : null}
104
+ </div>
105
+ </div>
106
+ ) : null}
107
+ {outputs ? (
108
+ <JsonCode
109
+ title="Outputs"
110
+ value={outputs}
111
+ emptyMessage="No outputs declared; the `outputs:` mapping is empty."
112
+ />
113
+ ) : null}
114
+ {job.success ? <ContextValue label="Condition" value={job.success} mono /> : null}
115
+ {statusReason ? (
116
+ <ContextValue label="Status reason" value={humanize(statusReason)} />
117
+ ) : null}
118
+ {execution.statusReasonMessage ? (
119
+ <ContextValue label="Failure details" value={execution.statusReasonMessage} />
120
+ ) : null}
121
+ {execution.triggerEvents.length ? (
122
+ <JsonCode
123
+ title={`Trigger events (${execution.triggerEvents.length})`}
124
+ value={execution.triggerEvents}
125
+ />
126
+ ) : null}
127
+ {executionNameTrace.length ? (
128
+ <EvaluationTraceSection
129
+ title={`Execution name evaluation (${executionNameTrace.length})`}
130
+ trace={executionNameTrace}
131
+ />
132
+ ) : null}
133
+ {conditionTrace.length ? (
134
+ <EvaluationTraceSection
135
+ title={`Condition evaluation (${conditionTrace.length})`}
136
+ trace={conditionTrace}
137
+ />
138
+ ) : null}
139
+ </div>
140
+ </SheetBody>
141
+ </SheetContent>
142
+ </Sheet>
143
+ );
144
+ }
145
+
146
+ function EvaluationTraceSection({
147
+ title,
148
+ trace,
149
+ }: {
150
+ title: string;
151
+ trace: readonly EvaluationTraceEntry[];
152
+ }) {
153
+ return (
154
+ <div className="flex min-w-0 flex-col gap-tight">
155
+ <Text size="xs" bold className="text-foreground-neutral-base">
156
+ {title}
157
+ </Text>
158
+ <EvaluationTrace trace={trace} />
159
+ </div>
160
+ );
161
+ }
162
+
163
+ function ContextValue({
164
+ label,
165
+ value,
166
+ mono = false,
167
+ }: {
168
+ label: string;
169
+ value: string;
170
+ mono?: boolean;
171
+ }) {
172
+ return (
173
+ <div className="flex min-w-0 flex-col gap-tight">
174
+ <Text size="xs" bold className="text-foreground-neutral-base">
175
+ {label}
176
+ </Text>
177
+ <Text
178
+ size="xs"
179
+ className={
180
+ mono
181
+ ? 'break-words font-code text-foreground-neutral-muted'
182
+ : 'text-foreground-neutral-muted'
183
+ }
184
+ >
185
+ {value}
186
+ </Text>
187
+ </div>
188
+ );
189
+ }
190
+
191
+ function ContextList({
192
+ label,
193
+ values,
194
+ mono = false,
195
+ }: {
196
+ label: string;
197
+ values: string[];
198
+ mono?: boolean;
199
+ }) {
200
+ return (
201
+ <div className="flex min-w-0 flex-col gap-tight">
202
+ <Text size="xs" bold className="text-foreground-neutral-base">
203
+ {label}
204
+ </Text>
205
+ <div className="flex min-w-0 flex-wrap gap-tight">
206
+ {values.map((value) => (
207
+ <Code
208
+ key={value}
209
+ as="span"
210
+ variant="label"
211
+ className={mono ? 'text-foreground-neutral-muted' : 'text-foreground-neutral-base'}
212
+ >
213
+ {value}
214
+ </Code>
215
+ ))}
216
+ </div>
217
+ </div>
218
+ );
219
+ }
220
+
221
+ function humanize(value: string): string {
222
+ return value.replaceAll('_', ' ').replace(/\b\w/g, (letter) => letter.toUpperCase());
223
+ }
224
+
225
+ function splitJobEvaluationTrace(trace: readonly EvaluationTraceEntry[]): {
226
+ conditionTrace: EvaluationTraceEntry[];
227
+ executionNameTrace: EvaluationTraceEntry[];
228
+ } {
229
+ const conditionTrace: EvaluationTraceEntry[] = [];
230
+ const executionNameTrace: EvaluationTraceEntry[] = [];
231
+
232
+ for (const entry of trace) {
233
+ if (!('dropped' in entry) && entry.field === 'job.execution_name') {
234
+ executionNameTrace.push(entry);
235
+ } else {
236
+ conditionTrace.push(entry);
237
+ }
238
+ }
239
+
240
+ return {conditionTrace, executionNameTrace};
241
+ }
@@ -2,6 +2,7 @@ import {Icon} from '@shipfox/react-ui/icon';
2
2
  import {Tooltip, TooltipContent, TooltipTrigger} from '@shipfox/react-ui/tooltip';
3
3
  import {Code, Text} from '@shipfox/react-ui/typography';
4
4
  import {formatTimestamp} from '@shipfox/react-ui/utils';
5
+ import type {ReactNode} from 'react';
5
6
  import {getWorkflowStatusVisual} from '#components/workflow-status/status-visuals.js';
6
7
  import {WorkflowStatusIcon} from '#components/workflow-status/workflow-status-icon.js';
7
8
  import type {RunAnnotationSummary} from '#core/run-annotation.js';
@@ -26,6 +27,7 @@ export interface JobDetailHeaderProps {
26
27
  runAttempt?: number | undefined;
27
28
  /** Counts for this job only. Renders a link into the run's Annotations section, never a body. */
28
29
  annotationSummary?: RunAnnotationSummary | undefined;
30
+ jobContext?: ReactNode;
29
31
  }
30
32
 
31
33
  export function JobDetailHeader({
@@ -37,58 +39,62 @@ export function JobDetailHeader({
37
39
  workflowRunId,
38
40
  runAttempt,
39
41
  annotationSummary,
42
+ jobContext,
40
43
  }: JobDetailHeaderProps) {
41
44
  const selectedStatus = selectedExecutionStatus(job, selectedJobExecution);
42
45
  const jobStatus = getWorkflowStatusVisual(selectedStatus);
43
46
 
44
47
  return (
45
- <header className="border-b border-border-neutral-base px-16 py-12">
46
- <div className="flex min-w-0 flex-col gap-8">
47
- <div className="flex min-w-0 items-center gap-8">
48
- <WorkflowStatusIcon
49
- status={selectedStatus}
50
- size={14}
51
- tooltip
52
- ariaLabel={`Job status: ${jobStatus.label}`}
53
- />
54
- <Code
55
- as="h2"
56
- variant="paragraph"
57
- bold
58
- tabIndex={-1}
59
- className="min-w-0 truncate text-lg leading-24 text-foreground-neutral-base outline-none"
60
- data-job-heading
61
- >
62
- {job.displayName}
63
- </Code>
64
- </div>
65
-
66
- {selectedJobExecution || annotationSummary?.total ? (
67
- <div className="flex min-w-0 flex-wrap items-center gap-10 text-foreground-neutral-muted">
68
- {selectedJobExecution && job.executionCountVisible ? (
69
- <JobExecutionSwitcher
70
- job={job}
71
- selectedJobExecution={selectedJobExecution.id}
72
- onSelectedJobExecutionChange={onSelectedJobExecutionChange}
73
- variant="title"
74
- />
75
- ) : null}
76
- {selectedJobExecution ? (
77
- <>
78
- <JobDurationMeta execution={selectedJobExecution} kind="queue" />
79
- <JobDurationMeta execution={selectedJobExecution} kind="run" />
80
- </>
81
- ) : null}
82
- <RunAnnotationCountChip
83
- summary={annotationSummary}
84
- workspaceSlug={workspaceSlug}
85
- projectSlug={projectSlug}
86
- workflowRunId={workflowRunId}
87
- runAttempt={runAttempt}
88
- jobId={job.id}
48
+ <header className="border-b border-border-neutral-base px-row py-row">
49
+ <div className="flex min-w-0 items-start justify-between gap-cluster">
50
+ <div className="flex min-w-0 flex-col gap-inline">
51
+ <div className="flex min-w-0 items-center gap-inline">
52
+ <WorkflowStatusIcon
53
+ status={selectedStatus}
54
+ size={14}
55
+ tooltip
56
+ ariaLabel={`Job status: ${jobStatus.label}`}
89
57
  />
58
+ <Code
59
+ as="h1"
60
+ variant="paragraph"
61
+ bold
62
+ tabIndex={-1}
63
+ className="min-w-0 truncate text-lg leading-24 text-foreground-neutral-base outline-none"
64
+ data-job-heading
65
+ >
66
+ {job.displayName}
67
+ </Code>
90
68
  </div>
91
- ) : null}
69
+
70
+ {selectedJobExecution || annotationSummary?.total ? (
71
+ <div className="flex min-w-0 flex-wrap items-center gap-inline text-foreground-neutral-muted">
72
+ {selectedJobExecution && job.executionCountVisible ? (
73
+ <JobExecutionSwitcher
74
+ job={job}
75
+ selectedJobExecution={selectedJobExecution.id}
76
+ onSelectedJobExecutionChange={onSelectedJobExecutionChange}
77
+ variant="title"
78
+ />
79
+ ) : null}
80
+ {selectedJobExecution ? (
81
+ <>
82
+ <JobDurationMeta execution={selectedJobExecution} kind="queue" />
83
+ <JobDurationMeta execution={selectedJobExecution} kind="run" />
84
+ </>
85
+ ) : null}
86
+ <RunAnnotationCountChip
87
+ summary={annotationSummary}
88
+ workspaceSlug={workspaceSlug}
89
+ projectSlug={projectSlug}
90
+ workflowRunId={workflowRunId}
91
+ runAttempt={runAttempt}
92
+ jobId={job.id}
93
+ />
94
+ </div>
95
+ ) : null}
96
+ </div>
97
+ {jobContext}
92
98
  </div>
93
99
  </header>
94
100
  );
@@ -116,7 +122,7 @@ function JobDurationMeta({execution, kind}: {execution: JobExecution; kind: 'que
116
122
  return (
117
123
  <Tooltip>
118
124
  <TooltipTrigger asChild>
119
- <span className="inline-flex items-center gap-4 whitespace-nowrap font-code text-xs leading-20 tabular-nums">
125
+ <span className="inline-flex items-center gap-tight whitespace-nowrap font-code text-xs leading-20 tabular-nums">
120
126
  <Icon
121
127
  name={kind === 'queue' ? 'hourglassLine' : 'timerLine'}
122
128
  size={12}