@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.
- package/.storybook/preview.css +4 -0
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +41 -0
- package/dist/components/identifier/identifier.js +2 -2
- package/dist/components/identifier/identifier.js.map +1 -1
- package/dist/components/job-detail/agent-config-failure-callout.d.ts.map +1 -1
- package/dist/components/job-detail/agent-config-failure-callout.js +1 -1
- package/dist/components/job-detail/agent-config-failure-callout.js.map +1 -1
- package/dist/components/job-detail/job-context-panel.d.ts +6 -0
- package/dist/components/job-detail/job-context-panel.d.ts.map +1 -0
- package/dist/components/job-detail/job-context-panel.js +224 -0
- package/dist/components/job-detail/job-context-panel.js.map +1 -0
- package/dist/components/job-detail/job-detail-header.d.ts +10 -1
- package/dist/components/job-detail/job-detail-header.d.ts.map +1 -1
- package/dist/components/job-detail/job-detail-header.js +58 -39
- package/dist/components/job-detail/job-detail-header.js.map +1 -1
- package/dist/components/job-detail/job-detail-view.d.ts +1 -1
- package/dist/components/job-detail/job-detail-view.d.ts.map +1 -1
- package/dist/components/job-detail/job-detail-view.js +92 -41
- package/dist/components/job-detail/job-detail-view.js.map +1 -1
- package/dist/components/job-detail/job-empty-states.d.ts.map +1 -1
- package/dist/components/job-detail/job-empty-states.js +19 -10
- package/dist/components/job-detail/job-empty-states.js.map +1 -1
- package/dist/components/job-detail/job-execution-switcher.js +4 -4
- package/dist/components/job-detail/job-execution-switcher.js.map +1 -1
- package/dist/components/job-detail/json-code.d.ts +14 -0
- package/dist/components/job-detail/json-code.d.ts.map +1 -0
- package/dist/components/job-detail/json-code.js +93 -0
- package/dist/components/job-detail/json-code.js.map +1 -0
- package/dist/components/job-detail/step-attempt-log-panel.d.ts.map +1 -1
- package/dist/components/job-detail/step-attempt-log-panel.js +9 -6
- package/dist/components/job-detail/step-attempt-log-panel.js.map +1 -1
- package/dist/components/job-detail/step-troubleshooting.d.ts +18 -0
- package/dist/components/job-detail/step-troubleshooting.d.ts.map +1 -0
- package/dist/components/job-detail/step-troubleshooting.js +493 -0
- package/dist/components/job-detail/step-troubleshooting.js.map +1 -0
- package/dist/components/job-graph/job-graph-content.d.ts.map +1 -1
- package/dist/components/job-graph/job-graph-content.js +5 -4
- package/dist/components/job-graph/job-graph-content.js.map +1 -1
- package/dist/components/job-graph/job-node.js +3 -3
- package/dist/components/job-graph/job-node.js.map +1 -1
- package/dist/components/step-list/index.d.ts +1 -0
- package/dist/components/step-list/index.d.ts.map +1 -1
- package/dist/components/step-list/index.js.map +1 -1
- package/dist/components/step-list/step-list-model.d.ts.map +1 -1
- package/dist/components/step-list/step-list-model.js +5 -0
- package/dist/components/step-list/step-list-model.js.map +1 -1
- package/dist/components/step-list/step-list.d.ts +5 -1
- package/dist/components/step-list/step-list.d.ts.map +1 -1
- package/dist/components/step-list/step-list.js +117 -82
- package/dist/components/step-list/step-list.js.map +1 -1
- package/dist/components/workflow-run-duration-label.js +1 -1
- package/dist/components/workflow-run-duration-label.js.map +1 -1
- package/dist/components/workflow-run-list/job-status-strip.js +6 -6
- package/dist/components/workflow-run-list/job-status-strip.js.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-filter-menu.js +1 -1
- package/dist/components/workflow-run-list/workflow-run-filter-menu.js.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-filters.js +4 -4
- package/dist/components/workflow-run-list/workflow-run-filters.js.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-list-states.js +6 -6
- package/dist/components/workflow-run-list/workflow-run-list-states.js.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-list-view.js +1 -1
- package/dist/components/workflow-run-list/workflow-run-list-view.js.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-row.js +7 -7
- package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
- package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js +1 -1
- package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js.map +1 -1
- package/dist/components/workflow-run-summary/workflow-run-summary.d.ts +0 -1
- package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
- package/dist/components/workflow-run-summary/workflow-run-summary.js +9 -13
- 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 +39 -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 +211 -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 +90 -25
- 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 +16 -10
- package/dist/components/workflow-run-view/run-workspace-nav.js.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-states.js +6 -6
- package/dist/components/workflow-run-view/workflow-run-states.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 +217 -59
- package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
- package/dist/core/entities/job-execution.d.ts +7 -0
- package/dist/core/entities/job-execution.d.ts.map +1 -1
- package/dist/core/entities/job-execution.js.map +1 -1
- package/dist/core/entities/job.d.ts +9 -0
- package/dist/core/entities/job.d.ts.map +1 -1
- package/dist/core/entities/job.js.map +1 -1
- package/dist/core/entities/step-attempt.d.ts +29 -0
- package/dist/core/entities/step-attempt.d.ts.map +1 -1
- package/dist/core/entities/step-attempt.js.map +1 -1
- package/dist/core/entities/step.d.ts +3 -1
- package/dist/core/entities/step.d.ts.map +1 -1
- package/dist/core/entities/step.js.map +1 -1
- package/dist/core/run-annotation.d.ts +79 -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/core/workflow-run.d.ts +1 -1
- package/dist/core/workflow-run.d.ts.map +1 -1
- package/dist/core/workflow-run.js.map +1 -1
- package/dist/hooks/api/annotations.d.ts +63 -0
- package/dist/hooks/api/annotations.d.ts.map +1 -0
- package/dist/hooks/api/annotations.js +145 -0
- package/dist/hooks/api/annotations.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/hooks/api/step-attempt-detail.d.ts +14 -0
- package/dist/hooks/api/step-attempt-detail.d.ts.map +1 -0
- package/dist/hooks/api/step-attempt-detail.js +43 -0
- package/dist/hooks/api/step-attempt-detail.js.map +1 -0
- package/dist/hooks/api/workflow-run-mapper.d.ts +9 -2
- package/dist/hooks/api/workflow-run-mapper.d.ts.map +1 -1
- package/dist/hooks/api/workflow-run-mapper.js +47 -0
- package/dist/hooks/api/workflow-run-mapper.js.map +1 -1
- package/dist/pages/project-workflows-page.js +19 -19
- package/dist/pages/project-workflows-page.js.map +1 -1
- package/dist/pages/workflow-run-list-page.js +1 -1
- package/dist/pages/workflow-run-list-page.js.map +1 -1
- 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/identifier/identifier.tsx +2 -2
- package/src/components/job-detail/agent-config-failure-callout.tsx +5 -1
- package/src/components/job-detail/job-context-panel.test.tsx +72 -0
- package/src/components/job-detail/job-context-panel.tsx +237 -0
- package/src/components/job-detail/job-detail-header.test.tsx +3 -0
- package/src/components/job-detail/job-detail-header.tsx +65 -34
- package/src/components/job-detail/job-detail-view.stories.tsx +555 -48
- package/src/components/job-detail/job-detail-view.tsx +101 -42
- package/src/components/job-detail/job-empty-states.tsx +23 -10
- package/src/components/job-detail/job-execution-switcher.tsx +5 -5
- package/src/components/job-detail/json-code.tsx +120 -0
- package/src/components/job-detail/step-attempt-log-panel.test.tsx +1 -0
- package/src/components/job-detail/step-attempt-log-panel.tsx +17 -6
- package/src/components/job-detail/step-troubleshooting.stories.tsx +120 -0
- package/src/components/job-detail/step-troubleshooting.test.tsx +337 -0
- package/src/components/job-detail/step-troubleshooting.tsx +537 -0
- package/src/components/job-graph/job-graph-content.tsx +4 -3
- package/src/components/job-graph/job-node.tsx +3 -3
- package/src/components/step-list/index.ts +1 -0
- package/src/components/step-list/step-list-model.ts +5 -0
- package/src/components/step-list/step-list.test.tsx +75 -2
- package/src/components/step-list/step-list.tsx +73 -20
- package/src/components/workflow-run-duration-label.tsx +1 -1
- package/src/components/workflow-run-list/job-status-strip.tsx +6 -6
- package/src/components/workflow-run-list/workflow-run-filter-menu.tsx +1 -1
- package/src/components/workflow-run-list/workflow-run-filters.tsx +4 -4
- package/src/components/workflow-run-list/workflow-run-list-states.tsx +6 -6
- package/src/components/workflow-run-list/workflow-run-list-view.tsx +1 -1
- package/src/components/workflow-run-list/workflow-run-row.tsx +7 -7
- package/src/components/workflow-run-summary/workflow-run-attempt-switcher.tsx +1 -1
- package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +1 -1
- package/src/components/workflow-run-summary/workflow-run-summary.tsx +12 -12
- package/src/components/workflow-run-tabs/index.ts +7 -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 +273 -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 +95 -20
- 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 +22 -9
- package/src/components/workflow-run-view/workflow-run-states.tsx +6 -6
- package/src/components/workflow-run-view/workflow-run-view.test.tsx +246 -20
- package/src/components/workflow-run-view/workflow-run-view.tsx +255 -37
- package/src/core/entities/job-execution.ts +7 -0
- package/src/core/entities/job.ts +9 -0
- package/src/core/entities/step-attempt.ts +33 -0
- package/src/core/entities/step.ts +3 -1
- package/src/core/run-annotation.test.ts +315 -0
- package/src/core/run-annotation.ts +244 -0
- package/src/core/workflow-run.ts +4 -0
- package/src/hooks/api/annotations.test.ts +84 -0
- package/src/hooks/api/annotations.ts +240 -0
- package/src/hooks/api/run-annotation-mapper.ts +30 -0
- package/src/hooks/api/run-annotations.ts +137 -0
- package/src/hooks/api/step-attempt-detail.ts +67 -0
- package/src/hooks/api/workflow-run-mapper.ts +46 -0
- package/src/hooks/api/workflow-runs.test.tsx +61 -1
- package/src/pages/project-workflows-page.tsx +19 -19
- package/src/pages/workflow-job-detail-page.test.tsx +118 -1
- package/src/pages/workflow-run-list-page.tsx +1 -1
- package/src/routes/inputs.ts +7 -2
- package/test/fixtures/workflow-run.ts +7 -0
- 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
|
@@ -1,2 +1,9 @@
|
|
|
1
|
+
export {RunAnnotationCountChip} from './run-annotation-count-chip.js';
|
|
2
|
+
export {annotationElementId, RunAnnotationItem} from './run-annotation-item.js';
|
|
3
|
+
export {
|
|
4
|
+
type DerivedRunAnnotation,
|
|
5
|
+
RunAnnotationList,
|
|
6
|
+
type RunAnnotationListQuery,
|
|
7
|
+
} from './run-annotation-list.js';
|
|
1
8
|
export {RunAnnotationSummaryLine} from './run-annotation-summary-line.js';
|
|
2
9
|
export {RunAnnotationsEmpty} from './run-annotations-empty.js';
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {Icon} from '@shipfox/react-ui/icon';
|
|
2
|
+
import {cn} from '@shipfox/react-ui/utils';
|
|
3
|
+
import {Link} from '@tanstack/react-router';
|
|
4
|
+
import {highestRunAnnotationSeverity, type RunAnnotationSummary} from '#core/run-annotation.js';
|
|
5
|
+
import {type WorkflowRunsSearch, workflowRunSearchParams} from '#routes/inputs.js';
|
|
6
|
+
import {SEVERITY_CHIP_TONE, SEVERITY_ICON} from './severity-visuals.js';
|
|
7
|
+
|
|
8
|
+
export interface RunAnnotationCountChipProps {
|
|
9
|
+
summary: RunAnnotationSummary | undefined;
|
|
10
|
+
workspaceSlug: string;
|
|
11
|
+
projectSlug: string;
|
|
12
|
+
workflowRunId: string;
|
|
13
|
+
runAttempt?: number | undefined;
|
|
14
|
+
/** Scopes the destination list to one job. */
|
|
15
|
+
jobId?: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A bounded reference into the run's Annotations section: a count, its highest severity, and one
|
|
20
|
+
* link. Never a body, so an annotation is rendered exactly once per page.
|
|
21
|
+
*/
|
|
22
|
+
export function RunAnnotationCountChip({
|
|
23
|
+
summary,
|
|
24
|
+
workspaceSlug,
|
|
25
|
+
projectSlug,
|
|
26
|
+
workflowRunId,
|
|
27
|
+
runAttempt,
|
|
28
|
+
jobId,
|
|
29
|
+
}: RunAnnotationCountChipProps) {
|
|
30
|
+
if (!summary || summary.total === 0) return null;
|
|
31
|
+
|
|
32
|
+
const severity = highestRunAnnotationSeverity(summary);
|
|
33
|
+
const count = `${summary.total}${summary.truncated ? '+' : ''}`;
|
|
34
|
+
const label = `${count} ${summary.total === 1 && !summary.truncated ? 'annotation' : 'annotations'}`;
|
|
35
|
+
const accessibleLabel = severity
|
|
36
|
+
? `View ${label}, highest severity ${severity}`
|
|
37
|
+
: `View ${label}`;
|
|
38
|
+
const search: WorkflowRunsSearch = {tab: 'annotations', ...(runAttempt ? {runAttempt} : {})};
|
|
39
|
+
if (jobId) search.jobId = jobId;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Link
|
|
43
|
+
to="/w/$workspaceSlug/p/$projectSlug/runs/$workflowRunId"
|
|
44
|
+
params={{workspaceSlug, projectSlug, workflowRunId}}
|
|
45
|
+
search={workflowRunSearchParams(search, search) as never}
|
|
46
|
+
aria-label={accessibleLabel}
|
|
47
|
+
className={cn(
|
|
48
|
+
'inline-flex h-20 shrink-0 items-center gap-tight rounded-4 border px-tight font-code text-xs leading-16 tabular-nums outline-none transition-colors hover:bg-background-neutral-hover focus-visible:shadow-border-interactive-with-active',
|
|
49
|
+
// The chip stays 20px optically so it sits level with the duration metadata beside it;
|
|
50
|
+
// the hit area grows to the 44px minimum only where the pointer is coarse.
|
|
51
|
+
'[@media(pointer:coarse)]:h-44 [@media(pointer:coarse)]:px-[10px]',
|
|
52
|
+
severity ? SEVERITY_CHIP_TONE[severity] : 'border-border-neutral-base',
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
<Icon
|
|
56
|
+
name={severity ? SEVERITY_ICON[severity] : 'fileTextLine'}
|
|
57
|
+
size={12}
|
|
58
|
+
aria-hidden="true"
|
|
59
|
+
/>
|
|
60
|
+
<span className="text-foreground-neutral-base">{count}</span>
|
|
61
|
+
</Link>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import {AnnotationCard} from '@shipfox/client-ui';
|
|
2
|
+
import {Button} from '@shipfox/react-ui/button';
|
|
3
|
+
import {Icon} from '@shipfox/react-ui/icon';
|
|
4
|
+
import {Text} from '@shipfox/react-ui/typography';
|
|
5
|
+
import {cn} from '@shipfox/react-ui/utils';
|
|
6
|
+
import {Link} from '@tanstack/react-router';
|
|
7
|
+
import {useEffect, useRef} from 'react';
|
|
8
|
+
import type {RunAnnotationEntry} from '#core/run-annotation.js';
|
|
9
|
+
import {workflowJobSearchParams} from '#routes/inputs.js';
|
|
10
|
+
|
|
11
|
+
export interface RunAnnotationItemProps {
|
|
12
|
+
entry: RunAnnotationEntry;
|
|
13
|
+
workspaceSlug?: string | undefined;
|
|
14
|
+
projectSlug?: string | undefined;
|
|
15
|
+
workflowRunId: string;
|
|
16
|
+
runAttempt?: number | undefined;
|
|
17
|
+
/** Deep-link target from `?annotation=`, which takes focus rather than only being scrolled to. */
|
|
18
|
+
selected?: boolean | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* One annotation, and the only place in the product that renders an annotation body.
|
|
23
|
+
*
|
|
24
|
+
* The title is the annotation's `context`, which is what the emitting step named the block.
|
|
25
|
+
* Everything below it exists so a reader can get from a diagnostic back to its cause.
|
|
26
|
+
*/
|
|
27
|
+
export function RunAnnotationItem({
|
|
28
|
+
entry,
|
|
29
|
+
workspaceSlug,
|
|
30
|
+
projectSlug,
|
|
31
|
+
workflowRunId,
|
|
32
|
+
runAttempt,
|
|
33
|
+
selected = false,
|
|
34
|
+
}: RunAnnotationItemProps) {
|
|
35
|
+
const {annotation, origin} = entry;
|
|
36
|
+
const canLink = Boolean(origin && workspaceSlug && projectSlug);
|
|
37
|
+
const itemRef = useRef<HTMLLIElement>(null);
|
|
38
|
+
const focusedRef = useRef(false);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!selected) {
|
|
42
|
+
focusedRef.current = false;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (focusedRef.current) return;
|
|
46
|
+
|
|
47
|
+
// A deep link that only scrolls leaves a keyboard or screen-reader user at the document
|
|
48
|
+
// start while the page moves under them.
|
|
49
|
+
focusedRef.current = true;
|
|
50
|
+
itemRef.current?.focus({preventScroll: true});
|
|
51
|
+
itemRef.current?.scrollIntoView({block: 'nearest'});
|
|
52
|
+
}, [selected]);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<li
|
|
56
|
+
ref={itemRef}
|
|
57
|
+
tabIndex={-1}
|
|
58
|
+
aria-current={selected ? 'true' : undefined}
|
|
59
|
+
className={cn(
|
|
60
|
+
'rounded-8 outline-none focus-visible:shadow-border-interactive-with-active',
|
|
61
|
+
selected && 'shadow-border-interactive-with-active',
|
|
62
|
+
)}
|
|
63
|
+
>
|
|
64
|
+
<AnnotationCard
|
|
65
|
+
id={annotationElementId(annotation.id)}
|
|
66
|
+
style={annotation.style}
|
|
67
|
+
title={annotation.context}
|
|
68
|
+
titleAs="h3"
|
|
69
|
+
provenance={<RunAnnotationProvenance entry={entry} />}
|
|
70
|
+
body={annotation.body}
|
|
71
|
+
action={
|
|
72
|
+
canLink && origin ? (
|
|
73
|
+
<Button
|
|
74
|
+
asChild
|
|
75
|
+
size="xs"
|
|
76
|
+
variant="transparent"
|
|
77
|
+
className="[@media(pointer:coarse)]:min-h-44"
|
|
78
|
+
>
|
|
79
|
+
<Link
|
|
80
|
+
to="/w/$workspaceSlug/p/$projectSlug/runs/$workflowRunId/jobs/$jobId"
|
|
81
|
+
params={{
|
|
82
|
+
workspaceSlug: workspaceSlug ?? '',
|
|
83
|
+
projectSlug: projectSlug ?? '',
|
|
84
|
+
workflowRunId,
|
|
85
|
+
jobId: origin.jobId,
|
|
86
|
+
}}
|
|
87
|
+
search={
|
|
88
|
+
workflowJobSearchParams({
|
|
89
|
+
jobExecutionId: origin.jobExecutionId,
|
|
90
|
+
stepId: origin.stepId,
|
|
91
|
+
stepAttemptId: origin.stepAttemptId,
|
|
92
|
+
runAttempt,
|
|
93
|
+
}) as never
|
|
94
|
+
}
|
|
95
|
+
>
|
|
96
|
+
Open step
|
|
97
|
+
<Icon name="arrowRightLine" size={12} aria-hidden="true" />
|
|
98
|
+
</Link>
|
|
99
|
+
</Button>
|
|
100
|
+
) : null
|
|
101
|
+
}
|
|
102
|
+
/>
|
|
103
|
+
</li>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** `job · execution #2 · run tests · attempt 1`, dropping any part the run no longer resolves. */
|
|
108
|
+
function RunAnnotationProvenance({entry}: {entry: RunAnnotationEntry}) {
|
|
109
|
+
const parts = [entry.jobName, entry.executionLabel, entry.stepLabel, entry.attemptLabel].filter(
|
|
110
|
+
(part): part is string => Boolean(part),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<Text as="p" size="xs" className="min-w-0 break-words font-code text-foreground-neutral-subtle">
|
|
115
|
+
{parts.join(' · ')}
|
|
116
|
+
</Text>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Stable element id so `?annotation=<id>` can scroll to and highlight one annotation. */
|
|
121
|
+
export function annotationElementId(annotationId: string): string {
|
|
122
|
+
return `run-annotation-${annotationId}`;
|
|
123
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import {AnnotationCard, QueryLoadError, type QueryLoadErrorQuery} from '@shipfox/client-ui';
|
|
2
|
+
import {Button} from '@shipfox/react-ui/button';
|
|
3
|
+
import {Callout} from '@shipfox/react-ui/callout';
|
|
4
|
+
import {EmptyState} from '@shipfox/react-ui/empty-state';
|
|
5
|
+
import {Skeleton} from '@shipfox/react-ui/skeleton';
|
|
6
|
+
import {Text} from '@shipfox/react-ui/typography';
|
|
7
|
+
import {useState} from 'react';
|
|
8
|
+
import type {RunAnnotationEntry, RunAnnotationStyle} from '#core/run-annotation.js';
|
|
9
|
+
import {RunAnnotationItem} from './run-annotation-item.js';
|
|
10
|
+
import {RunAnnotationsEmpty} from './run-annotations-empty.js';
|
|
11
|
+
|
|
12
|
+
export interface RunAnnotationListQuery extends QueryLoadErrorQuery {
|
|
13
|
+
isPending: boolean;
|
|
14
|
+
hasNextPage: boolean;
|
|
15
|
+
isFetchingNextPage: boolean;
|
|
16
|
+
fetchNextPage: () => unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RunAnnotationListProps {
|
|
20
|
+
query: RunAnnotationListQuery;
|
|
21
|
+
entries: RunAnnotationEntry[] | undefined;
|
|
22
|
+
/** Synthetic terminal-job diagnostics for jobs that never created an execution record. */
|
|
23
|
+
derivedAnnotations?: readonly DerivedRunAnnotation[] | undefined;
|
|
24
|
+
workflowRunId: string;
|
|
25
|
+
workspaceSlug?: string | undefined;
|
|
26
|
+
projectSlug?: string | undefined;
|
|
27
|
+
runAttempt?: number | undefined;
|
|
28
|
+
/** Name of the job the list is filtered to, used only for empty-state copy. */
|
|
29
|
+
filteredJobName?: string | undefined;
|
|
30
|
+
/** Active severity filter, named in the empty state so the reader knows what to clear. */
|
|
31
|
+
filteredSeverity?: string | undefined;
|
|
32
|
+
filtered: boolean;
|
|
33
|
+
onClearFilters?: (() => void) | undefined;
|
|
34
|
+
/** Annotation id from `?annotation=`, which the matching card scrolls to and focuses. */
|
|
35
|
+
selectedAnnotationId?: string | undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DerivedRunAnnotation {
|
|
39
|
+
id: string;
|
|
40
|
+
style: RunAnnotationStyle;
|
|
41
|
+
body: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* How many annotations render at once.
|
|
46
|
+
*
|
|
47
|
+
* A page holds up to 500 rows and each one mounts a Markdown pipeline, so the fetch budget is
|
|
48
|
+
* not a render budget. The window grows on demand and the server page only loads once the
|
|
49
|
+
* reader has actually seen everything already fetched.
|
|
50
|
+
*/
|
|
51
|
+
const RENDER_WINDOW = 25;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The annotations list.
|
|
55
|
+
*
|
|
56
|
+
* Loading, empty, error, and stale are four distinct renderings on purpose: coalescing a failed
|
|
57
|
+
* fetch into an empty result tells a reader their run is clean when the truth is unknown.
|
|
58
|
+
*/
|
|
59
|
+
export function RunAnnotationList({
|
|
60
|
+
query,
|
|
61
|
+
entries,
|
|
62
|
+
derivedAnnotations = [],
|
|
63
|
+
workflowRunId,
|
|
64
|
+
workspaceSlug,
|
|
65
|
+
projectSlug,
|
|
66
|
+
runAttempt,
|
|
67
|
+
filteredJobName,
|
|
68
|
+
filteredSeverity,
|
|
69
|
+
filtered,
|
|
70
|
+
onClearFilters,
|
|
71
|
+
selectedAnnotationId,
|
|
72
|
+
}: RunAnnotationListProps) {
|
|
73
|
+
const [visibleCount, setVisibleCount] = useState(RENDER_WINDOW);
|
|
74
|
+
|
|
75
|
+
// A deep-linked annotation past the window would otherwise be unreachable: it is in the data,
|
|
76
|
+
// absent from the DOM, and nothing on screen says so.
|
|
77
|
+
const selectedIndex = selectedAnnotationId
|
|
78
|
+
? (entries?.findIndex((entry) => entry.annotation.id === selectedAnnotationId) ?? -1)
|
|
79
|
+
: -1;
|
|
80
|
+
const renderCount = Math.max(visibleCount, selectedIndex + 1);
|
|
81
|
+
|
|
82
|
+
if (query.isPending) return <RunAnnotationListSkeleton />;
|
|
83
|
+
|
|
84
|
+
if (query.isError && entries === undefined) {
|
|
85
|
+
return <QueryLoadError query={query} subject="annotations" icon="fileDamageLine" />;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (entries === undefined) return <RunAnnotationListSkeleton />;
|
|
89
|
+
|
|
90
|
+
const visible = entries.slice(0, renderCount);
|
|
91
|
+
const hiddenCount = entries.length - visible.length;
|
|
92
|
+
const hasContent = entries.length > 0 || derivedAnnotations.length > 0;
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div className="flex min-w-0 flex-col gap-cluster">
|
|
96
|
+
{query.isError ? <RunAnnotationStaleError query={query} /> : null}
|
|
97
|
+
|
|
98
|
+
{!hasContent ? (
|
|
99
|
+
filtered ? (
|
|
100
|
+
<RunAnnotationsFilteredEmpty
|
|
101
|
+
jobName={filteredJobName}
|
|
102
|
+
severity={filteredSeverity}
|
|
103
|
+
incomplete={query.hasNextPage}
|
|
104
|
+
onClearFilters={onClearFilters}
|
|
105
|
+
/>
|
|
106
|
+
) : (
|
|
107
|
+
<RunAnnotationsEmpty />
|
|
108
|
+
)
|
|
109
|
+
) : (
|
|
110
|
+
<ol className="flex min-w-0 flex-col gap-cluster">
|
|
111
|
+
{visible.map((entry) => (
|
|
112
|
+
<RunAnnotationItem
|
|
113
|
+
key={entry.annotation.id}
|
|
114
|
+
entry={entry}
|
|
115
|
+
workspaceSlug={workspaceSlug}
|
|
116
|
+
projectSlug={projectSlug}
|
|
117
|
+
workflowRunId={workflowRunId}
|
|
118
|
+
runAttempt={runAttempt}
|
|
119
|
+
selected={entry.annotation.id === selectedAnnotationId}
|
|
120
|
+
/>
|
|
121
|
+
))}
|
|
122
|
+
{derivedAnnotations.map((annotation) => (
|
|
123
|
+
<li key={annotation.id}>
|
|
124
|
+
<AnnotationCard style={annotation.style} body={annotation.body} />
|
|
125
|
+
</li>
|
|
126
|
+
))}
|
|
127
|
+
</ol>
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
{hiddenCount > 0 ? (
|
|
131
|
+
<Button
|
|
132
|
+
type="button"
|
|
133
|
+
size="sm"
|
|
134
|
+
variant="secondary"
|
|
135
|
+
className="self-start [@media(pointer:coarse)]:min-h-44"
|
|
136
|
+
onClick={() => setVisibleCount((current) => current + RENDER_WINDOW)}
|
|
137
|
+
>
|
|
138
|
+
Show {Math.min(hiddenCount, RENDER_WINDOW)} more of {entries.length}
|
|
139
|
+
</Button>
|
|
140
|
+
) : query.hasNextPage ? (
|
|
141
|
+
<Button
|
|
142
|
+
type="button"
|
|
143
|
+
size="sm"
|
|
144
|
+
variant="secondary"
|
|
145
|
+
className="self-start [@media(pointer:coarse)]:min-h-44"
|
|
146
|
+
isLoading={query.isFetchingNextPage}
|
|
147
|
+
onClick={() => {
|
|
148
|
+
void query.fetchNextPage();
|
|
149
|
+
}}
|
|
150
|
+
>
|
|
151
|
+
Load more annotations
|
|
152
|
+
</Button>
|
|
153
|
+
) : null}
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Names the filters that are actually active.
|
|
160
|
+
*
|
|
161
|
+
* Blaming severity for a job-only filter sends the reader to change the wrong control, which
|
|
162
|
+
* is worse than saying nothing.
|
|
163
|
+
*/
|
|
164
|
+
function filteredEmptyDescription({
|
|
165
|
+
jobName,
|
|
166
|
+
severity,
|
|
167
|
+
incomplete,
|
|
168
|
+
}: {
|
|
169
|
+
jobName: string | undefined;
|
|
170
|
+
severity: string | undefined;
|
|
171
|
+
incomplete: boolean;
|
|
172
|
+
}): string {
|
|
173
|
+
if (incomplete) {
|
|
174
|
+
if (jobName && severity) {
|
|
175
|
+
return `None of the loaded annotations are from ${jobName} at ${severity} severity. Load more annotations to continue searching.`;
|
|
176
|
+
}
|
|
177
|
+
if (jobName) {
|
|
178
|
+
return `None of the loaded annotations are from ${jobName}. Load more annotations to continue searching.`;
|
|
179
|
+
}
|
|
180
|
+
if (severity) {
|
|
181
|
+
return `None of the loaded annotations are at ${severity} severity. Load more annotations to continue searching.`;
|
|
182
|
+
}
|
|
183
|
+
return 'None of the loaded annotations match the current filters. Load more annotations to continue searching.';
|
|
184
|
+
}
|
|
185
|
+
if (jobName && severity) {
|
|
186
|
+
return `This run has annotations, but none from ${jobName} at ${severity} severity.`;
|
|
187
|
+
}
|
|
188
|
+
if (jobName) return `This run has annotations, but none from ${jobName}.`;
|
|
189
|
+
if (severity) return `This run has annotations, but none at ${severity} severity.`;
|
|
190
|
+
return 'This run has annotations, but none match the current filters.';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function RunAnnotationsFilteredEmpty({
|
|
194
|
+
jobName,
|
|
195
|
+
severity,
|
|
196
|
+
incomplete,
|
|
197
|
+
onClearFilters,
|
|
198
|
+
}: {
|
|
199
|
+
jobName: string | undefined;
|
|
200
|
+
severity: string | undefined;
|
|
201
|
+
incomplete: boolean;
|
|
202
|
+
onClearFilters: (() => void) | undefined;
|
|
203
|
+
}) {
|
|
204
|
+
return (
|
|
205
|
+
<div className="flex flex-col items-center gap-cluster">
|
|
206
|
+
<EmptyState
|
|
207
|
+
icon="fileDamageLine"
|
|
208
|
+
title={incomplete ? 'No matches in loaded annotations' : 'No matching annotations'}
|
|
209
|
+
description={filteredEmptyDescription({jobName, severity, incomplete})}
|
|
210
|
+
/>
|
|
211
|
+
{onClearFilters ? (
|
|
212
|
+
<Button
|
|
213
|
+
type="button"
|
|
214
|
+
size="sm"
|
|
215
|
+
variant="secondary"
|
|
216
|
+
className="[@media(pointer:coarse)]:min-h-44"
|
|
217
|
+
onClick={onClearFilters}
|
|
218
|
+
>
|
|
219
|
+
Clear filters
|
|
220
|
+
</Button>
|
|
221
|
+
) : null}
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* A failed poll keeps the annotations already on screen and marks them stale rather than
|
|
228
|
+
* clearing. Polite rather than assertive: the poll runs every 4s and a flapping API would
|
|
229
|
+
* otherwise interrupt a screen reader on every cycle.
|
|
230
|
+
*/
|
|
231
|
+
function RunAnnotationStaleError({query}: {query: QueryLoadErrorQuery}) {
|
|
232
|
+
return (
|
|
233
|
+
<Callout role="status" aria-live="polite" type="error">
|
|
234
|
+
<div className="flex w-full items-center justify-between gap-inline">
|
|
235
|
+
<Text size="xs">Could not refresh annotations.</Text>
|
|
236
|
+
<Button
|
|
237
|
+
type="button"
|
|
238
|
+
size="2xs"
|
|
239
|
+
variant="secondary"
|
|
240
|
+
className="[@media(pointer:coarse)]:min-h-44"
|
|
241
|
+
isLoading={query.isFetching}
|
|
242
|
+
onClick={() => {
|
|
243
|
+
void query.refetch();
|
|
244
|
+
}}
|
|
245
|
+
>
|
|
246
|
+
Retry
|
|
247
|
+
</Button>
|
|
248
|
+
</div>
|
|
249
|
+
</Callout>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const ANNOTATION_SKELETON_ROWS = ['first', 'second', 'third'];
|
|
254
|
+
|
|
255
|
+
function RunAnnotationListSkeleton() {
|
|
256
|
+
return (
|
|
257
|
+
<section aria-label="Loading annotations" className="flex flex-col gap-cluster">
|
|
258
|
+
{ANNOTATION_SKELETON_ROWS.map((row) => (
|
|
259
|
+
<div
|
|
260
|
+
key={row}
|
|
261
|
+
className="flex gap-cluster rounded-8 border border-border-neutral-base bg-background-components-base px-[12px] py-[8px]"
|
|
262
|
+
>
|
|
263
|
+
<Skeleton className="h-40 w-4 rounded-full" />
|
|
264
|
+
<div className="flex min-w-0 flex-1 flex-col gap-inline">
|
|
265
|
+
<Skeleton className="h-20 w-160 rounded-4" />
|
|
266
|
+
<Skeleton className="h-16 w-240 rounded-4" />
|
|
267
|
+
<Skeleton className="h-40 w-full rounded-4" />
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
))}
|
|
271
|
+
</section>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
@@ -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"
|