@shipfox/client-workflows 15.0.0 → 17.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 (225) hide show
  1. package/.storybook/preview.css +4 -0
  2. package/.turbo/turbo-build.log +1 -1
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +41 -0
  5. package/dist/components/identifier/identifier.js +2 -2
  6. package/dist/components/identifier/identifier.js.map +1 -1
  7. package/dist/components/job-detail/agent-config-failure-callout.d.ts.map +1 -1
  8. package/dist/components/job-detail/agent-config-failure-callout.js +1 -1
  9. package/dist/components/job-detail/agent-config-failure-callout.js.map +1 -1
  10. package/dist/components/job-detail/job-context-panel.d.ts +6 -0
  11. package/dist/components/job-detail/job-context-panel.d.ts.map +1 -0
  12. package/dist/components/job-detail/job-context-panel.js +224 -0
  13. package/dist/components/job-detail/job-context-panel.js.map +1 -0
  14. package/dist/components/job-detail/job-detail-header.d.ts +10 -1
  15. package/dist/components/job-detail/job-detail-header.d.ts.map +1 -1
  16. package/dist/components/job-detail/job-detail-header.js +58 -39
  17. package/dist/components/job-detail/job-detail-header.js.map +1 -1
  18. package/dist/components/job-detail/job-detail-view.d.ts +1 -1
  19. package/dist/components/job-detail/job-detail-view.d.ts.map +1 -1
  20. package/dist/components/job-detail/job-detail-view.js +92 -41
  21. package/dist/components/job-detail/job-detail-view.js.map +1 -1
  22. package/dist/components/job-detail/job-empty-states.d.ts.map +1 -1
  23. package/dist/components/job-detail/job-empty-states.js +19 -10
  24. package/dist/components/job-detail/job-empty-states.js.map +1 -1
  25. package/dist/components/job-detail/job-execution-switcher.js +4 -4
  26. package/dist/components/job-detail/job-execution-switcher.js.map +1 -1
  27. package/dist/components/job-detail/json-code.d.ts +14 -0
  28. package/dist/components/job-detail/json-code.d.ts.map +1 -0
  29. package/dist/components/job-detail/json-code.js +93 -0
  30. package/dist/components/job-detail/json-code.js.map +1 -0
  31. package/dist/components/job-detail/step-attempt-log-panel.d.ts.map +1 -1
  32. package/dist/components/job-detail/step-attempt-log-panel.js +9 -6
  33. package/dist/components/job-detail/step-attempt-log-panel.js.map +1 -1
  34. package/dist/components/job-detail/step-troubleshooting.d.ts +18 -0
  35. package/dist/components/job-detail/step-troubleshooting.d.ts.map +1 -0
  36. package/dist/components/job-detail/step-troubleshooting.js +493 -0
  37. package/dist/components/job-detail/step-troubleshooting.js.map +1 -0
  38. package/dist/components/job-graph/job-graph-content.d.ts.map +1 -1
  39. package/dist/components/job-graph/job-graph-content.js +5 -4
  40. package/dist/components/job-graph/job-graph-content.js.map +1 -1
  41. package/dist/components/job-graph/job-node.js +3 -3
  42. package/dist/components/job-graph/job-node.js.map +1 -1
  43. package/dist/components/step-list/index.d.ts +1 -0
  44. package/dist/components/step-list/index.d.ts.map +1 -1
  45. package/dist/components/step-list/index.js.map +1 -1
  46. package/dist/components/step-list/step-list-model.d.ts.map +1 -1
  47. package/dist/components/step-list/step-list-model.js +5 -0
  48. package/dist/components/step-list/step-list-model.js.map +1 -1
  49. package/dist/components/step-list/step-list.d.ts +5 -1
  50. package/dist/components/step-list/step-list.d.ts.map +1 -1
  51. package/dist/components/step-list/step-list.js +117 -82
  52. package/dist/components/step-list/step-list.js.map +1 -1
  53. package/dist/components/workflow-run-duration-label.js +1 -1
  54. package/dist/components/workflow-run-duration-label.js.map +1 -1
  55. package/dist/components/workflow-run-list/job-status-strip.js +6 -6
  56. package/dist/components/workflow-run-list/job-status-strip.js.map +1 -1
  57. package/dist/components/workflow-run-list/workflow-run-filter-menu.js +1 -1
  58. package/dist/components/workflow-run-list/workflow-run-filter-menu.js.map +1 -1
  59. package/dist/components/workflow-run-list/workflow-run-filters.js +4 -4
  60. package/dist/components/workflow-run-list/workflow-run-filters.js.map +1 -1
  61. package/dist/components/workflow-run-list/workflow-run-list-states.js +6 -6
  62. package/dist/components/workflow-run-list/workflow-run-list-states.js.map +1 -1
  63. package/dist/components/workflow-run-list/workflow-run-list-view.js +1 -1
  64. package/dist/components/workflow-run-list/workflow-run-list-view.js.map +1 -1
  65. package/dist/components/workflow-run-list/workflow-run-row.js +7 -7
  66. package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
  67. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js +1 -1
  68. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js.map +1 -1
  69. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts +0 -1
  70. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
  71. package/dist/components/workflow-run-summary/workflow-run-summary.js +9 -13
  72. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
  73. package/dist/components/workflow-run-tabs/index.d.ts +3 -0
  74. package/dist/components/workflow-run-tabs/index.d.ts.map +1 -1
  75. package/dist/components/workflow-run-tabs/index.js +3 -0
  76. package/dist/components/workflow-run-tabs/index.js.map +1 -1
  77. package/dist/components/workflow-run-tabs/run-annotation-count-chip.d.ts +16 -0
  78. package/dist/components/workflow-run-tabs/run-annotation-count-chip.d.ts.map +1 -0
  79. package/dist/components/workflow-run-tabs/run-annotation-count-chip.js +50 -0
  80. package/dist/components/workflow-run-tabs/run-annotation-count-chip.js.map +1 -0
  81. package/dist/components/workflow-run-tabs/run-annotation-item.d.ts +20 -0
  82. package/dist/components/workflow-run-tabs/run-annotation-item.d.ts.map +1 -0
  83. package/dist/components/workflow-run-tabs/run-annotation-item.js +102 -0
  84. package/dist/components/workflow-run-tabs/run-annotation-item.js.map +1 -0
  85. package/dist/components/workflow-run-tabs/run-annotation-list.d.ts +39 -0
  86. package/dist/components/workflow-run-tabs/run-annotation-list.d.ts.map +1 -0
  87. package/dist/components/workflow-run-tabs/run-annotation-list.js +211 -0
  88. package/dist/components/workflow-run-tabs/run-annotation-list.js.map +1 -0
  89. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts +1 -1
  90. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts.map +1 -1
  91. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js +90 -25
  92. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js.map +1 -1
  93. package/dist/components/workflow-run-tabs/severity-visuals.d.ts +20 -0
  94. package/dist/components/workflow-run-tabs/severity-visuals.d.ts.map +1 -0
  95. package/dist/components/workflow-run-tabs/severity-visuals.js +31 -0
  96. package/dist/components/workflow-run-tabs/severity-visuals.js.map +1 -0
  97. package/dist/components/workflow-run-view/run-workspace-nav.d.ts +1 -1
  98. package/dist/components/workflow-run-view/run-workspace-nav.d.ts.map +1 -1
  99. package/dist/components/workflow-run-view/run-workspace-nav.js +16 -10
  100. package/dist/components/workflow-run-view/run-workspace-nav.js.map +1 -1
  101. package/dist/components/workflow-run-view/workflow-run-states.js +6 -6
  102. package/dist/components/workflow-run-view/workflow-run-states.js.map +1 -1
  103. package/dist/components/workflow-run-view/workflow-run-view.d.ts +1 -3
  104. package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
  105. package/dist/components/workflow-run-view/workflow-run-view.js +217 -59
  106. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
  107. package/dist/core/entities/job-execution.d.ts +7 -0
  108. package/dist/core/entities/job-execution.d.ts.map +1 -1
  109. package/dist/core/entities/job-execution.js.map +1 -1
  110. package/dist/core/entities/job.d.ts +9 -0
  111. package/dist/core/entities/job.d.ts.map +1 -1
  112. package/dist/core/entities/job.js.map +1 -1
  113. package/dist/core/entities/step-attempt.d.ts +29 -0
  114. package/dist/core/entities/step-attempt.d.ts.map +1 -1
  115. package/dist/core/entities/step-attempt.js.map +1 -1
  116. package/dist/core/entities/step.d.ts +3 -1
  117. package/dist/core/entities/step.d.ts.map +1 -1
  118. package/dist/core/entities/step.js.map +1 -1
  119. package/dist/core/run-annotation.d.ts +79 -0
  120. package/dist/core/run-annotation.d.ts.map +1 -0
  121. package/dist/core/run-annotation.js +125 -0
  122. package/dist/core/run-annotation.js.map +1 -0
  123. package/dist/core/workflow-run.d.ts +1 -1
  124. package/dist/core/workflow-run.d.ts.map +1 -1
  125. package/dist/core/workflow-run.js.map +1 -1
  126. package/dist/hooks/api/annotations.d.ts +63 -0
  127. package/dist/hooks/api/annotations.d.ts.map +1 -0
  128. package/dist/hooks/api/annotations.js +145 -0
  129. package/dist/hooks/api/annotations.js.map +1 -0
  130. package/dist/hooks/api/run-annotation-mapper.d.ts +10 -0
  131. package/dist/hooks/api/run-annotation-mapper.d.ts.map +1 -0
  132. package/dist/hooks/api/run-annotation-mapper.js +22 -0
  133. package/dist/hooks/api/run-annotation-mapper.js.map +1 -0
  134. package/dist/hooks/api/run-annotations.d.ts +25 -0
  135. package/dist/hooks/api/run-annotations.d.ts.map +1 -0
  136. package/dist/hooks/api/run-annotations.js +78 -0
  137. package/dist/hooks/api/run-annotations.js.map +1 -0
  138. package/dist/hooks/api/step-attempt-detail.d.ts +14 -0
  139. package/dist/hooks/api/step-attempt-detail.d.ts.map +1 -0
  140. package/dist/hooks/api/step-attempt-detail.js +43 -0
  141. package/dist/hooks/api/step-attempt-detail.js.map +1 -0
  142. package/dist/hooks/api/workflow-run-mapper.d.ts +9 -2
  143. package/dist/hooks/api/workflow-run-mapper.d.ts.map +1 -1
  144. package/dist/hooks/api/workflow-run-mapper.js +47 -0
  145. package/dist/hooks/api/workflow-run-mapper.js.map +1 -1
  146. package/dist/pages/project-workflows-page.js +19 -19
  147. package/dist/pages/project-workflows-page.js.map +1 -1
  148. package/dist/pages/workflow-run-list-page.js +1 -1
  149. package/dist/pages/workflow-run-list-page.js.map +1 -1
  150. package/dist/routes/inputs.d.ts +7 -2
  151. package/dist/routes/inputs.d.ts.map +1 -1
  152. package/dist/routes/inputs.js +5 -6
  153. package/dist/routes/inputs.js.map +1 -1
  154. package/dist/tsconfig.test.tsbuildinfo +1 -1
  155. package/package.json +10 -9
  156. package/src/components/identifier/identifier.tsx +2 -2
  157. package/src/components/job-detail/agent-config-failure-callout.tsx +5 -1
  158. package/src/components/job-detail/job-context-panel.test.tsx +72 -0
  159. package/src/components/job-detail/job-context-panel.tsx +237 -0
  160. package/src/components/job-detail/job-detail-header.test.tsx +3 -0
  161. package/src/components/job-detail/job-detail-header.tsx +65 -34
  162. package/src/components/job-detail/job-detail-view.stories.tsx +555 -48
  163. package/src/components/job-detail/job-detail-view.tsx +101 -42
  164. package/src/components/job-detail/job-empty-states.tsx +23 -10
  165. package/src/components/job-detail/job-execution-switcher.tsx +5 -5
  166. package/src/components/job-detail/json-code.tsx +120 -0
  167. package/src/components/job-detail/step-attempt-log-panel.test.tsx +1 -0
  168. package/src/components/job-detail/step-attempt-log-panel.tsx +17 -6
  169. package/src/components/job-detail/step-troubleshooting.stories.tsx +120 -0
  170. package/src/components/job-detail/step-troubleshooting.test.tsx +337 -0
  171. package/src/components/job-detail/step-troubleshooting.tsx +537 -0
  172. package/src/components/job-graph/job-graph-content.tsx +4 -3
  173. package/src/components/job-graph/job-node.tsx +3 -3
  174. package/src/components/step-list/index.ts +1 -0
  175. package/src/components/step-list/step-list-model.ts +5 -0
  176. package/src/components/step-list/step-list.test.tsx +75 -2
  177. package/src/components/step-list/step-list.tsx +73 -20
  178. package/src/components/workflow-run-duration-label.tsx +1 -1
  179. package/src/components/workflow-run-list/job-status-strip.tsx +6 -6
  180. package/src/components/workflow-run-list/workflow-run-filter-menu.tsx +1 -1
  181. package/src/components/workflow-run-list/workflow-run-filters.tsx +4 -4
  182. package/src/components/workflow-run-list/workflow-run-list-states.tsx +6 -6
  183. package/src/components/workflow-run-list/workflow-run-list-view.tsx +1 -1
  184. package/src/components/workflow-run-list/workflow-run-row.tsx +7 -7
  185. package/src/components/workflow-run-summary/workflow-run-attempt-switcher.tsx +1 -1
  186. package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +1 -1
  187. package/src/components/workflow-run-summary/workflow-run-summary.tsx +12 -12
  188. package/src/components/workflow-run-tabs/index.ts +7 -0
  189. package/src/components/workflow-run-tabs/run-annotation-count-chip.tsx +63 -0
  190. package/src/components/workflow-run-tabs/run-annotation-item.tsx +123 -0
  191. package/src/components/workflow-run-tabs/run-annotation-list.tsx +273 -0
  192. package/src/components/workflow-run-tabs/run-annotation-summary-line.test.tsx +77 -3
  193. package/src/components/workflow-run-tabs/run-annotation-summary-line.tsx +95 -20
  194. package/src/components/workflow-run-tabs/run-annotations.stories.tsx +293 -0
  195. package/src/components/workflow-run-tabs/severity-visuals.ts +37 -0
  196. package/src/components/workflow-run-view/run-workspace-nav.test.tsx +44 -0
  197. package/src/components/workflow-run-view/run-workspace-nav.tsx +22 -9
  198. package/src/components/workflow-run-view/workflow-run-states.tsx +6 -6
  199. package/src/components/workflow-run-view/workflow-run-view.test.tsx +246 -20
  200. package/src/components/workflow-run-view/workflow-run-view.tsx +255 -37
  201. package/src/core/entities/job-execution.ts +7 -0
  202. package/src/core/entities/job.ts +9 -0
  203. package/src/core/entities/step-attempt.ts +33 -0
  204. package/src/core/entities/step.ts +3 -1
  205. package/src/core/run-annotation.test.ts +315 -0
  206. package/src/core/run-annotation.ts +244 -0
  207. package/src/core/workflow-run.ts +4 -0
  208. package/src/hooks/api/annotations.test.ts +84 -0
  209. package/src/hooks/api/annotations.ts +240 -0
  210. package/src/hooks/api/run-annotation-mapper.ts +30 -0
  211. package/src/hooks/api/run-annotations.ts +137 -0
  212. package/src/hooks/api/step-attempt-detail.ts +67 -0
  213. package/src/hooks/api/workflow-run-mapper.ts +46 -0
  214. package/src/hooks/api/workflow-runs.test.tsx +61 -1
  215. package/src/pages/project-workflows-page.tsx +19 -19
  216. package/src/pages/workflow-job-detail-page.test.tsx +118 -1
  217. package/src/pages/workflow-run-list-page.tsx +1 -1
  218. package/src/routes/inputs.ts +7 -2
  219. package/test/fixtures/workflow-run.ts +7 -0
  220. package/tsconfig.build.tsbuildinfo +1 -1
  221. package/dist/core/workflow-run-tabs.d.ts +0 -9
  222. package/dist/core/workflow-run-tabs.d.ts.map +0 -1
  223. package/dist/core/workflow-run-tabs.js +0 -3
  224. package/dist/core/workflow-run-tabs.js.map +0 -1
  225. package/src/core/workflow-run-tabs.ts +0 -8
@@ -1,13 +1,28 @@
1
+ import {MetadataSeparator} from '@shipfox/client-ui';
2
+ import {Icon} from '@shipfox/react-ui/icon';
1
3
  import {Text} from '@shipfox/react-ui/typography';
4
+ import {cn} from '@shipfox/react-ui/utils';
2
5
  import {Link} from '@tanstack/react-router';
3
- import type {RunAnnotationSummary} from '#core/workflow-run-tabs.js';
6
+ import type {RunAnnotationSummary} from '#core/run-annotation.js';
4
7
  import {
5
- WORKFLOW_RUN_ANNOTATION_SEVERITIES,
6
8
  type WorkflowRunAnnotationSeverity,
7
9
  type WorkflowRunsSearch,
8
10
  workflowRunSearchParams,
9
11
  } from '#routes/inputs.js';
10
- import {MetadataSeparator} from '../workflow-run-summary/workflow-run-summary.js';
12
+ import {SEVERITY_ICON, SEVERITY_ICON_TONE} from './severity-visuals.js';
13
+
14
+ /**
15
+ * The severities this line names.
16
+ *
17
+ * The line exists so a failing run cannot look clean while the explanation sits behind an
18
+ * inactive section, which makes its subject "what needs attention", not "what happened".
19
+ * `info` and `success` are already inside the total, and repeating them gives "nothing is
20
+ * wrong" the same weight and the same accent as "something failed".
21
+ */
22
+ const ATTENTION_SEVERITIES = [
23
+ 'error',
24
+ 'warning',
25
+ ] as const satisfies readonly WorkflowRunAnnotationSeverity[];
11
26
 
12
27
  export interface RunAnnotationSummaryLineProps {
13
28
  summary?: RunAnnotationSummary | undefined;
@@ -24,22 +39,49 @@ export function RunAnnotationSummaryLine({
24
39
  workflowRunId,
25
40
  search = {},
26
41
  }: RunAnnotationSummaryLineProps) {
27
- if (!summary) return null;
42
+ // A run with no annotations says so in its empty state; a "0 annotations" line beside it is
43
+ // the same fact twice.
44
+ if (!summary || summary.total === 0) return null;
28
45
 
29
- const visibleSeverities = WORKFLOW_RUN_ANNOTATION_SEVERITIES.filter(
30
- (severity) => summary[severity] > 0,
31
- );
46
+ const visibleSeverities = ATTENTION_SEVERITIES.filter((severity) => summary[severity] > 0);
47
+
48
+ const totalLabel = countLabel(summary.total, 'annotation', summary.truncated);
32
49
 
33
50
  return (
34
- <div className="flex flex-wrap items-center gap-x-8 gap-y-4 text-foreground-neutral-subtle">
35
- <Text as="span" size="xs" className="shrink-0 text-foreground-neutral-subtle">
36
- {summary.total} {pluralize('annotation', summary.total)}
37
- </Text>
51
+ <div className="flex flex-wrap items-center gap-x-inline gap-y-tight text-foreground-neutral-subtle">
52
+ {search.severity && workspaceSlug && projectSlug && workflowRunId ? (
53
+ // The severity filter has no control of its own, so while one is active the total is
54
+ // the way back out. Unfiltered it stays plain text: a link to where you already are
55
+ // is noise.
56
+ <Link
57
+ to="/w/$workspaceSlug/p/$projectSlug/runs/$workflowRunId"
58
+ params={{workspaceSlug, projectSlug, workflowRunId}}
59
+ search={
60
+ workflowRunSearchParams(
61
+ {...withoutSeverity(search), tab: 'annotations'},
62
+ search,
63
+ ) as never
64
+ }
65
+ aria-label={`Show all ${totalLabel}`}
66
+ className="shrink-0 rounded-4 outline-none focus-visible:shadow-border-interactive-with-active"
67
+ >
68
+ {/* Underlined rather than accented: the escape hatch has to read as a link without
69
+ becoming the loudest thing on a line whose job is to be scannable. */}
70
+ <Text as="span" size="xs" className="text-foreground-neutral-subtle underline">
71
+ {totalLabel}
72
+ </Text>
73
+ </Link>
74
+ ) : (
75
+ <Text as="span" size="xs" className="shrink-0 text-foreground-neutral-subtle">
76
+ {totalLabel}
77
+ </Text>
78
+ )}
38
79
  {visibleSeverities.map((severity) => (
39
- <span key={severity} className="inline-flex shrink-0 items-center gap-8">
80
+ <span key={severity} className="inline-flex shrink-0 items-center gap-inline">
40
81
  <MetadataSeparator />
41
82
  <SeverityLink
42
83
  count={summary[severity]}
84
+ truncated={summary.truncated}
43
85
  severity={severity}
44
86
  workspaceSlug={workspaceSlug}
45
87
  projectSlug={projectSlug}
@@ -54,6 +96,7 @@ export function RunAnnotationSummaryLine({
54
96
 
55
97
  function SeverityLink({
56
98
  count,
99
+ truncated,
57
100
  severity,
58
101
  workspaceSlug,
59
102
  projectSlug,
@@ -61,19 +104,33 @@ function SeverityLink({
61
104
  search,
62
105
  }: {
63
106
  count: number;
107
+ truncated: boolean;
64
108
  severity: WorkflowRunAnnotationSeverity;
65
109
  workspaceSlug?: string | undefined;
66
110
  projectSlug?: string | undefined;
67
111
  workflowRunId?: string | undefined;
68
112
  search: WorkflowRunsSearch;
69
113
  }) {
70
- const label = `${count} ${pluralize(severity, count)}`;
114
+ const label = countLabel(count, severity, truncated);
115
+ const content = (
116
+ <>
117
+ <Icon
118
+ name={SEVERITY_ICON[severity]}
119
+ size={12}
120
+ aria-hidden="true"
121
+ className={cn('shrink-0', SEVERITY_ICON_TONE[severity])}
122
+ />
123
+ <Text as="span" size="xs" className="text-foreground-neutral-subtle">
124
+ {label}
125
+ </Text>
126
+ </>
127
+ );
128
+
71
129
  if (!workspaceSlug || !projectSlug || !workflowRunId) {
72
- return <span className="text-foreground-highlight-interactive">{label}</span>;
130
+ return <span className="inline-flex items-center gap-tight">{content}</span>;
73
131
  }
74
132
 
75
- const searchWithoutAnnotation = {...search};
76
- delete searchWithoutAnnotation.annotation;
133
+ const searchWithoutAnnotation = withoutAnnotation(search);
77
134
 
78
135
  return (
79
136
  <Link
@@ -85,13 +142,31 @@ function SeverityLink({
85
142
  search,
86
143
  ) as never
87
144
  }
88
- className="text-foreground-highlight-interactive outline-none hover:underline focus-visible:shadow-border-interactive-with-active"
145
+ className="inline-flex items-center gap-tight rounded-4 outline-none hover:underline focus-visible:shadow-border-interactive-with-active"
89
146
  >
90
- {label}
147
+ {content}
91
148
  </Link>
92
149
  );
93
150
  }
94
151
 
95
- function pluralize(word: string, count: number): string {
96
- return count === 1 ? word : `${word}s`;
152
+ /** A selected annotation is stale the moment the list it was chosen from is refiltered. */
153
+ function withoutAnnotation(search: WorkflowRunsSearch): WorkflowRunsSearch {
154
+ const next = {...search};
155
+ delete next.annotation;
156
+ return next;
157
+ }
158
+
159
+ function withoutSeverity(search: WorkflowRunsSearch): WorkflowRunsSearch {
160
+ const next = withoutAnnotation(search);
161
+ delete next.severity;
162
+ return next;
163
+ }
164
+
165
+ /**
166
+ * A truncated read renders `500+ annotations`: the counts are a lower bound once the page budget
167
+ * is hit, and presenting them as exact would understate a run the reader is trying to trust.
168
+ */
169
+ function countLabel(count: number, word: string, truncated: boolean): string {
170
+ const plural = count === 1 && !truncated ? word : `${word}s`;
171
+ return `${count}${truncated ? '+' : ''} ${plural}`;
97
172
  }
@@ -0,0 +1,293 @@
1
+ import type {Meta, StoryObj} from '@storybook/react';
2
+ import type {ReactNode} from 'react';
3
+ import {
4
+ buildRunAnnotationList,
5
+ type RunAnnotationRecord,
6
+ type RunAnnotationStyle,
7
+ summarizeRunAnnotations,
8
+ } from '#core/run-annotation.js';
9
+ import type {Job} from '#core/workflow-run.js';
10
+ import {
11
+ workflowJob,
12
+ workflowJobExecutionDto,
13
+ workflowStepAttemptDto,
14
+ workflowStepDto,
15
+ } from '#test/fixtures/workflow-run.js';
16
+ import {RunAnnotationCountChip} from './run-annotation-count-chip.js';
17
+ import {RunAnnotationList, type RunAnnotationListQuery} from './run-annotation-list.js';
18
+ import {RunAnnotationSummaryLine} from './run-annotation-summary-line.js';
19
+
20
+ const WORKSPACE_SLUG = 'acme';
21
+ const PROJECT_SLUG = 'platform';
22
+ const RUN_ID = '11111111-1111-4111-8111-111111111111';
23
+ const BUILD_JOB_ID = '44444444-4444-4444-8444-00000000000b';
24
+ const TEST_JOB_ID = '44444444-4444-4444-8444-00000000000c';
25
+ const BUILD_EXECUTION_ID = '77777777-7777-4777-8777-00000000000b';
26
+ const TEST_EXECUTION_ID = '77777777-7777-4777-8777-00000000000c';
27
+ const BUILD_STEP_ID = '55555555-5555-4555-8555-00000000000b';
28
+ const TEST_STEP_ID = '55555555-5555-4555-8555-00000000000c';
29
+ const BUILD_ATTEMPT_ID = '66666666-6666-4666-8666-00000000000b';
30
+ const TEST_ATTEMPT_ID = '66666666-6666-4666-8666-00000000000c';
31
+
32
+ const LONG_BODY = [
33
+ '### Failing specs',
34
+ '',
35
+ ...Array.from(
36
+ {length: 40},
37
+ (_unused, index) => `- \`packages/web/src/route-${index}.test.ts\` expected 200, received 503`,
38
+ ),
39
+ ].join('\n');
40
+
41
+ const meta = {
42
+ title: 'Workflows/RunAnnotations',
43
+ parameters: {
44
+ layout: 'fullscreen',
45
+ },
46
+ } satisfies Meta;
47
+
48
+ export default meta;
49
+ type Story = StoryObj<typeof meta>;
50
+
51
+ export const Playground: Story = {
52
+ render: () => {
53
+ const annotations = [
54
+ annotation({
55
+ id: 'a1',
56
+ context: 'smoke check',
57
+ style: 'error',
58
+ sequence: 3,
59
+ body: 'Task nine failed the smoke check against `https://preview.example.com`.\n\n```sh\ncurl -sSf https://preview.example.com/healthz\n```',
60
+ }),
61
+ annotation({
62
+ id: 'a2',
63
+ context: 'flaky tests',
64
+ style: 'warning',
65
+ sequence: 1,
66
+ jobId: TEST_JOB_ID,
67
+ jobExecutionId: TEST_EXECUTION_ID,
68
+ originStepId: TEST_STEP_ID,
69
+ body: '2 specs retried before passing.\n\n| Spec | Retries |\n| --- | --- |\n| `checkout.spec.ts` | 1 |\n| `billing.spec.ts` | 2 |',
70
+ }),
71
+ annotation({
72
+ id: 'a3',
73
+ context: 'deploy',
74
+ style: 'success',
75
+ sequence: 2,
76
+ body: 'Deployed **v42** to staging: [preview](https://preview.example.com)',
77
+ }),
78
+ annotation({
79
+ id: 'a4',
80
+ context: 'coverage',
81
+ style: 'info',
82
+ sequence: 4,
83
+ body: 'Line coverage held at 87.4%.',
84
+ }),
85
+ ];
86
+
87
+ return <AnnotationsFrame annotations={annotations} />;
88
+ },
89
+ };
90
+
91
+ export const BoundedBody: Story = {
92
+ // The only story in this package that earns a second Argos mode. The clamp's fade is the one
93
+ // gradient in the client, and it blends a surface token that flips with the theme, so a
94
+ // dark-only capture would never catch it breaking on a light card.
95
+ parameters: {
96
+ argos: {modes: {dark: {theme: 'dark'}, light: {theme: 'light'}}},
97
+ },
98
+ render: () => (
99
+ <AnnotationsFrame
100
+ annotations={[
101
+ annotation({id: 'long', context: 'test results', style: 'error', body: LONG_BODY}),
102
+ ]}
103
+ />
104
+ ),
105
+ };
106
+
107
+ export const DataStates: Story = {
108
+ render: () => {
109
+ const loaded = [annotation({id: 'a1', context: 'deploy', style: 'success'})];
110
+ return (
111
+ <div className="grid max-w-[1120px] gap-16 p-16 md:grid-cols-2">
112
+ <StateExample label="Loading">
113
+ <AnnotationsList annotations={[]} query={storyQuery({isPending: true})} />
114
+ </StateExample>
115
+ <StateExample label="Empty">
116
+ <AnnotationsList annotations={[]} />
117
+ </StateExample>
118
+ <StateExample label="Error">
119
+ <AnnotationsList
120
+ annotations={[]}
121
+ query={storyQuery({isError: true, data: undefined, error: new Error('Storybook')})}
122
+ />
123
+ </StateExample>
124
+ <StateExample label="Stale">
125
+ <AnnotationsList
126
+ annotations={loaded}
127
+ query={storyQuery({isError: true, error: new Error('Storybook')})}
128
+ />
129
+ </StateExample>
130
+ <StateExample label="Filtered miss">
131
+ <AnnotationsList annotations={[]} filtered filteredJobName="build" />
132
+ </StateExample>
133
+ <StateExample label="Truncated counts">
134
+ <RunAnnotationSummaryLine
135
+ summary={{...summarizeRunAnnotations(loaded), total: 500, error: 12, truncated: true}}
136
+ workspaceSlug={WORKSPACE_SLUG}
137
+ projectSlug={PROJECT_SLUG}
138
+ workflowRunId={RUN_ID}
139
+ />
140
+ </StateExample>
141
+ </div>
142
+ );
143
+ },
144
+ };
145
+
146
+ export const CountChips: Story = {
147
+ render: () => (
148
+ <div className="flex max-w-[1120px] flex-wrap items-center gap-12 p-16">
149
+ {(['error', 'warning', 'info', 'success'] as const).map((style) => (
150
+ <RunAnnotationCountChip
151
+ key={style}
152
+ summary={summarizeRunAnnotations([annotation({id: style, context: style, style})])}
153
+ workspaceSlug={WORKSPACE_SLUG}
154
+ projectSlug={PROJECT_SLUG}
155
+ workflowRunId={RUN_ID}
156
+ jobId={BUILD_JOB_ID}
157
+ />
158
+ ))}
159
+ <RunAnnotationCountChip
160
+ summary={{total: 500, error: 12, warning: 3, info: 0, success: 0, truncated: true}}
161
+ workspaceSlug={WORKSPACE_SLUG}
162
+ projectSlug={PROJECT_SLUG}
163
+ workflowRunId={RUN_ID}
164
+ />
165
+ </div>
166
+ ),
167
+ };
168
+
169
+ function AnnotationsFrame({annotations}: {annotations: RunAnnotationRecord[]}) {
170
+ return (
171
+ <div className="min-h-screen bg-background-neutral-base py-16">
172
+ <div className="mx-auto flex w-full max-w-[1120px] flex-col gap-16 px-24">
173
+ <RunAnnotationSummaryLine
174
+ summary={summarizeRunAnnotations(annotations)}
175
+ workspaceSlug={WORKSPACE_SLUG}
176
+ projectSlug={PROJECT_SLUG}
177
+ workflowRunId={RUN_ID}
178
+ />
179
+ <AnnotationsList annotations={annotations} />
180
+ </div>
181
+ </div>
182
+ );
183
+ }
184
+
185
+ function AnnotationsList({
186
+ annotations,
187
+ query = storyQuery(),
188
+ filtered = false,
189
+ filteredJobName,
190
+ }: {
191
+ annotations: RunAnnotationRecord[];
192
+ query?: RunAnnotationListQuery;
193
+ filtered?: boolean;
194
+ filteredJobName?: string;
195
+ }) {
196
+ return (
197
+ <RunAnnotationList
198
+ query={query}
199
+ entries={query.data === undefined ? undefined : buildRunAnnotationList({annotations, jobs})}
200
+ workspaceSlug={WORKSPACE_SLUG}
201
+ projectSlug={PROJECT_SLUG}
202
+ workflowRunId={RUN_ID}
203
+ runAttempt={1}
204
+ filtered={filtered}
205
+ filteredJobName={filteredJobName}
206
+ onClearFilters={() => undefined}
207
+ />
208
+ );
209
+ }
210
+
211
+ function StateExample({label, children}: {label: string; children: ReactNode}) {
212
+ return (
213
+ <div className="flex flex-col gap-8">
214
+ <span className="text-xs font-medium text-foreground-neutral-muted">{label}</span>
215
+ {children}
216
+ </div>
217
+ );
218
+ }
219
+
220
+ function storyQuery(overrides: Partial<RunAnnotationListQuery> = {}): RunAnnotationListQuery {
221
+ return {
222
+ isPending: false,
223
+ isError: false,
224
+ isFetching: false,
225
+ data: {pages: []},
226
+ error: null,
227
+ refetch: () => undefined,
228
+ hasNextPage: false,
229
+ isFetchingNextPage: false,
230
+ fetchNextPage: () => undefined,
231
+ ...overrides,
232
+ };
233
+ }
234
+
235
+ function annotation(
236
+ overrides: Partial<RunAnnotationRecord> & {id: string; style: RunAnnotationStyle},
237
+ ): RunAnnotationRecord {
238
+ return {
239
+ jobId: BUILD_JOB_ID,
240
+ jobExecutionId: BUILD_EXECUTION_ID,
241
+ originStepId: BUILD_STEP_ID,
242
+ originStepAttempt: 1,
243
+ context: 'default',
244
+ sequence: 1,
245
+ body: 'Body',
246
+ ...overrides,
247
+ };
248
+ }
249
+
250
+ const jobs: Job[] = [
251
+ workflowJob({
252
+ id: BUILD_JOB_ID,
253
+ key: 'build',
254
+ name: 'build',
255
+ position: 0,
256
+ job_executions: [
257
+ workflowJobExecutionDto({
258
+ id: BUILD_EXECUTION_ID,
259
+ job_id: BUILD_JOB_ID,
260
+ steps: [
261
+ workflowStepDto({
262
+ id: BUILD_STEP_ID,
263
+ name: 'run smoke checks',
264
+ attempts: [
265
+ workflowStepAttemptDto({id: BUILD_ATTEMPT_ID, step_id: BUILD_STEP_ID, attempt: 1}),
266
+ ],
267
+ }),
268
+ ],
269
+ }),
270
+ ],
271
+ }),
272
+ workflowJob({
273
+ id: TEST_JOB_ID,
274
+ key: 'test',
275
+ name: 'test',
276
+ position: 1,
277
+ job_executions: [
278
+ workflowJobExecutionDto({
279
+ id: TEST_EXECUTION_ID,
280
+ job_id: TEST_JOB_ID,
281
+ steps: [
282
+ workflowStepDto({
283
+ id: TEST_STEP_ID,
284
+ name: 'vitest',
285
+ attempts: [
286
+ workflowStepAttemptDto({id: TEST_ATTEMPT_ID, step_id: TEST_STEP_ID, attempt: 1}),
287
+ ],
288
+ }),
289
+ ],
290
+ }),
291
+ ],
292
+ }),
293
+ ];
@@ -0,0 +1,37 @@
1
+ import type {IconName} from '@shipfox/react-ui/icon';
2
+ import type {RunAnnotationSeverity} from '#core/run-annotation.js';
3
+
4
+ /**
5
+ * The glyph vocabulary for annotation severity, shared by every count surface.
6
+ *
7
+ * Deliberately the same marks the `Callout` uses on the cards themselves, so the glyph beside
8
+ * `1 error` in the summary is the glyph on the error card below it.
9
+ */
10
+ export const SEVERITY_ICON: Record<RunAnnotationSeverity, IconName> = {
11
+ error: 'closeCircleFill',
12
+ warning: 'errorWarningFill',
13
+ info: 'info',
14
+ success: 'checkboxCircleFill',
15
+ };
16
+
17
+ /**
18
+ * Severity tone lives in the glyph, never in the label.
19
+ *
20
+ * Coloring the text would make severity compete with the brand accent, which this system
21
+ * reserves for links, focus, and "you are here". Shape plus tone in a 12px mark distinguishes
22
+ * an error from a warning without either of them shouting.
23
+ */
24
+ export const SEVERITY_ICON_TONE: Record<RunAnnotationSeverity, string> = {
25
+ error: 'text-tag-error-icon',
26
+ warning: 'text-tag-warning-icon',
27
+ info: 'text-tag-blue-icon',
28
+ success: 'text-tag-success-icon',
29
+ };
30
+
31
+ /** Hairline tone for a bordered count chip, matching the glyph it sits beside. */
32
+ export const SEVERITY_CHIP_TONE: Record<RunAnnotationSeverity, string> = {
33
+ error: 'border-tag-error-border text-tag-error-icon',
34
+ warning: 'border-tag-warning-border text-tag-warning-icon',
35
+ info: 'border-tag-blue-border text-tag-blue-icon',
36
+ success: 'border-tag-success-border text-tag-success-icon',
37
+ };
@@ -27,6 +27,50 @@ describe('RunWorkspaceNav', () => {
27
27
  vi.useRealTimers();
28
28
  });
29
29
 
30
+ test('announces the annotation count with its unit and marks a truncated read', async () => {
31
+ const run = workflowRunDetail({
32
+ id: RUN_ID,
33
+ jobs: [workflowJobDto({id: CURRENT_JOB_ID, name: 'build', position: 0})],
34
+ });
35
+
36
+ renderWithRouter(
37
+ <RunWorkspaceNav
38
+ workspaceSlug="acme"
39
+ projectSlug="project"
40
+ run={run}
41
+ activeSection="summary"
42
+ annotationSummary={{
43
+ total: 500,
44
+ error: 12,
45
+ warning: 3,
46
+ info: 0,
47
+ success: 0,
48
+ truncated: true,
49
+ }}
50
+ />,
51
+ );
52
+
53
+ expect(await screen.findByRole('link', {name: 'Annotations, 500 or more'})).toBeVisible();
54
+ });
55
+
56
+ test('omits the annotation count until the read resolves', async () => {
57
+ const run = workflowRunDetail({
58
+ id: RUN_ID,
59
+ jobs: [workflowJobDto({id: CURRENT_JOB_ID, name: 'build', position: 0})],
60
+ });
61
+
62
+ renderWithRouter(
63
+ <RunWorkspaceNav
64
+ workspaceSlug="acme"
65
+ projectSlug="project"
66
+ run={run}
67
+ activeSection="summary"
68
+ />,
69
+ );
70
+
71
+ expect(await screen.findByRole('link', {name: 'Annotations'})).toBeVisible();
72
+ });
73
+
30
74
  test('shows the complete run hierarchy and marks the current job', async () => {
31
75
  const run = workflowRunDetail({
32
76
  id: RUN_ID,
@@ -6,13 +6,13 @@ import {cn} from '@shipfox/react-ui/utils';
6
6
  import {Link} from '@tanstack/react-router';
7
7
  import {useEffect, useMemo, useRef, useState} from 'react';
8
8
  import {WorkflowStatusIcon} from '#components/workflow-status/workflow-status-icon.js';
9
+ import type {RunAnnotationSummary} from '#core/run-annotation.js';
9
10
  import {
10
11
  defaultJobExecution,
11
12
  deriveJobDisplayStatus,
12
13
  type Job,
13
14
  type WorkflowRunDetail,
14
15
  } from '#core/workflow-run.js';
15
- import type {RunAnnotationSummary} from '#core/workflow-run-tabs.js';
16
16
  import {
17
17
  type WorkflowJobSearch,
18
18
  type WorkflowRunTab,
@@ -58,7 +58,7 @@ export function RunWorkspaceNav({
58
58
  <CollapsibleTrigger asChild>
59
59
  <button
60
60
  type="button"
61
- className="flex min-h-44 w-full items-center justify-between gap-12 px-16 text-left outline-none focus-visible:shadow-border-interactive-with-active min-[768px]:hidden"
61
+ className="flex min-h-44 w-full items-center justify-between gap-cluster px-row text-left outline-none focus-visible:shadow-border-interactive-with-active min-[768px]:hidden"
62
62
  aria-label="Toggle run navigation"
63
63
  >
64
64
  <span className="min-w-0">
@@ -82,7 +82,7 @@ export function RunWorkspaceNav({
82
82
  </CollapsibleTrigger>
83
83
  <div
84
84
  className={cn(
85
- 'max-h-[50vh] overflow-y-auto p-8 min-[768px]:block min-[768px]:max-h-none min-[768px]:p-12',
85
+ 'max-h-[50vh] overflow-y-auto p-tight min-[768px]:block min-[768px]:max-h-none min-[768px]:p-[12px]',
86
86
  !mobileOpen && 'hidden',
87
87
  )}
88
88
  >
@@ -136,7 +136,7 @@ function RunWorkspaceNavContent({
136
136
  }, [currentJobId, mobileOpen]);
137
137
 
138
138
  return (
139
- <nav aria-label="Run workspace" className="flex min-w-0 flex-col gap-16">
139
+ <nav aria-label="Run workspace" className="flex min-w-0 flex-col gap-group">
140
140
  <ul>
141
141
  <li>
142
142
  <RunSectionLink
@@ -152,7 +152,7 @@ function RunWorkspaceNavContent({
152
152
  </ul>
153
153
 
154
154
  <section aria-labelledby="run-workspace-jobs-heading" className="min-w-0">
155
- <div className="flex items-center justify-between gap-8 px-8 pb-6">
155
+ <div className="flex items-center justify-between gap-inline px-tight pb-[6px]">
156
156
  <Text
157
157
  as="h2"
158
158
  id="run-workspace-jobs-heading"
@@ -182,7 +182,7 @@ function RunWorkspaceNavContent({
182
182
  onClick={onNavigate}
183
183
  aria-current={current ? 'page' : undefined}
184
184
  className={cn(
185
- 'flex min-h-32 min-w-0 items-center gap-8 rounded-4 px-8 text-left outline-none transition-colors hover:bg-background-neutral-hover focus-visible:shadow-border-interactive-with-active @max-[767px]:min-h-44 [@media(pointer:coarse)]:min-h-44',
185
+ 'flex min-h-32 min-w-0 items-center gap-inline rounded-4 px-tight text-left outline-none transition-colors hover:bg-background-neutral-hover focus-visible:shadow-border-interactive-with-active @max-[767px]:min-h-44 [@media(pointer:coarse)]:min-h-44',
186
186
  current && 'bg-background-neutral-hover',
187
187
  )}
188
188
  >
@@ -210,7 +210,7 @@ function RunWorkspaceNavContent({
210
210
  id="run-workspace-details-heading"
211
211
  size="xs"
212
212
  bold
213
- className="px-8 pb-6 text-foreground-neutral-muted"
213
+ className="px-tight pb-[6px] text-foreground-neutral-muted"
214
214
  >
215
215
  Run details
216
216
  </Text>
@@ -224,6 +224,7 @@ function RunWorkspaceNavContent({
224
224
  section="annotations"
225
225
  current={!currentJobId && activeSection === 'annotations'}
226
226
  count={annotationSummary?.total}
227
+ countTruncated={annotationSummary?.truncated}
227
228
  onNavigate={onNavigate}
228
229
  />
229
230
  </li>
@@ -252,6 +253,7 @@ function RunSectionLink({
252
253
  section,
253
254
  current,
254
255
  count,
256
+ countTruncated = false,
255
257
  onNavigate,
256
258
  }: {
257
259
  workspaceSlug: string;
@@ -261,6 +263,8 @@ function RunSectionLink({
261
263
  section: RunWorkspaceSection;
262
264
  current: boolean;
263
265
  count?: number | undefined;
266
+ /** The read hit its page budget, so the count is a lower bound and renders as `N+`. */
267
+ countTruncated?: boolean | undefined;
264
268
  onNavigate?: (() => void) | undefined;
265
269
  }) {
266
270
  return (
@@ -275,15 +279,24 @@ function RunSectionLink({
275
279
  }
276
280
  onClick={onNavigate}
277
281
  aria-current={current ? 'page' : undefined}
282
+ aria-label={
283
+ count
284
+ ? `${sectionLabel(section)}, ${countTruncated ? `${count} or more` : count}`
285
+ : undefined
286
+ }
278
287
  className={cn(
279
- 'flex min-h-32 items-center gap-8 rounded-4 px-8 text-xs font-medium text-foreground-neutral-base outline-none transition-colors hover:bg-background-neutral-hover focus-visible:shadow-border-interactive-with-active @max-[767px]:min-h-44 [@media(pointer:coarse)]:min-h-44',
288
+ 'flex min-h-32 items-center gap-inline rounded-4 px-tight text-xs font-medium text-foreground-neutral-base outline-none transition-colors hover:bg-background-neutral-hover focus-visible:shadow-border-interactive-with-active @max-[767px]:min-h-44 [@media(pointer:coarse)]:min-h-44',
280
289
  current && 'bg-background-neutral-hover',
281
290
  )}
282
291
  >
283
292
  {sectionLabel(section)}
284
293
  {count ? (
285
- <span className="ml-auto font-code text-xs text-foreground-neutral-muted tabular-nums">
294
+ <span
295
+ aria-hidden="true"
296
+ className="ml-auto font-code text-xs text-foreground-neutral-muted tabular-nums"
297
+ >
286
298
  {count}
299
+ {countTruncated ? '+' : ''}
287
300
  </span>
288
301
  ) : null}
289
302
  </Link>