@shipfox/client-workflows 15.0.0 → 16.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.
- package/.storybook/preview.css +4 -0
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +21 -0
- package/dist/components/job-detail/job-detail-header.d.ts +8 -1
- package/dist/components/job-detail/job-detail-header.d.ts.map +1 -1
- package/dist/components/job-detail/job-detail-header.js +25 -12
- package/dist/components/job-detail/job-detail-header.js.map +1 -1
- package/dist/components/job-detail/job-detail-view.d.ts.map +1 -1
- package/dist/components/job-detail/job-detail-view.js +17 -2
- package/dist/components/job-detail/job-detail-view.js.map +1 -1
- package/dist/components/workflow-run-summary/workflow-run-summary.js +1 -0
- package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
- package/dist/components/workflow-run-tabs/index.d.ts +3 -0
- package/dist/components/workflow-run-tabs/index.d.ts.map +1 -1
- package/dist/components/workflow-run-tabs/index.js +3 -0
- package/dist/components/workflow-run-tabs/index.js.map +1 -1
- package/dist/components/workflow-run-tabs/run-annotation-count-chip.d.ts +16 -0
- package/dist/components/workflow-run-tabs/run-annotation-count-chip.d.ts.map +1 -0
- package/dist/components/workflow-run-tabs/run-annotation-count-chip.js +50 -0
- package/dist/components/workflow-run-tabs/run-annotation-count-chip.js.map +1 -0
- package/dist/components/workflow-run-tabs/run-annotation-item.d.ts +20 -0
- package/dist/components/workflow-run-tabs/run-annotation-item.d.ts.map +1 -0
- package/dist/components/workflow-run-tabs/run-annotation-item.js +102 -0
- package/dist/components/workflow-run-tabs/run-annotation-item.js.map +1 -0
- package/dist/components/workflow-run-tabs/run-annotation-list.d.ts +32 -0
- package/dist/components/workflow-run-tabs/run-annotation-list.d.ts.map +1 -0
- package/dist/components/workflow-run-tabs/run-annotation-list.js +202 -0
- package/dist/components/workflow-run-tabs/run-annotation-list.js.map +1 -0
- package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts +1 -1
- package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts.map +1 -1
- package/dist/components/workflow-run-tabs/run-annotation-summary-line.js +87 -22
- package/dist/components/workflow-run-tabs/run-annotation-summary-line.js.map +1 -1
- package/dist/components/workflow-run-tabs/severity-visuals.d.ts +20 -0
- package/dist/components/workflow-run-tabs/severity-visuals.d.ts.map +1 -0
- package/dist/components/workflow-run-tabs/severity-visuals.js +31 -0
- package/dist/components/workflow-run-tabs/severity-visuals.js.map +1 -0
- package/dist/components/workflow-run-view/run-workspace-nav.d.ts +1 -1
- package/dist/components/workflow-run-view/run-workspace-nav.d.ts.map +1 -1
- package/dist/components/workflow-run-view/run-workspace-nav.js +9 -3
- package/dist/components/workflow-run-view/run-workspace-nav.js.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-view.d.ts +1 -3
- package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-view.js +150 -52
- package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
- package/dist/core/run-annotation.d.ts +73 -0
- package/dist/core/run-annotation.d.ts.map +1 -0
- package/dist/core/run-annotation.js +125 -0
- package/dist/core/run-annotation.js.map +1 -0
- package/dist/hooks/api/run-annotation-mapper.d.ts +10 -0
- package/dist/hooks/api/run-annotation-mapper.d.ts.map +1 -0
- package/dist/hooks/api/run-annotation-mapper.js +22 -0
- package/dist/hooks/api/run-annotation-mapper.js.map +1 -0
- package/dist/hooks/api/run-annotations.d.ts +25 -0
- package/dist/hooks/api/run-annotations.d.ts.map +1 -0
- package/dist/hooks/api/run-annotations.js +78 -0
- package/dist/hooks/api/run-annotations.js.map +1 -0
- package/dist/routes/inputs.d.ts +7 -2
- package/dist/routes/inputs.d.ts.map +1 -1
- package/dist/routes/inputs.js +5 -6
- package/dist/routes/inputs.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +10 -9
- package/src/components/job-detail/job-detail-header.test.tsx +3 -0
- package/src/components/job-detail/job-detail-header.tsx +30 -5
- package/src/components/job-detail/job-detail-view.stories.tsx +5 -0
- package/src/components/job-detail/job-detail-view.tsx +18 -1
- package/src/components/workflow-run-summary/workflow-run-summary.tsx +1 -1
- package/src/components/workflow-run-tabs/index.ts +3 -0
- package/src/components/workflow-run-tabs/run-annotation-count-chip.tsx +63 -0
- package/src/components/workflow-run-tabs/run-annotation-item.tsx +123 -0
- package/src/components/workflow-run-tabs/run-annotation-list.tsx +258 -0
- package/src/components/workflow-run-tabs/run-annotation-summary-line.test.tsx +77 -3
- package/src/components/workflow-run-tabs/run-annotation-summary-line.tsx +92 -17
- package/src/components/workflow-run-tabs/run-annotations.stories.tsx +293 -0
- package/src/components/workflow-run-tabs/severity-visuals.ts +37 -0
- package/src/components/workflow-run-view/run-workspace-nav.test.tsx +44 -0
- package/src/components/workflow-run-view/run-workspace-nav.tsx +15 -2
- package/src/components/workflow-run-view/workflow-run-view.test.tsx +246 -20
- package/src/components/workflow-run-view/workflow-run-view.tsx +148 -30
- package/src/core/run-annotation.test.ts +315 -0
- package/src/core/run-annotation.ts +238 -0
- package/src/hooks/api/run-annotation-mapper.ts +30 -0
- package/src/hooks/api/run-annotations.ts +137 -0
- package/src/pages/workflow-job-detail-page.test.tsx +66 -0
- package/src/routes/inputs.ts +7 -2
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/core/workflow-run-tabs.d.ts +0 -9
- package/dist/core/workflow-run-tabs.d.ts.map +0 -1
- package/dist/core/workflow-run-tabs.js +0 -3
- package/dist/core/workflow-run-tabs.js.map +0 -1
- package/src/core/workflow-run-tabs.ts +0 -8
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
RouterProvider,
|
|
8
8
|
} from '@tanstack/react-router';
|
|
9
9
|
import {act, render, screen} from '@testing-library/react';
|
|
10
|
-
import type {RunAnnotationSummary} from '#core/
|
|
10
|
+
import type {RunAnnotationSummary} from '#core/run-annotation.js';
|
|
11
11
|
import type {WorkflowRunsSearch} from '#routes/inputs.js';
|
|
12
12
|
import {RunAnnotationSummaryLine} from './run-annotation-summary-line.js';
|
|
13
13
|
|
|
@@ -17,8 +17,12 @@ const ANNOTATION_SUMMARY: RunAnnotationSummary = {
|
|
|
17
17
|
warning: 1,
|
|
18
18
|
info: 2,
|
|
19
19
|
success: 0,
|
|
20
|
+
truncated: false,
|
|
20
21
|
};
|
|
21
22
|
|
|
23
|
+
const INFO_OR_SUCCESS_PATTERN = /info|success/;
|
|
24
|
+
const SHOW_ALL_PATTERN = /Show all/;
|
|
25
|
+
|
|
22
26
|
describe('RunAnnotationSummaryLine', () => {
|
|
23
27
|
test('links the severity breakdown into the Annotations panel', async () => {
|
|
24
28
|
await renderSummaryLine();
|
|
@@ -30,6 +34,66 @@ describe('RunAnnotationSummaryLine', () => {
|
|
|
30
34
|
);
|
|
31
35
|
});
|
|
32
36
|
|
|
37
|
+
test('renders every count at one type scale', async () => {
|
|
38
|
+
// The severity counts previously bypassed `Text` and inherited the ambient 14px, so they
|
|
39
|
+
// rendered a step larger and heavier than the total they sit beside.
|
|
40
|
+
await renderSummaryLine();
|
|
41
|
+
|
|
42
|
+
const total = screen.getByText('5 annotations');
|
|
43
|
+
const errors = screen.getByText('2 errors');
|
|
44
|
+
|
|
45
|
+
expect(total).toHaveClass('text-xs', 'font-display');
|
|
46
|
+
expect(errors).toHaveClass('text-xs', 'font-display');
|
|
47
|
+
expect(errors).not.toHaveClass('text-foreground-highlight-interactive');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('distinguishes severities by glyph rather than by label colour', async () => {
|
|
51
|
+
const {container} = await renderSummaryLine();
|
|
52
|
+
|
|
53
|
+
const tones = [...container.querySelectorAll('svg')].map((glyph) =>
|
|
54
|
+
glyph.getAttribute('class'),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
expect(tones.some((tone) => tone?.includes('text-tag-error-icon'))).toBe(true);
|
|
58
|
+
expect(tones.some((tone) => tone?.includes('text-tag-warning-icon'))).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('names only the severities that need attention', async () => {
|
|
62
|
+
await renderSummaryLine();
|
|
63
|
+
|
|
64
|
+
expect(screen.getByRole('link', {name: '2 errors'})).toBeInTheDocument();
|
|
65
|
+
expect(screen.getByRole('link', {name: '1 warning'})).toBeInTheDocument();
|
|
66
|
+
// The 2 info annotations are inside the total; naming them again would give a clean
|
|
67
|
+
// result the same weight and the same accent as a failure.
|
|
68
|
+
expect(screen.queryByRole('link', {name: '2 infos'})).not.toBeInTheDocument();
|
|
69
|
+
expect(screen.queryByText(INFO_OR_SUCCESS_PATTERN)).not.toBeInTheDocument();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('turns the total into the way out of an active severity filter', async () => {
|
|
73
|
+
await renderSummaryLine({severity: 'error'});
|
|
74
|
+
|
|
75
|
+
expect(screen.getByRole('link', {name: 'Show all 5 annotations'})).toHaveAttribute(
|
|
76
|
+
'href',
|
|
77
|
+
'/w/acme/p/project/runs/run-1?tab=annotations',
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('leaves the total as plain text when no severity filter is active', async () => {
|
|
82
|
+
await renderSummaryLine();
|
|
83
|
+
|
|
84
|
+
expect(screen.queryByRole('link', {name: SHOW_ALL_PATTERN})).not.toBeInTheDocument();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('falls back to the bare total when nothing needs attention', async () => {
|
|
88
|
+
await renderSummaryLine(
|
|
89
|
+
{},
|
|
90
|
+
{total: 3, error: 0, warning: 0, info: 3, success: 0, truncated: false},
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
expect(screen.getByText('3 annotations')).toBeInTheDocument();
|
|
94
|
+
expect(screen.queryByRole('link')).not.toBeInTheDocument();
|
|
95
|
+
});
|
|
96
|
+
|
|
33
97
|
test('clears a stale annotation when linking to a severity', async () => {
|
|
34
98
|
await renderSummaryLine({annotation: 'annotation-old'});
|
|
35
99
|
|
|
@@ -38,16 +102,26 @@ describe('RunAnnotationSummaryLine', () => {
|
|
|
38
102
|
'/w/acme/p/project/runs/run-1?tab=annotations&severity=error',
|
|
39
103
|
);
|
|
40
104
|
});
|
|
105
|
+
|
|
106
|
+
test('marks a truncated read as a lower bound', async () => {
|
|
107
|
+
await renderSummaryLine({}, {...ANNOTATION_SUMMARY, truncated: true});
|
|
108
|
+
|
|
109
|
+
expect(screen.getByText('5+ annotations')).toBeInTheDocument();
|
|
110
|
+
expect(screen.getByRole('link', {name: '2+ errors'})).toBeInTheDocument();
|
|
111
|
+
});
|
|
41
112
|
});
|
|
42
113
|
|
|
43
|
-
async function renderSummaryLine(
|
|
114
|
+
async function renderSummaryLine(
|
|
115
|
+
search: WorkflowRunsSearch = {},
|
|
116
|
+
summary: RunAnnotationSummary = ANNOTATION_SUMMARY,
|
|
117
|
+
) {
|
|
44
118
|
const rootRoute = createRootRoute({component: Outlet});
|
|
45
119
|
const runRoute = createRoute({
|
|
46
120
|
getParentRoute: () => rootRoute,
|
|
47
121
|
path: '/w/$workspaceSlug/p/$projectSlug/runs/$workflowRunId',
|
|
48
122
|
component: () => (
|
|
49
123
|
<RunAnnotationSummaryLine
|
|
50
|
-
summary={
|
|
124
|
+
summary={summary}
|
|
51
125
|
workspaceSlug="acme"
|
|
52
126
|
projectSlug="project"
|
|
53
127
|
workflowRunId="run-1"
|
|
@@ -1,13 +1,28 @@
|
|
|
1
|
+
import {Icon} from '@shipfox/react-ui/icon';
|
|
1
2
|
import {Text} from '@shipfox/react-ui/typography';
|
|
3
|
+
import {cn} from '@shipfox/react-ui/utils';
|
|
2
4
|
import {Link} from '@tanstack/react-router';
|
|
3
|
-
import type {RunAnnotationSummary} from '#core/
|
|
5
|
+
import type {RunAnnotationSummary} from '#core/run-annotation.js';
|
|
4
6
|
import {
|
|
5
|
-
WORKFLOW_RUN_ANNOTATION_SEVERITIES,
|
|
6
7
|
type WorkflowRunAnnotationSeverity,
|
|
7
8
|
type WorkflowRunsSearch,
|
|
8
9
|
workflowRunSearchParams,
|
|
9
10
|
} from '#routes/inputs.js';
|
|
10
11
|
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
|
-
|
|
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 =
|
|
30
|
-
|
|
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
51
|
<div className="flex flex-wrap items-center gap-x-8 gap-y-4 text-foreground-neutral-subtle">
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
80
|
<span key={severity} className="inline-flex shrink-0 items-center gap-8">
|
|
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 =
|
|
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="
|
|
130
|
+
return <span className="inline-flex items-center gap-4">{content}</span>;
|
|
73
131
|
}
|
|
74
132
|
|
|
75
|
-
const searchWithoutAnnotation =
|
|
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="
|
|
145
|
+
className="inline-flex items-center gap-4 rounded-4 outline-none hover:underline focus-visible:shadow-border-interactive-with-active"
|
|
89
146
|
>
|
|
90
|
-
{
|
|
147
|
+
{content}
|
|
91
148
|
</Link>
|
|
92
149
|
);
|
|
93
150
|
}
|
|
94
151
|
|
|
95
|
-
|
|
96
|
-
|
|
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,
|