@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
|
@@ -10,11 +10,13 @@ import {
|
|
|
10
10
|
SelectValue,
|
|
11
11
|
} from '@shipfox/react-ui/select';
|
|
12
12
|
import {toast} from '@shipfox/react-ui/toast';
|
|
13
|
+
import {Text} from '@shipfox/react-ui/typography';
|
|
13
14
|
import {useNavigate} from '@tanstack/react-router';
|
|
14
|
-
import {type ReactNode, useEffect} from 'react';
|
|
15
|
-
import
|
|
16
|
-
import type
|
|
15
|
+
import {type ReactNode, useEffect, useMemo} from 'react';
|
|
16
|
+
import {buildRunAnnotationList, type RunAnnotationSummary} from '#core/run-annotation.js';
|
|
17
|
+
import {isWorkflowRunTerminal, type Job, type WorkflowRunRerunMode} from '#core/workflow-run.js';
|
|
17
18
|
import {withoutWorkflowRunSelectionSearch} from '#core/workflow-run-url-state.js';
|
|
19
|
+
import {useRunAnnotationsQuery} from '#hooks/api/run-annotations.js';
|
|
18
20
|
import {
|
|
19
21
|
useCancelWorkflowRunMutation,
|
|
20
22
|
useRerunWorkflowRunMutation,
|
|
@@ -30,7 +32,7 @@ import {
|
|
|
30
32
|
import {JobGraph} from '../job-graph/index.js';
|
|
31
33
|
import type {JobGraphSelectionSource} from '../job-graph/types.js';
|
|
32
34
|
import {WorkflowRunSummary} from '../workflow-run-summary/index.js';
|
|
33
|
-
import {
|
|
35
|
+
import {RunAnnotationList, RunAnnotationSummaryLine} from '../workflow-run-tabs/index.js';
|
|
34
36
|
import {WorkflowSourceContent} from '../workflow-source-panel/index.js';
|
|
35
37
|
import {RunWorkspaceNav} from './run-workspace-nav.js';
|
|
36
38
|
import {resolveWorkflowRunSelection} from './workflow-run-selection.js';
|
|
@@ -51,7 +53,6 @@ export interface WorkflowRunViewProps {
|
|
|
51
53
|
runAttempt?: number | undefined;
|
|
52
54
|
selection?: WorkflowRunsSearch | undefined;
|
|
53
55
|
tab?: WorkflowRunTab | undefined;
|
|
54
|
-
annotationSummary?: RunAnnotationSummary | undefined;
|
|
55
56
|
activeJobId?: string | undefined;
|
|
56
57
|
jobSearch?: WorkflowJobSearch | undefined;
|
|
57
58
|
jobContent?: ReactNode | undefined;
|
|
@@ -69,7 +70,6 @@ export function WorkflowRunView({
|
|
|
69
70
|
runAttempt,
|
|
70
71
|
selection,
|
|
71
72
|
tab,
|
|
72
|
-
annotationSummary,
|
|
73
73
|
activeJobId,
|
|
74
74
|
jobSearch,
|
|
75
75
|
jobContent,
|
|
@@ -79,6 +79,22 @@ export function WorkflowRunView({
|
|
|
79
79
|
runAttempt: selection?.runAttempt ?? runAttempt,
|
|
80
80
|
});
|
|
81
81
|
const rerunMutation = useRerunWorkflowRunMutation(projectId);
|
|
82
|
+
// Annotations are read on their own cadence rather than hydrated into the run, so the two
|
|
83
|
+
// modules stay decoupled. The key is shared, so the job page's count chip costs no extra fetch.
|
|
84
|
+
//
|
|
85
|
+
// Polling is scoped to the surface that renders bodies. The read has no counts-only mode, so
|
|
86
|
+
// every poll transfers whole Markdown bodies; doing that every four seconds behind Summary,
|
|
87
|
+
// Source, and the job log spends megabytes to keep one rail number warm. Off that surface the
|
|
88
|
+
// counts still refresh on navigation and on window focus.
|
|
89
|
+
const annotationsQuery = useRunAnnotationsQuery({
|
|
90
|
+
workflowRunId,
|
|
91
|
+
runAttempt: runQuery.data?.runAttempt.attempt,
|
|
92
|
+
live:
|
|
93
|
+
runWorkspaceSection(tab) === 'annotations' &&
|
|
94
|
+
!activeJobId &&
|
|
95
|
+
Boolean(runQuery.data) &&
|
|
96
|
+
!isWorkflowRunTerminal(runQuery.data?.runAttempt.status ?? 'pending'),
|
|
97
|
+
});
|
|
82
98
|
|
|
83
99
|
return (
|
|
84
100
|
<RelativeTimeProvider>
|
|
@@ -87,11 +103,11 @@ export function WorkflowRunView({
|
|
|
87
103
|
workspaceSlug={workspaceSlug}
|
|
88
104
|
projectSlug={projectSlug}
|
|
89
105
|
query={runQuery}
|
|
106
|
+
annotations={annotationsQuery}
|
|
90
107
|
rerunMutation={rerunMutation}
|
|
91
108
|
runAttempt={runAttempt}
|
|
92
109
|
selection={selection}
|
|
93
110
|
tab={tab}
|
|
94
|
-
annotationSummary={annotationSummary}
|
|
95
111
|
activeJobId={activeJobId}
|
|
96
112
|
jobSearch={jobSearch}
|
|
97
113
|
jobContent={jobContent}
|
|
@@ -105,11 +121,11 @@ function RunViewContent({
|
|
|
105
121
|
workspaceSlug,
|
|
106
122
|
projectSlug,
|
|
107
123
|
query,
|
|
124
|
+
annotations,
|
|
108
125
|
rerunMutation,
|
|
109
126
|
runAttempt,
|
|
110
127
|
selection,
|
|
111
128
|
tab,
|
|
112
|
-
annotationSummary,
|
|
113
129
|
activeJobId,
|
|
114
130
|
jobSearch,
|
|
115
131
|
jobContent,
|
|
@@ -117,17 +133,18 @@ function RunViewContent({
|
|
|
117
133
|
workspaceSlug: string | undefined;
|
|
118
134
|
projectSlug: string | undefined;
|
|
119
135
|
query: ReturnType<typeof useWorkflowRunAttemptQuery>;
|
|
136
|
+
annotations: ReturnType<typeof useRunAnnotationsQuery>;
|
|
120
137
|
rerunMutation: ReturnType<typeof useRerunWorkflowRunMutation>;
|
|
121
138
|
runAttempt: number | undefined;
|
|
122
139
|
selection: WorkflowRunsSearch | undefined;
|
|
123
140
|
tab: WorkflowRunTab | undefined;
|
|
124
|
-
annotationSummary: RunAnnotationSummary | undefined;
|
|
125
141
|
activeJobId: string | undefined;
|
|
126
142
|
jobSearch: WorkflowJobSearch | undefined;
|
|
127
143
|
jobContent: ReactNode | undefined;
|
|
128
144
|
}) {
|
|
129
145
|
const navigate = useNavigate();
|
|
130
146
|
const runData = query.data;
|
|
147
|
+
const annotationSummary = annotations.summary;
|
|
131
148
|
const activeSection = runWorkspaceSection(tab);
|
|
132
149
|
const cancelMutation = useCancelWorkflowRunMutation(runData);
|
|
133
150
|
const sourceSnapshot = runData?.sourceSnapshot ?? null;
|
|
@@ -228,6 +245,24 @@ function RunViewContent({
|
|
|
228
245
|
});
|
|
229
246
|
}
|
|
230
247
|
|
|
248
|
+
function clearAnnotationFilters() {
|
|
249
|
+
if (!runData || !workspaceSlug || !projectSlug) return;
|
|
250
|
+
const nextSearch: WorkflowRunsSearch = {...selection, tab: 'annotations'};
|
|
251
|
+
delete nextSearch.jobId;
|
|
252
|
+
delete nextSearch.jobExecutionId;
|
|
253
|
+
delete nextSearch.stepId;
|
|
254
|
+
delete nextSearch.stepAttemptId;
|
|
255
|
+
delete nextSearch.severity;
|
|
256
|
+
delete nextSearch.annotation;
|
|
257
|
+
|
|
258
|
+
void navigate({
|
|
259
|
+
to: '/w/$workspaceSlug/p/$projectSlug/runs/$workflowRunId',
|
|
260
|
+
params: {workspaceSlug, projectSlug, workflowRunId: runData.id},
|
|
261
|
+
search: workflowRunSearchParams(nextSearch, nextSearch) as never,
|
|
262
|
+
replace: true,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
231
266
|
function cancelRun() {
|
|
232
267
|
cancelMutation.mutate(undefined, {
|
|
233
268
|
onError: (error) => toast.error(cancelErrorMessage(error)),
|
|
@@ -291,6 +326,7 @@ function RunViewContent({
|
|
|
291
326
|
<RunSectionContent
|
|
292
327
|
section={activeSection}
|
|
293
328
|
run={runData}
|
|
329
|
+
annotations={annotations}
|
|
294
330
|
annotationSummary={annotationSummary}
|
|
295
331
|
workspaceSlug={workspaceSlug}
|
|
296
332
|
projectSlug={projectSlug}
|
|
@@ -298,6 +334,7 @@ function RunViewContent({
|
|
|
298
334
|
selectedJobId={selection?.jobId}
|
|
299
335
|
onSelectGraphJob={selectGraphJob}
|
|
300
336
|
onSelectAnnotationJob={selectAnnotationJob}
|
|
337
|
+
onClearAnnotationFilters={clearAnnotationFilters}
|
|
301
338
|
sourceSnapshot={sourceSnapshot}
|
|
302
339
|
highlightedLineRange={highlightedLineRange}
|
|
303
340
|
/>
|
|
@@ -313,6 +350,7 @@ function RunViewContent({
|
|
|
313
350
|
function RunSectionContent({
|
|
314
351
|
section,
|
|
315
352
|
run,
|
|
353
|
+
annotations,
|
|
316
354
|
annotationSummary,
|
|
317
355
|
workspaceSlug,
|
|
318
356
|
projectSlug,
|
|
@@ -320,11 +358,13 @@ function RunSectionContent({
|
|
|
320
358
|
selectedJobId,
|
|
321
359
|
onSelectGraphJob,
|
|
322
360
|
onSelectAnnotationJob,
|
|
361
|
+
onClearAnnotationFilters,
|
|
323
362
|
sourceSnapshot,
|
|
324
363
|
highlightedLineRange,
|
|
325
364
|
}: {
|
|
326
365
|
section: RunWorkspaceSection;
|
|
327
366
|
run: NonNullable<ReturnType<typeof useWorkflowRunAttemptQuery>['data']>;
|
|
367
|
+
annotations: ReturnType<typeof useRunAnnotationsQuery>;
|
|
328
368
|
annotationSummary: RunAnnotationSummary | undefined;
|
|
329
369
|
workspaceSlug: string | undefined;
|
|
330
370
|
projectSlug: string | undefined;
|
|
@@ -332,6 +372,7 @@ function RunSectionContent({
|
|
|
332
372
|
selectedJobId: string | undefined;
|
|
333
373
|
onSelectGraphJob: (jobId: string | undefined, source?: JobGraphSelectionSource) => void;
|
|
334
374
|
onSelectAnnotationJob: (jobId: string | undefined) => void;
|
|
375
|
+
onClearAnnotationFilters: () => void;
|
|
335
376
|
sourceSnapshot: NonNullable<
|
|
336
377
|
ReturnType<typeof useWorkflowRunAttemptQuery>['data']
|
|
337
378
|
>['sourceSnapshot'];
|
|
@@ -343,6 +384,9 @@ function RunSectionContent({
|
|
|
343
384
|
return (
|
|
344
385
|
<section aria-label="All jobs summary" className="min-h-0 flex-1 overflow-auto pb-24 pt-16">
|
|
345
386
|
<div className="mx-auto flex w-full max-w-[1120px] flex-col gap-16 px-24">
|
|
387
|
+
<Text as="h2" className="sr-only">
|
|
388
|
+
All jobs summary
|
|
389
|
+
</Text>
|
|
346
390
|
<JobGraph
|
|
347
391
|
run={run}
|
|
348
392
|
selectedJobId={selectedJobId}
|
|
@@ -355,33 +399,27 @@ function RunSectionContent({
|
|
|
355
399
|
}
|
|
356
400
|
|
|
357
401
|
if (section === 'annotations') {
|
|
358
|
-
const selectedJob = run.jobs.find((job) => job.id === selectedJobId);
|
|
359
402
|
return (
|
|
360
|
-
<
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
jobs={run.jobs}
|
|
372
|
-
selectedJobId={selectedJob?.id}
|
|
373
|
-
onSelect={onSelectAnnotationJob}
|
|
374
|
-
/>
|
|
375
|
-
</div>
|
|
376
|
-
<RunAnnotationsEmpty jobName={selectedJob?.displayName} />
|
|
377
|
-
</div>
|
|
378
|
-
</section>
|
|
403
|
+
<RunAnnotationsSection
|
|
404
|
+
run={run}
|
|
405
|
+
annotations={annotations}
|
|
406
|
+
annotationSummary={annotationSummary}
|
|
407
|
+
workspaceSlug={workspaceSlug}
|
|
408
|
+
projectSlug={projectSlug}
|
|
409
|
+
selection={selection}
|
|
410
|
+
selectedJobId={selectedJobId}
|
|
411
|
+
onSelectAnnotationJob={onSelectAnnotationJob}
|
|
412
|
+
onClearAnnotationFilters={onClearAnnotationFilters}
|
|
413
|
+
/>
|
|
379
414
|
);
|
|
380
415
|
}
|
|
381
416
|
|
|
382
417
|
return (
|
|
383
418
|
<section aria-label="Workflow source" className="min-h-0 flex-1 overflow-auto pb-24 pt-16">
|
|
384
|
-
<div className="mx-auto flex min-h-full w-full max-w-[1120px] px-24">
|
|
419
|
+
<div className="mx-auto flex min-h-full w-full max-w-[1120px] flex-col px-24">
|
|
420
|
+
<Text as="h2" className="sr-only">
|
|
421
|
+
Workflow source
|
|
422
|
+
</Text>
|
|
385
423
|
{sourceSnapshot ? (
|
|
386
424
|
<WorkflowSourceContent
|
|
387
425
|
source={sourceSnapshot}
|
|
@@ -405,6 +443,86 @@ function RunSectionContent({
|
|
|
405
443
|
);
|
|
406
444
|
}
|
|
407
445
|
|
|
446
|
+
function RunAnnotationsSection({
|
|
447
|
+
run,
|
|
448
|
+
annotations,
|
|
449
|
+
annotationSummary,
|
|
450
|
+
workspaceSlug,
|
|
451
|
+
projectSlug,
|
|
452
|
+
selection,
|
|
453
|
+
selectedJobId,
|
|
454
|
+
onSelectAnnotationJob,
|
|
455
|
+
onClearAnnotationFilters,
|
|
456
|
+
}: {
|
|
457
|
+
run: NonNullable<ReturnType<typeof useWorkflowRunAttemptQuery>['data']>;
|
|
458
|
+
annotations: ReturnType<typeof useRunAnnotationsQuery>;
|
|
459
|
+
annotationSummary: RunAnnotationSummary | undefined;
|
|
460
|
+
workspaceSlug: string | undefined;
|
|
461
|
+
projectSlug: string | undefined;
|
|
462
|
+
selection: WorkflowRunsSearch | undefined;
|
|
463
|
+
selectedJobId: string | undefined;
|
|
464
|
+
onSelectAnnotationJob: (jobId: string | undefined) => void;
|
|
465
|
+
onClearAnnotationFilters: () => void;
|
|
466
|
+
}) {
|
|
467
|
+
const selectedJob = run.jobs.find((job) => job.id === selectedJobId);
|
|
468
|
+
const severity = selection?.severity;
|
|
469
|
+
const records = annotations.annotations;
|
|
470
|
+
|
|
471
|
+
const entries = useMemo(
|
|
472
|
+
() =>
|
|
473
|
+
records
|
|
474
|
+
? buildRunAnnotationList({
|
|
475
|
+
annotations: records,
|
|
476
|
+
jobs: run.jobs,
|
|
477
|
+
severity,
|
|
478
|
+
jobId: selectedJob?.id,
|
|
479
|
+
})
|
|
480
|
+
: undefined,
|
|
481
|
+
[records, run.jobs, selectedJob?.id, severity],
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
return (
|
|
485
|
+
<section aria-label="Run annotations" className="min-h-0 flex-1 overflow-auto pb-24 pt-16">
|
|
486
|
+
<div className="mx-auto flex w-full max-w-[1120px] flex-col gap-16 px-24">
|
|
487
|
+
<Text as="h2" className="sr-only">
|
|
488
|
+
Annotations
|
|
489
|
+
</Text>
|
|
490
|
+
<div className="flex flex-wrap items-center justify-between gap-8">
|
|
491
|
+
<RunAnnotationSummaryLine
|
|
492
|
+
summary={annotationSummary}
|
|
493
|
+
workspaceSlug={workspaceSlug}
|
|
494
|
+
projectSlug={projectSlug}
|
|
495
|
+
workflowRunId={run.id}
|
|
496
|
+
search={selection}
|
|
497
|
+
/>
|
|
498
|
+
<AnnotationJobFilter
|
|
499
|
+
jobs={run.jobs}
|
|
500
|
+
selectedJobId={selectedJob?.id}
|
|
501
|
+
onSelect={onSelectAnnotationJob}
|
|
502
|
+
/>
|
|
503
|
+
</div>
|
|
504
|
+
<RunAnnotationList
|
|
505
|
+
// Remounting on a filter or run-attempt change resets the render window, so the next
|
|
506
|
+
// list never inherits a "show more" position from different data.
|
|
507
|
+
key={`${run.id}:${run.runAttempt.attempt}:${severity ?? 'all'}:${selectedJob?.id ?? 'all'}`}
|
|
508
|
+
query={annotations.query}
|
|
509
|
+
entries={entries}
|
|
510
|
+
workspaceSlug={workspaceSlug}
|
|
511
|
+
projectSlug={projectSlug}
|
|
512
|
+
workflowRunId={run.id}
|
|
513
|
+
runAttempt={run.runAttempt.attempt}
|
|
514
|
+
// A run with no annotations at all offers no filter to clear, whatever the URL says.
|
|
515
|
+
filtered={Boolean((severity || selectedJob) && (annotationSummary?.total ?? 0) > 0)}
|
|
516
|
+
filteredJobName={selectedJob?.displayName}
|
|
517
|
+
filteredSeverity={severity}
|
|
518
|
+
onClearFilters={onClearAnnotationFilters}
|
|
519
|
+
selectedAnnotationId={selection?.annotation}
|
|
520
|
+
/>
|
|
521
|
+
</div>
|
|
522
|
+
</section>
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
|
|
408
526
|
const ALL_ANNOTATION_JOBS = 'all-jobs';
|
|
409
527
|
|
|
410
528
|
function AnnotationJobFilter({
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import {
|
|
2
|
+
workflowJob,
|
|
3
|
+
workflowJobExecutionDto,
|
|
4
|
+
workflowStepAttemptDto,
|
|
5
|
+
workflowStepDto,
|
|
6
|
+
} from '#test/fixtures/workflow-run.js';
|
|
7
|
+
import {
|
|
8
|
+
buildRunAnnotationList,
|
|
9
|
+
highestRunAnnotationSeverity,
|
|
10
|
+
type RunAnnotationRecord,
|
|
11
|
+
type RunAnnotationStyle,
|
|
12
|
+
summarizeJobAnnotations,
|
|
13
|
+
summarizeRunAnnotations,
|
|
14
|
+
} from './run-annotation.js';
|
|
15
|
+
import type {Job} from './workflow-run.js';
|
|
16
|
+
|
|
17
|
+
const BUILD_JOB_ID = '44444444-4444-4444-8444-00000000000b';
|
|
18
|
+
const TEST_JOB_ID = '44444444-4444-4444-8444-00000000000c';
|
|
19
|
+
const BUILD_EXECUTION_ID = '77777777-7777-4777-8777-00000000000b';
|
|
20
|
+
const TEST_EXECUTION_ONE_ID = '77777777-7777-4777-8777-00000000000c';
|
|
21
|
+
const TEST_EXECUTION_TWO_ID = '77777777-7777-4777-8777-00000000000d';
|
|
22
|
+
const BUILD_STEP_ID = '55555555-5555-4555-8555-00000000000b';
|
|
23
|
+
const TEST_STEP_ID = '55555555-5555-4555-8555-00000000000c';
|
|
24
|
+
const BUILD_ATTEMPT_ID = '66666666-6666-4666-8666-00000000000b';
|
|
25
|
+
|
|
26
|
+
describe('summarizeRunAnnotations', () => {
|
|
27
|
+
test('counts every annotation in the total and only severities per severity', () => {
|
|
28
|
+
const annotations = [
|
|
29
|
+
annotation({id: 'a', style: 'error'}),
|
|
30
|
+
annotation({id: 'b', style: 'warning'}),
|
|
31
|
+
annotation({id: 'c', style: 'default'}),
|
|
32
|
+
annotation({id: 'd', style: 'error'}),
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const summary = summarizeRunAnnotations(annotations);
|
|
36
|
+
|
|
37
|
+
expect(summary).toEqual({
|
|
38
|
+
total: 4,
|
|
39
|
+
error: 2,
|
|
40
|
+
warning: 1,
|
|
41
|
+
info: 0,
|
|
42
|
+
success: 0,
|
|
43
|
+
truncated: false,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('carries truncation so counts read as a lower bound', () => {
|
|
48
|
+
const summary = summarizeRunAnnotations([annotation({id: 'a', style: 'info'})], {
|
|
49
|
+
truncated: true,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(summary.truncated).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('summarizeJobAnnotations', () => {
|
|
57
|
+
test('counts only the annotations owned by one job', () => {
|
|
58
|
+
const annotations = [
|
|
59
|
+
annotation({id: 'a', style: 'error', jobId: BUILD_JOB_ID}),
|
|
60
|
+
annotation({id: 'b', style: 'warning', jobId: TEST_JOB_ID}),
|
|
61
|
+
annotation({id: 'c', style: 'info', jobId: TEST_JOB_ID}),
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const summary = summarizeJobAnnotations(annotations, TEST_JOB_ID);
|
|
65
|
+
|
|
66
|
+
expect(summary.total).toBe(2);
|
|
67
|
+
expect(summary.error).toBe(0);
|
|
68
|
+
expect(summary.warning).toBe(1);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('highestRunAnnotationSeverity', () => {
|
|
73
|
+
test('reports the loudest severity present', () => {
|
|
74
|
+
const summary = summarizeRunAnnotations([
|
|
75
|
+
annotation({id: 'a', style: 'success'}),
|
|
76
|
+
annotation({id: 'b', style: 'warning'}),
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
expect(highestRunAnnotationSeverity(summary)).toBe('warning');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('reports nothing when only unstyled annotations exist', () => {
|
|
83
|
+
const summary = summarizeRunAnnotations([annotation({id: 'a', style: 'default'})]);
|
|
84
|
+
|
|
85
|
+
expect(highestRunAnnotationSeverity(summary)).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('buildRunAnnotationList', () => {
|
|
90
|
+
test('ranks by severity before emission order', () => {
|
|
91
|
+
const annotations = [
|
|
92
|
+
annotation({id: 'a', style: 'success', sequence: 1}),
|
|
93
|
+
annotation({id: 'b', style: 'error', sequence: 2}),
|
|
94
|
+
annotation({id: 'c', style: 'default', sequence: 3}),
|
|
95
|
+
annotation({id: 'd', style: 'warning', sequence: 4}),
|
|
96
|
+
annotation({id: 'e', style: 'info', sequence: 5}),
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
const entries = buildRunAnnotationList({annotations, jobs: jobs()});
|
|
100
|
+
|
|
101
|
+
expect(entries.map((entry) => entry.annotation.id)).toEqual(['b', 'd', 'e', 'a', 'c']);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('breaks a severity tie by job position before sequence', () => {
|
|
105
|
+
// `sequence` restarts per execution, so the later job's lower sequence must not lead.
|
|
106
|
+
const annotations = [
|
|
107
|
+
annotation({id: 'late-job', style: 'error', sequence: 1, jobId: TEST_JOB_ID}),
|
|
108
|
+
annotation({id: 'early-job', style: 'error', sequence: 9, jobId: BUILD_JOB_ID}),
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
const entries = buildRunAnnotationList({annotations, jobs: jobs()});
|
|
112
|
+
|
|
113
|
+
expect(entries.map((entry) => entry.annotation.id)).toEqual(['early-job', 'late-job']);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('breaks a job tie by execution sequence', () => {
|
|
117
|
+
const annotations = [
|
|
118
|
+
annotation({
|
|
119
|
+
id: 'second-execution',
|
|
120
|
+
style: 'error',
|
|
121
|
+
sequence: 1,
|
|
122
|
+
jobId: TEST_JOB_ID,
|
|
123
|
+
jobExecutionId: TEST_EXECUTION_TWO_ID,
|
|
124
|
+
}),
|
|
125
|
+
annotation({
|
|
126
|
+
id: 'first-execution',
|
|
127
|
+
style: 'error',
|
|
128
|
+
sequence: 2,
|
|
129
|
+
jobId: TEST_JOB_ID,
|
|
130
|
+
jobExecutionId: TEST_EXECUTION_ONE_ID,
|
|
131
|
+
}),
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
const entries = buildRunAnnotationList({annotations, jobs: jobs()});
|
|
135
|
+
|
|
136
|
+
expect(entries.map((entry) => entry.annotation.id)).toEqual([
|
|
137
|
+
'first-execution',
|
|
138
|
+
'second-execution',
|
|
139
|
+
]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('resolves provenance and an origin the run can route back to', () => {
|
|
143
|
+
const annotations = [annotation({id: 'a', style: 'error'})];
|
|
144
|
+
|
|
145
|
+
const [entry] = buildRunAnnotationList({annotations, jobs: jobs()});
|
|
146
|
+
|
|
147
|
+
expect(entry?.jobName).toBe('build');
|
|
148
|
+
expect(entry?.stepLabel).toBe('compile');
|
|
149
|
+
expect(entry?.attemptLabel).toBe('attempt 1');
|
|
150
|
+
expect(entry?.origin).toEqual({
|
|
151
|
+
jobId: BUILD_JOB_ID,
|
|
152
|
+
jobExecutionId: BUILD_EXECUTION_ID,
|
|
153
|
+
stepId: BUILD_STEP_ID,
|
|
154
|
+
stepAttemptId: BUILD_ATTEMPT_ID,
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('names an execution only when the job ran more than once', () => {
|
|
159
|
+
const annotations = [
|
|
160
|
+
annotation({id: 'single', style: 'info'}),
|
|
161
|
+
annotation({
|
|
162
|
+
id: 'multi',
|
|
163
|
+
style: 'info',
|
|
164
|
+
jobId: TEST_JOB_ID,
|
|
165
|
+
jobExecutionId: TEST_EXECUTION_TWO_ID,
|
|
166
|
+
originStepId: TEST_STEP_ID,
|
|
167
|
+
}),
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
const entries = buildRunAnnotationList({annotations, jobs: jobs()});
|
|
171
|
+
const byId = new Map(entries.map((entry) => [entry.annotation.id, entry]));
|
|
172
|
+
|
|
173
|
+
expect(byId.get('single')?.executionLabel).toBeNull();
|
|
174
|
+
expect(byId.get('multi')?.executionLabel).toBe('execution #2');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('keeps an annotation whose step the run attempt no longer holds, without an origin', () => {
|
|
178
|
+
const annotations = [
|
|
179
|
+
annotation({
|
|
180
|
+
id: 'orphan',
|
|
181
|
+
style: 'error',
|
|
182
|
+
originStepId: '55555555-5555-4555-8555-0000000000ff',
|
|
183
|
+
}),
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
const [entry] = buildRunAnnotationList({annotations, jobs: jobs()});
|
|
187
|
+
|
|
188
|
+
expect(entry?.annotation.id).toBe('orphan');
|
|
189
|
+
expect(entry?.jobName).toBe('build');
|
|
190
|
+
expect(entry?.stepLabel).toBeNull();
|
|
191
|
+
expect(entry?.origin).toBeNull();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('sorts an annotation from an unknown job last rather than first', () => {
|
|
195
|
+
const annotations = [
|
|
196
|
+
annotation({
|
|
197
|
+
id: 'unknown-job',
|
|
198
|
+
style: 'error',
|
|
199
|
+
jobId: '44444444-4444-4444-8444-0000000000ff',
|
|
200
|
+
}),
|
|
201
|
+
annotation({id: 'known-job', style: 'error'}),
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
const entries = buildRunAnnotationList({annotations, jobs: jobs()});
|
|
205
|
+
|
|
206
|
+
expect(entries.map((entry) => entry.annotation.id)).toEqual(['known-job', 'unknown-job']);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('filters by severity and by job', () => {
|
|
210
|
+
const annotations = [
|
|
211
|
+
annotation({id: 'build-error', style: 'error'}),
|
|
212
|
+
annotation({id: 'build-info', style: 'info'}),
|
|
213
|
+
annotation({id: 'test-error', style: 'error', jobId: TEST_JOB_ID}),
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
expect(
|
|
217
|
+
buildRunAnnotationList({annotations, jobs: jobs(), severity: 'error'}).map(
|
|
218
|
+
(entry) => entry.annotation.id,
|
|
219
|
+
),
|
|
220
|
+
).toEqual(['build-error', 'test-error']);
|
|
221
|
+
expect(
|
|
222
|
+
buildRunAnnotationList({annotations, jobs: jobs(), jobId: BUILD_JOB_ID}).map(
|
|
223
|
+
(entry) => entry.annotation.id,
|
|
224
|
+
),
|
|
225
|
+
).toEqual(['build-error', 'build-info']);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('never selects an unstyled annotation with a severity filter', () => {
|
|
229
|
+
const annotations = [annotation({id: 'plain', style: 'default'})];
|
|
230
|
+
|
|
231
|
+
expect(buildRunAnnotationList({annotations, jobs: jobs(), severity: 'info'})).toEqual([]);
|
|
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: `context-${overrides.id}`,
|
|
244
|
+
sequence: 1,
|
|
245
|
+
body: 'Body',
|
|
246
|
+
...overrides,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function jobs(): Job[] {
|
|
251
|
+
return [
|
|
252
|
+
workflowJob({
|
|
253
|
+
id: BUILD_JOB_ID,
|
|
254
|
+
key: 'build',
|
|
255
|
+
name: 'build',
|
|
256
|
+
position: 0,
|
|
257
|
+
job_executions: [
|
|
258
|
+
workflowJobExecutionDto({
|
|
259
|
+
id: BUILD_EXECUTION_ID,
|
|
260
|
+
job_id: BUILD_JOB_ID,
|
|
261
|
+
sequence: 1,
|
|
262
|
+
steps: [
|
|
263
|
+
workflowStepDto({
|
|
264
|
+
id: BUILD_STEP_ID,
|
|
265
|
+
job_execution_id: BUILD_EXECUTION_ID,
|
|
266
|
+
name: 'compile',
|
|
267
|
+
attempts: [
|
|
268
|
+
workflowStepAttemptDto({
|
|
269
|
+
id: BUILD_ATTEMPT_ID,
|
|
270
|
+
step_id: BUILD_STEP_ID,
|
|
271
|
+
attempt: 1,
|
|
272
|
+
status: 'succeeded',
|
|
273
|
+
}),
|
|
274
|
+
],
|
|
275
|
+
}),
|
|
276
|
+
],
|
|
277
|
+
}),
|
|
278
|
+
],
|
|
279
|
+
}),
|
|
280
|
+
workflowJob({
|
|
281
|
+
id: TEST_JOB_ID,
|
|
282
|
+
key: 'test',
|
|
283
|
+
name: 'test',
|
|
284
|
+
position: 1,
|
|
285
|
+
job_executions: [
|
|
286
|
+
workflowJobExecutionDto({
|
|
287
|
+
id: TEST_EXECUTION_ONE_ID,
|
|
288
|
+
job_id: TEST_JOB_ID,
|
|
289
|
+
sequence: 1,
|
|
290
|
+
steps: [
|
|
291
|
+
workflowStepDto({
|
|
292
|
+
id: TEST_STEP_ID,
|
|
293
|
+
job_execution_id: TEST_EXECUTION_ONE_ID,
|
|
294
|
+
name: 'run tests',
|
|
295
|
+
attempts: [],
|
|
296
|
+
}),
|
|
297
|
+
],
|
|
298
|
+
}),
|
|
299
|
+
workflowJobExecutionDto({
|
|
300
|
+
id: TEST_EXECUTION_TWO_ID,
|
|
301
|
+
job_id: TEST_JOB_ID,
|
|
302
|
+
sequence: 2,
|
|
303
|
+
steps: [
|
|
304
|
+
workflowStepDto({
|
|
305
|
+
id: TEST_STEP_ID,
|
|
306
|
+
job_execution_id: TEST_EXECUTION_TWO_ID,
|
|
307
|
+
name: 'run tests',
|
|
308
|
+
attempts: [],
|
|
309
|
+
}),
|
|
310
|
+
],
|
|
311
|
+
}),
|
|
312
|
+
],
|
|
313
|
+
}),
|
|
314
|
+
];
|
|
315
|
+
}
|