@shipfox/client-workflows 13.0.0 → 14.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/main.ts +16 -1
- package/.storybook/workspace-source.ts +31 -0
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +23 -0
- package/dist/components/job-graph/job-duration-label.js +1 -1
- package/dist/components/job-graph/job-duration-label.js.map +1 -1
- package/dist/components/job-graph/job-graph-content.d.ts +2 -1
- package/dist/components/job-graph/job-graph-content.d.ts.map +1 -1
- package/dist/components/job-graph/job-graph-content.js +3 -3
- package/dist/components/job-graph/job-graph-content.js.map +1 -1
- package/dist/components/job-graph/job-graph-view.d.ts +2 -1
- package/dist/components/job-graph/job-graph-view.d.ts.map +1 -1
- package/dist/components/job-graph/job-graph-view.js +2 -2
- package/dist/components/job-graph/job-graph-view.js.map +1 -1
- package/dist/components/job-graph/job-node.js +5 -5
- package/dist/components/job-graph/job-node.js.map +1 -1
- package/dist/components/job-graph/types.d.ts +2 -1
- package/dist/components/job-graph/types.d.ts.map +1 -1
- package/dist/components/job-graph/types.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-number-label.js +1 -1
- package/dist/components/workflow-run-number-label.js.map +1 -1
- package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js +2 -2
- 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 +2 -7
- package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
- package/dist/components/workflow-run-summary/workflow-run-summary.js +92 -68
- package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
- package/dist/components/workflow-run-tabs/index.d.ts +6 -0
- package/dist/components/workflow-run-tabs/index.d.ts.map +1 -0
- package/dist/components/workflow-run-tabs/index.js +7 -0
- package/dist/components/workflow-run-tabs/index.js.map +1 -0
- package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts +11 -0
- package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts.map +1 -0
- package/dist/components/workflow-run-tabs/run-annotation-summary-line.js +71 -0
- package/dist/components/workflow-run-tabs/run-annotation-summary-line.js.map +1 -0
- package/dist/components/workflow-run-tabs/run-annotations-empty.d.ts +2 -0
- package/dist/components/workflow-run-tabs/run-annotations-empty.d.ts.map +1 -0
- package/dist/components/workflow-run-tabs/run-annotations-empty.js +11 -0
- package/dist/components/workflow-run-tabs/run-annotations-empty.js.map +1 -0
- package/dist/components/workflow-run-tabs/run-jobs-list.d.ts +18 -0
- package/dist/components/workflow-run-tabs/run-jobs-list.d.ts.map +1 -0
- package/dist/components/workflow-run-tabs/run-jobs-list.js +107 -0
- package/dist/components/workflow-run-tabs/run-jobs-list.js.map +1 -0
- package/dist/components/workflow-run-tabs/run-tab-count.d.ts +6 -0
- package/dist/components/workflow-run-tabs/run-tab-count.d.ts.map +1 -0
- package/dist/components/workflow-run-tabs/run-tab-count.js +27 -0
- package/dist/components/workflow-run-tabs/run-tab-count.js.map +1 -0
- package/dist/components/workflow-run-tabs/run-tab-strip.d.ts +8 -0
- package/dist/components/workflow-run-tabs/run-tab-strip.d.ts.map +1 -0
- package/dist/components/workflow-run-tabs/run-tab-strip.js +82 -0
- package/dist/components/workflow-run-tabs/run-tab-strip.js.map +1 -0
- package/dist/components/workflow-run-view/workflow-run-selection.d.ts.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-selection.js +40 -1
- package/dist/components/workflow-run-view/workflow-run-selection.js.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-states.d.ts +1 -0
- package/dist/components/workflow-run-view/workflow-run-states.d.ts.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-states.js +10 -1
- package/dist/components/workflow-run-view/workflow-run-states.js.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-view.d.ts +8 -4
- package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-view.js +125 -78
- package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
- package/dist/components/workflow-source-panel/index.d.ts +1 -0
- package/dist/components/workflow-source-panel/index.d.ts.map +1 -1
- package/dist/components/workflow-source-panel/index.js +1 -0
- package/dist/components/workflow-source-panel/index.js.map +1 -1
- package/dist/components/workflow-source-panel/workflow-source-content.d.ts +12 -0
- package/dist/components/workflow-source-panel/workflow-source-content.d.ts.map +1 -0
- package/dist/components/workflow-source-panel/workflow-source-content.js +53 -0
- package/dist/components/workflow-source-panel/workflow-source-content.js.map +1 -0
- package/dist/components/workflow-source-panel/workflow-source-panel.d.ts +1 -1
- package/dist/components/workflow-source-panel/workflow-source-panel.d.ts.map +1 -1
- package/dist/components/workflow-source-panel/workflow-source-panel.js +7 -54
- package/dist/components/workflow-source-panel/workflow-source-panel.js.map +1 -1
- package/dist/core/entities/step.d.ts +1 -1
- package/dist/core/entities/step.d.ts.map +1 -1
- package/dist/core/entities/step.js +2 -0
- package/dist/core/entities/step.js.map +1 -1
- package/dist/core/workflow-run-tabs.d.ts +9 -0
- package/dist/core/workflow-run-tabs.d.ts.map +1 -0
- package/dist/core/workflow-run-tabs.js +3 -0
- package/dist/core/workflow-run-tabs.js.map +1 -0
- package/dist/pages/workflow-run-detail-page.d.ts.map +1 -1
- package/dist/pages/workflow-run-detail-page.js +28 -6
- package/dist/pages/workflow-run-detail-page.js.map +1 -1
- package/dist/routes/inputs.d.ts +33 -0
- package/dist/routes/inputs.d.ts.map +1 -1
- package/dist/routes/inputs.js +49 -0
- package/dist/routes/inputs.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/src/components/job-graph/job-duration-label.tsx +5 -1
- package/src/components/job-graph/job-graph-content.tsx +7 -4
- package/src/components/job-graph/job-graph-view.tsx +6 -3
- package/src/components/job-graph/job-graph.stories.tsx +1 -1
- package/src/components/job-graph/job-node.stories.tsx +1 -1
- package/src/components/job-graph/job-node.test.tsx +27 -2
- package/src/components/job-graph/job-node.tsx +6 -6
- package/src/components/job-graph/types.ts +5 -1
- package/src/components/workflow-run-duration-label.tsx +1 -1
- package/src/components/workflow-run-number-label.tsx +1 -1
- package/src/components/workflow-run-summary/workflow-run-attempt-switcher.tsx +2 -2
- package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +23 -30
- package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +3 -22
- package/src/components/workflow-run-summary/workflow-run-summary.tsx +84 -69
- package/src/components/workflow-run-tabs/index.ts +5 -0
- package/src/components/workflow-run-tabs/run-annotation-summary-line.test.tsx +69 -0
- package/src/components/workflow-run-tabs/run-annotation-summary-line.tsx +97 -0
- package/src/components/workflow-run-tabs/run-annotations-empty.tsx +11 -0
- package/src/components/workflow-run-tabs/run-jobs-list.tsx +151 -0
- package/src/components/workflow-run-tabs/run-tab-count.tsx +28 -0
- package/src/components/workflow-run-tabs/run-tab-strip.stories.tsx +52 -0
- package/src/components/workflow-run-tabs/run-tab-strip.test.tsx +117 -0
- package/src/components/workflow-run-tabs/run-tab-strip.tsx +97 -0
- package/src/components/workflow-run-view/workflow-run-selection.test.ts +48 -0
- package/src/components/workflow-run-view/workflow-run-selection.ts +48 -1
- package/src/components/workflow-run-view/workflow-run-states.tsx +12 -1
- package/src/components/workflow-run-view/workflow-run-view.test.tsx +64 -64
- package/src/components/workflow-run-view/workflow-run-view.tsx +199 -85
- package/src/components/workflow-source-panel/index.ts +1 -0
- package/src/components/workflow-source-panel/workflow-source-content.tsx +84 -0
- package/src/components/workflow-source-panel/workflow-source-panel.tsx +14 -81
- package/src/core/entities/step.ts +4 -0
- package/src/core/workflow-run-tabs.ts +8 -0
- package/src/pages/workflow-run-detail-page.tsx +31 -4
- package/src/pages/workflow-run-pages.test.tsx +185 -45
- package/src/routes/inputs.test.ts +47 -0
- package/src/routes/inputs.ts +45 -0
- package/src/storybook-config.test.ts +27 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/tsconfig.test.json +7 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/client-workflows",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "14.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -27,15 +27,15 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@swc/helpers": "^0.5.17",
|
|
29
29
|
"date-fns": "^4.1.0",
|
|
30
|
-
"@shipfox/api-workflows-dto": "12.0.0",
|
|
31
|
-
"@shipfox/client-api": "6.0.1",
|
|
32
|
-
"@shipfox/client-logs": "13.0.0",
|
|
33
30
|
"@shipfox/api-definitions-dto": "12.0.0",
|
|
34
|
-
"@shipfox/
|
|
35
|
-
"@shipfox/client-
|
|
36
|
-
"@shipfox/client-
|
|
37
|
-
"@shipfox/
|
|
38
|
-
"@shipfox/client-
|
|
31
|
+
"@shipfox/api-workflows-dto": "12.1.0",
|
|
32
|
+
"@shipfox/client-logs": "14.0.0",
|
|
33
|
+
"@shipfox/client-api": "6.0.1",
|
|
34
|
+
"@shipfox/client-projects": "14.0.0",
|
|
35
|
+
"@shipfox/client-shell": "14.0.0",
|
|
36
|
+
"@shipfox/client-triggers": "14.0.0",
|
|
37
|
+
"@shipfox/client-ui": "14.0.0",
|
|
38
|
+
"@shipfox/react-ui": "0.5.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@tanstack/react-query": "^5.101.0",
|
|
@@ -22,7 +22,11 @@ function LiveDurationText({fromIso}: {fromIso: string}) {
|
|
|
22
22
|
function DurationText({children}: {children: string}) {
|
|
23
23
|
// The status icon names the state; the node aria-label carries the verb.
|
|
24
24
|
return (
|
|
25
|
-
<Code
|
|
25
|
+
<Code
|
|
26
|
+
as="span"
|
|
27
|
+
variant="label"
|
|
28
|
+
className="shrink-0 tabular-nums text-foreground-neutral-subtle"
|
|
29
|
+
>
|
|
26
30
|
{children}
|
|
27
31
|
</Code>
|
|
28
32
|
);
|
|
@@ -7,6 +7,7 @@ import type {WorkflowRunDetail} from '#core/workflow-run.js';
|
|
|
7
7
|
import type {JobGraphModel, JobGraphNavigationKey} from './graph-model.js';
|
|
8
8
|
import {nextJobGraphNodeId} from './graph-model.js';
|
|
9
9
|
import {JobNode, TriggerNode} from './job-node.js';
|
|
10
|
+
import type {JobGraphSelectionSource} from './types.js';
|
|
10
11
|
|
|
11
12
|
const NODE_WIDTH = 208;
|
|
12
13
|
const NODE_HEIGHT = 48;
|
|
@@ -27,7 +28,7 @@ export function JobGraphContent({
|
|
|
27
28
|
'triggerDisplayLabel' | 'triggerLabel' | 'triggerProvider' | 'triggerSource'
|
|
28
29
|
>;
|
|
29
30
|
selectedJobId?: string | undefined;
|
|
30
|
-
onSelectJob: (jobId: string | undefined) => void;
|
|
31
|
+
onSelectJob: (jobId: string | undefined, source?: JobGraphSelectionSource) => void;
|
|
31
32
|
}) {
|
|
32
33
|
const nodeRefs = useRef(new Map<string, HTMLButtonElement>());
|
|
33
34
|
const [hoveredJobId, setHoveredJobId] = useState<string | undefined>();
|
|
@@ -74,13 +75,13 @@ export function JobGraphContent({
|
|
|
74
75
|
if (!nextNodeId) return;
|
|
75
76
|
|
|
76
77
|
event.preventDefault();
|
|
77
|
-
onSelectJob(nextNodeId);
|
|
78
|
+
onSelectJob(nextNodeId, 'keyboard');
|
|
78
79
|
nodeRefs.current.get(nextNodeId)?.focus();
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
return (
|
|
82
83
|
<TimeTickerProvider intervalMs={1000} reducedMotionIntervalMs={10_000}>
|
|
83
|
-
<div className="min-h-0 overflow-auto bg-background-neutral-
|
|
84
|
+
<div className="min-h-0 overflow-auto bg-background-neutral-background">
|
|
84
85
|
<div className="relative" style={{width: contentWidth, minHeight: contentHeight}}>
|
|
85
86
|
<GraphEdges model={model} hoveredJobId={hoveredJobId} />
|
|
86
87
|
<div
|
|
@@ -101,7 +102,9 @@ export function JobGraphContent({
|
|
|
101
102
|
node={node}
|
|
102
103
|
selected={node.id === selectedJobId}
|
|
103
104
|
ref={setNodeRef(node.id)}
|
|
104
|
-
onSelect={() =>
|
|
105
|
+
onSelect={() =>
|
|
106
|
+
onSelectJob(node.id === selectedJobId ? undefined : node.id, 'pointer')
|
|
107
|
+
}
|
|
105
108
|
onKeyDown={handleKeyDown}
|
|
106
109
|
onHoverStart={() => setHoveredJobId(node.id)}
|
|
107
110
|
onHoverEnd={() =>
|
|
@@ -3,6 +3,7 @@ import {useState} from 'react';
|
|
|
3
3
|
import type {WorkflowRunDetail} from '#core/workflow-run.js';
|
|
4
4
|
import type {JobGraphModel} from './graph-model.js';
|
|
5
5
|
import {JobGraphContent} from './job-graph-content.js';
|
|
6
|
+
import type {JobGraphSelectionSource} from './types.js';
|
|
6
7
|
|
|
7
8
|
export function JobGraphView({
|
|
8
9
|
model,
|
|
@@ -19,7 +20,9 @@ export function JobGraphView({
|
|
|
19
20
|
>;
|
|
20
21
|
selectedJobId?: string | undefined;
|
|
21
22
|
defaultSelectedJobId?: string | undefined;
|
|
22
|
-
onSelectedJobChange?:
|
|
23
|
+
onSelectedJobChange?:
|
|
24
|
+
| ((jobId: string | undefined, source?: JobGraphSelectionSource) => void)
|
|
25
|
+
| undefined;
|
|
23
26
|
className?: string | undefined;
|
|
24
27
|
}) {
|
|
25
28
|
const [localSelectedJobId, setLocalSelectedJobId] = useState<string | undefined>(
|
|
@@ -27,9 +30,9 @@ export function JobGraphView({
|
|
|
27
30
|
);
|
|
28
31
|
const selected = selectedJobId ?? localSelectedJobId;
|
|
29
32
|
|
|
30
|
-
function selectJob(jobId: string | undefined) {
|
|
33
|
+
function selectJob(jobId: string | undefined, source?: JobGraphSelectionSource) {
|
|
31
34
|
setLocalSelectedJobId(jobId);
|
|
32
|
-
onSelectedJobChange?.(jobId);
|
|
35
|
+
onSelectedJobChange?.(jobId, source);
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
return (
|
|
@@ -11,7 +11,7 @@ const meta = {
|
|
|
11
11
|
parameters: {layout: 'centered'},
|
|
12
12
|
decorators: [
|
|
13
13
|
(Story) => (
|
|
14
|
-
<div className="h-520 w-900 overflow-auto bg-background-neutral-
|
|
14
|
+
<div className="h-520 w-900 overflow-auto bg-background-neutral-background p-16">
|
|
15
15
|
<Story />
|
|
16
16
|
</div>
|
|
17
17
|
),
|
|
@@ -52,11 +52,14 @@ function makeNode(overrides: NodeOverrides): JobGraphNode {
|
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
function renderNode(
|
|
55
|
+
function renderNode(
|
|
56
|
+
node: JobGraphNode,
|
|
57
|
+
{live = false, selected = false}: {live?: boolean; selected?: boolean} = {},
|
|
58
|
+
) {
|
|
56
59
|
const element = (
|
|
57
60
|
<JobNode
|
|
58
61
|
node={node}
|
|
59
|
-
selected={
|
|
62
|
+
selected={selected}
|
|
60
63
|
onSelect={() => undefined}
|
|
61
64
|
onKeyDown={() => undefined}
|
|
62
65
|
onHoverStart={() => undefined}
|
|
@@ -91,6 +94,28 @@ function setVisibility(state: 'visible' | 'hidden') {
|
|
|
91
94
|
Object.defineProperty(document, 'visibilityState', {configurable: true, value: state});
|
|
92
95
|
}
|
|
93
96
|
|
|
97
|
+
describe('JobNode interaction', () => {
|
|
98
|
+
test('presents the selectable node as an elevated interactive surface', () => {
|
|
99
|
+
renderNode(makeNode({name: 'build', status: 'pending'}));
|
|
100
|
+
|
|
101
|
+
expect(screen.getByRole('button', {name: 'build, Pending'})).toHaveClass(
|
|
102
|
+
'cursor-pointer',
|
|
103
|
+
'bg-background-neutral-base',
|
|
104
|
+
'shadow-button-neutral',
|
|
105
|
+
'hover:bg-background-components-hover',
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('uses a quiet neutral surface for the selected node', () => {
|
|
110
|
+
renderNode(makeNode({name: 'build', status: 'pending'}), {selected: true});
|
|
111
|
+
|
|
112
|
+
expect(screen.getByRole('button', {name: 'build, Pending'})).toHaveClass(
|
|
113
|
+
'bg-background-components-hover',
|
|
114
|
+
'hover:bg-background-components-pressed',
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
94
119
|
describe('JobNode duration', () => {
|
|
95
120
|
beforeEach(() => {
|
|
96
121
|
setMatchMedia(false);
|
|
@@ -40,14 +40,14 @@ export function TriggerNode({
|
|
|
40
40
|
<button
|
|
41
41
|
type="button"
|
|
42
42
|
aria-label={label}
|
|
43
|
-
className="flex items-center justify-center rounded-full
|
|
43
|
+
className="flex cursor-help items-center justify-center rounded-full bg-background-neutral-base shadow-button-neutral transition-colors hover:bg-background-components-hover focus-visible:shadow-button-neutral-focus focus-visible:outline-none"
|
|
44
44
|
style={{width: TRIGGER_SIZE, height: TRIGGER_SIZE}}
|
|
45
45
|
>
|
|
46
46
|
<TriggerSourceIcon
|
|
47
47
|
provider={trigger.triggerProvider}
|
|
48
48
|
source={trigger.triggerSource}
|
|
49
49
|
aria-hidden
|
|
50
|
-
className="size-14 shrink-0 text-foreground-neutral-
|
|
50
|
+
className="size-14 shrink-0 text-foreground-neutral-subtle"
|
|
51
51
|
/>
|
|
52
52
|
</button>
|
|
53
53
|
</TooltipTrigger>
|
|
@@ -106,15 +106,15 @@ export function JobNode({
|
|
|
106
106
|
onPointerEnter={onHoverStart}
|
|
107
107
|
onPointerLeave={onHoverEnd}
|
|
108
108
|
className={cn(
|
|
109
|
-
'group relative flex h-48 w-208 items-center gap-8 rounded-8
|
|
110
|
-
selected && 'bg-background-components-hover',
|
|
109
|
+
'group relative flex h-48 w-208 cursor-pointer items-center gap-8 rounded-8 bg-background-neutral-base px-10 text-left shadow-button-neutral transition-colors hover:bg-background-components-hover focus-visible:shadow-button-neutral-focus focus-visible:outline-none',
|
|
110
|
+
selected && 'bg-background-components-hover hover:bg-background-components-pressed',
|
|
111
111
|
node.carriedOver && 'opacity-[0.55]',
|
|
112
112
|
)}
|
|
113
113
|
>
|
|
114
114
|
{selected ? (
|
|
115
115
|
<span
|
|
116
116
|
aria-hidden="true"
|
|
117
|
-
className="absolute inset-y-6 left-0 w-
|
|
117
|
+
className="absolute inset-y-6 left-0 w-px rounded-full bg-border-highlights-interactive"
|
|
118
118
|
/>
|
|
119
119
|
) : null}
|
|
120
120
|
<div className="flex min-w-0 flex-1 items-center gap-8">
|
|
@@ -136,7 +136,7 @@ function ExecutionCountText({executions}: {executions: JobExecution[]}) {
|
|
|
136
136
|
<TooltipTrigger asChild>
|
|
137
137
|
<span
|
|
138
138
|
aria-hidden="true"
|
|
139
|
-
className="inline-flex h-20 min-w-28 shrink-0 items-center justify-end gap-4 text-foreground-neutral-
|
|
139
|
+
className="inline-flex h-20 min-w-28 shrink-0 items-center justify-end gap-4 text-foreground-neutral-subtle"
|
|
140
140
|
>
|
|
141
141
|
<Icon name="loopRightLine" className="size-12" />
|
|
142
142
|
<Code as="span" variant="label" className="text-current">
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type {WorkflowRunDetail} from '#core/workflow-run.js';
|
|
2
2
|
|
|
3
|
+
export type JobGraphSelectionSource = 'pointer' | 'keyboard';
|
|
4
|
+
|
|
3
5
|
export interface JobGraphProps {
|
|
4
6
|
run: WorkflowRunDetail;
|
|
5
7
|
selectedJobId?: string | undefined;
|
|
6
8
|
defaultSelectedJobId?: string | undefined;
|
|
7
|
-
onSelectedJobChange?:
|
|
9
|
+
onSelectedJobChange?:
|
|
10
|
+
| ((jobId: string | undefined, source?: JobGraphSelectionSource) => void)
|
|
11
|
+
| undefined;
|
|
8
12
|
className?: string | undefined;
|
|
9
13
|
}
|
|
@@ -86,7 +86,7 @@ function DurationText({
|
|
|
86
86
|
variant="label"
|
|
87
87
|
aria-label={ariaLabel}
|
|
88
88
|
className={cn(
|
|
89
|
-
'inline-flex shrink-0 items-center gap-4 tabular-nums text-foreground-neutral-
|
|
89
|
+
'inline-flex shrink-0 items-center gap-4 tabular-nums text-foreground-neutral-subtle',
|
|
90
90
|
className,
|
|
91
91
|
)}
|
|
92
92
|
>
|
|
@@ -32,7 +32,7 @@ export function WorkflowRunNumberLabel({run}: {run: WorkflowRunNumberLabelRun})
|
|
|
32
32
|
<Code
|
|
33
33
|
as="span"
|
|
34
34
|
variant="label"
|
|
35
|
-
className="min-w-0 max-w-[240px] truncate text-foreground-neutral-
|
|
35
|
+
className="min-w-0 max-w-[240px] truncate text-foreground-neutral-subtle"
|
|
36
36
|
>
|
|
37
37
|
{visibleLabel}
|
|
38
38
|
</Code>
|
|
@@ -52,7 +52,7 @@ export function WorkflowRunAttemptSwitcher({
|
|
|
52
52
|
size="2xs"
|
|
53
53
|
iconRight="arrowDownSLine"
|
|
54
54
|
aria-label={`Switch attempt, currently ${run.runAttempt.attempt} of ${maxAttempt}`}
|
|
55
|
-
className="h-20 px-4 text-foreground-neutral-
|
|
55
|
+
className="h-20 px-4 text-foreground-neutral-subtle hover:text-foreground-neutral-base"
|
|
56
56
|
>
|
|
57
57
|
<Text as="span" size="xs" className="text-inherit">
|
|
58
58
|
Attempt {run.runAttempt.attempt} of {maxAttempt}
|
|
@@ -139,7 +139,7 @@ function AttemptItem({
|
|
|
139
139
|
</Code>
|
|
140
140
|
<RelativeTime
|
|
141
141
|
value={attempt.createdAt}
|
|
142
|
-
className="shrink-0 whitespace-nowrap font-code text-xs leading-20 text-foreground-neutral-
|
|
142
|
+
className="shrink-0 whitespace-nowrap font-code text-xs leading-20 text-foreground-neutral-subtle"
|
|
143
143
|
/>
|
|
144
144
|
</Link>
|
|
145
145
|
</DropdownMenuItem>
|
|
@@ -56,10 +56,10 @@ const RUN_ATTEMPTS_RESPONSE = runAttemptsResponseDto({
|
|
|
56
56
|
});
|
|
57
57
|
|
|
58
58
|
const withFrame: Decorator = (Story) => (
|
|
59
|
-
<div className="min-h-screen bg-background-neutral-
|
|
59
|
+
<div className="min-h-screen bg-background-neutral-background">
|
|
60
60
|
<div className="mx-auto flex min-h-screen w-full max-w-[1120px] flex-col overflow-hidden border-x border-border-neutral-base bg-background-subtle-base">
|
|
61
61
|
<Story />
|
|
62
|
-
<div className="min-h-0 flex-1 bg-background-neutral-
|
|
62
|
+
<div className="min-h-0 flex-1 bg-background-neutral-background p-16" />
|
|
63
63
|
</div>
|
|
64
64
|
</div>
|
|
65
65
|
);
|
|
@@ -160,30 +160,6 @@ const ATTEMPT_SUMMARY_ARGS = {
|
|
|
160
160
|
|
|
161
161
|
export const Playground: Story = {};
|
|
162
162
|
|
|
163
|
-
export const WithSourceButton: Story = {
|
|
164
|
-
args: {
|
|
165
|
-
run: workflowRunDetail({
|
|
166
|
-
status: 'succeeded',
|
|
167
|
-
source_snapshot: {format: 'yaml', content: 'jobs:\n build:\n steps: []'},
|
|
168
|
-
}),
|
|
169
|
-
sourceAvailable: true,
|
|
170
|
-
sourceOpen: false,
|
|
171
|
-
sourcePanelId: 'workflow-source-panel',
|
|
172
|
-
},
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
export const SourceOpen: Story = {
|
|
176
|
-
args: {
|
|
177
|
-
run: workflowRunDetail({
|
|
178
|
-
status: 'succeeded',
|
|
179
|
-
source_snapshot: {format: 'yaml', content: 'jobs:\n build:\n steps: []'},
|
|
180
|
-
}),
|
|
181
|
-
sourceAvailable: true,
|
|
182
|
-
sourceOpen: true,
|
|
183
|
-
sourcePanelId: 'workflow-source-panel',
|
|
184
|
-
},
|
|
185
|
-
};
|
|
186
|
-
|
|
187
163
|
export const WithAttempts: Story = {
|
|
188
164
|
decorators: [withAttemptApi],
|
|
189
165
|
args: ATTEMPT_SUMMARY_ARGS,
|
|
@@ -323,7 +299,7 @@ const ACTION_VARIANTS = [
|
|
|
323
299
|
>;
|
|
324
300
|
}>;
|
|
325
301
|
|
|
326
|
-
export const
|
|
302
|
+
export const ActionVariants: Story = {
|
|
327
303
|
render: () => (
|
|
328
304
|
<div className="flex flex-col">
|
|
329
305
|
{ACTION_VARIANTS.map(({label, run, props}, index) => (
|
|
@@ -333,9 +309,6 @@ export const ActionVariantsWithSource: Story = {
|
|
|
333
309
|
...run,
|
|
334
310
|
id: `22222222-2222-4222-8222-${String(index + 2).padStart(12, '0')}`,
|
|
335
311
|
}}
|
|
336
|
-
sourceAvailable
|
|
337
|
-
sourceOpen={label === 'Running'}
|
|
338
|
-
sourcePanelId={`workflow-source-panel-${index}`}
|
|
339
312
|
{...props}
|
|
340
313
|
/>
|
|
341
314
|
))}
|
|
@@ -410,3 +383,23 @@ export const LongTriggerMetadata: Story = {
|
|
|
410
383
|
}),
|
|
411
384
|
},
|
|
412
385
|
};
|
|
386
|
+
|
|
387
|
+
export const NarrowLongContent: Story = {
|
|
388
|
+
decorators: [
|
|
389
|
+
(Story) => (
|
|
390
|
+
<div className="w-[360px] max-w-full overflow-hidden border-x border-border-neutral-base">
|
|
391
|
+
<Story />
|
|
392
|
+
</div>
|
|
393
|
+
),
|
|
394
|
+
],
|
|
395
|
+
args: {
|
|
396
|
+
run: workflowRunDetail({
|
|
397
|
+
status: 'running',
|
|
398
|
+
name: 'release-production-multi-region-with-canary-and-post-deploy-validation',
|
|
399
|
+
trigger_provider: 'github',
|
|
400
|
+
trigger_source: 'github-enterprise-cloud-production-organization',
|
|
401
|
+
trigger_event: 'workflow_dispatch_with_release_candidate_payload',
|
|
402
|
+
}),
|
|
403
|
+
onCancel: noop,
|
|
404
|
+
},
|
|
405
|
+
};
|
|
@@ -34,6 +34,8 @@ describe('WorkflowRunSummary', () => {
|
|
|
34
34
|
|
|
35
35
|
const summary = await screen.findByRole('region', {name: 'deploy-web'});
|
|
36
36
|
|
|
37
|
+
expect(summary).toHaveClass('bg-background-neutral-background', 'px-16');
|
|
38
|
+
expect(summary.firstElementChild).not.toHaveClass('max-w-[1120px]');
|
|
37
39
|
expect(within(summary).getByRole('heading', {name: 'deploy-web'})).toBeInTheDocument();
|
|
38
40
|
expect(within(summary).getAllByText('Running')).not.toHaveLength(0);
|
|
39
41
|
expect(within(summary).getByText('CI #5184')).toBeInTheDocument();
|
|
@@ -128,27 +130,6 @@ describe('WorkflowRunSummary', () => {
|
|
|
128
130
|
);
|
|
129
131
|
});
|
|
130
132
|
|
|
131
|
-
test('renders source control only when source is available', async () => {
|
|
132
|
-
const user = userEvent.setup();
|
|
133
|
-
const onSourceToggle = vi.fn();
|
|
134
|
-
renderSummary(
|
|
135
|
-
{source_snapshot: {format: 'yaml', content: 'name: deploy-web'}},
|
|
136
|
-
{
|
|
137
|
-
sourceAvailable: true,
|
|
138
|
-
sourceOpen: false,
|
|
139
|
-
sourcePanelId: 'workflow-source-panel',
|
|
140
|
-
onSourceToggle,
|
|
141
|
-
},
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
const sourceButton = await screen.findByRole('button', {name: 'View source'});
|
|
145
|
-
await user.click(sourceButton);
|
|
146
|
-
|
|
147
|
-
expect(sourceButton).toHaveAttribute('aria-controls', 'workflow-source-panel');
|
|
148
|
-
expect(sourceButton).toHaveAttribute('aria-expanded', 'false');
|
|
149
|
-
expect(onSourceToggle).toHaveBeenCalledTimes(1);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
133
|
test('shows the selected attempt duration, not the top-level run duration', async () => {
|
|
153
134
|
renderSummary({
|
|
154
135
|
started_at: '2026-05-07T00:00:00.000Z',
|
|
@@ -193,7 +174,7 @@ describe('WorkflowRunSummary', () => {
|
|
|
193
174
|
expect(duration).toHaveAttribute('aria-label', 'running 2m 14s');
|
|
194
175
|
});
|
|
195
176
|
|
|
196
|
-
test('
|
|
177
|
+
test('does not render a whole-run source control', async () => {
|
|
197
178
|
renderSummary();
|
|
198
179
|
|
|
199
180
|
await screen.findByRole('region', {name: 'deploy-web'});
|
|
@@ -12,7 +12,7 @@ import {RelativeTime} from '@shipfox/react-ui/relative-time';
|
|
|
12
12
|
import {TimeTickerProvider} from '@shipfox/react-ui/time-ticker';
|
|
13
13
|
import {Tooltip, TooltipContent, TooltipTrigger} from '@shipfox/react-ui/tooltip';
|
|
14
14
|
import {Header, Text} from '@shipfox/react-ui/typography';
|
|
15
|
-
import
|
|
15
|
+
import {Link} from '@tanstack/react-router';
|
|
16
16
|
import {useId} from 'react';
|
|
17
17
|
import {WorkflowRunNumberLabel} from '#components/workflow-run-number-label.js';
|
|
18
18
|
import {
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
type WorkflowRunDetail,
|
|
23
23
|
type WorkflowRunRerunMode,
|
|
24
24
|
} from '#core/workflow-run.js';
|
|
25
|
+
import {validateWorkflowRunsSearch, workflowRunListSearchParams} from '#routes/inputs.js';
|
|
25
26
|
import {WorkflowRunDurationLabel} from '../workflow-run-duration-label.js';
|
|
26
27
|
import {getWorkflowStatusVisual} from '../workflow-status/status-visuals.js';
|
|
27
28
|
import {WorkflowRunAttemptSwitcher} from './workflow-run-attempt-switcher.js';
|
|
@@ -36,11 +37,6 @@ export interface WorkflowRunSummaryProps {
|
|
|
36
37
|
workspaceSlug?: string | undefined;
|
|
37
38
|
projectSlug?: string | undefined;
|
|
38
39
|
run: WorkflowRunDetail;
|
|
39
|
-
sourceAvailable?: boolean | undefined;
|
|
40
|
-
sourceOpen?: boolean | undefined;
|
|
41
|
-
sourcePanelId?: string | undefined;
|
|
42
|
-
sourceButtonRef?: Ref<HTMLButtonElement> | undefined;
|
|
43
|
-
onSourceToggle?: (() => void) | undefined;
|
|
44
40
|
cancelling?: boolean | undefined;
|
|
45
41
|
onCancel?: (() => void) | undefined;
|
|
46
42
|
rerunPending?: boolean | undefined;
|
|
@@ -52,11 +48,6 @@ export function WorkflowRunSummary({
|
|
|
52
48
|
workspaceSlug,
|
|
53
49
|
projectSlug,
|
|
54
50
|
run,
|
|
55
|
-
sourceAvailable = false,
|
|
56
|
-
sourceOpen = false,
|
|
57
|
-
sourcePanelId,
|
|
58
|
-
sourceButtonRef,
|
|
59
|
-
onSourceToggle,
|
|
60
51
|
cancelling = false,
|
|
61
52
|
onCancel,
|
|
62
53
|
rerunPending = false,
|
|
@@ -66,6 +57,7 @@ export function WorkflowRunSummary({
|
|
|
66
57
|
const headingId = useId();
|
|
67
58
|
const status = getWorkflowStatusVisual(run.runAttempt.status);
|
|
68
59
|
const action = workflowRunActionForRun(run);
|
|
60
|
+
const hasAction = canRenderWorkflowRunAction(action, onCancel, onRerun);
|
|
69
61
|
const attemptSwitcher =
|
|
70
62
|
latestAttempt && latestAttempt > 1 && workspaceSlug && projectSlug
|
|
71
63
|
? {workspaceSlug, projectSlug, latestAttempt}
|
|
@@ -76,64 +68,77 @@ export function WorkflowRunSummary({
|
|
|
76
68
|
|
|
77
69
|
return (
|
|
78
70
|
<TimeTickerProvider intervalMs={1000} reducedMotionIntervalMs={10_000}>
|
|
79
|
-
<section
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
title={isHeadingTruncated ? run.name : undefined}
|
|
97
|
-
className="block min-w-0 truncate"
|
|
71
|
+
<section aria-labelledby={headingId} className="bg-background-neutral-background px-16 py-12">
|
|
72
|
+
<div className="grid min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-x-12 gap-y-8 overflow-hidden max-[480px]:grid-cols-1">
|
|
73
|
+
<nav
|
|
74
|
+
aria-label="Breadcrumb"
|
|
75
|
+
className="col-start-1 row-start-1 min-w-0 max-[480px]:col-start-auto max-[480px]:row-start-auto"
|
|
76
|
+
>
|
|
77
|
+
<ol className="flex min-w-0 items-center gap-8">
|
|
78
|
+
{workspaceSlug && projectSlug ? (
|
|
79
|
+
<li className="shrink-0">
|
|
80
|
+
<Link
|
|
81
|
+
to="/w/$workspaceSlug/p/$projectSlug/runs"
|
|
82
|
+
params={{workspaceSlug, projectSlug}}
|
|
83
|
+
search={
|
|
84
|
+
((previous: Record<string, unknown>) =>
|
|
85
|
+
workflowRunListSearchParams(validateWorkflowRunsSearch(previous))) as never
|
|
86
|
+
}
|
|
87
|
+
className="rounded-4 text-xs font-medium text-foreground-neutral-subtle underline decoration-border-neutral-strong underline-offset-4 outline-none hover:text-foreground-neutral-base focus-visible:shadow-border-interactive-with-active"
|
|
98
88
|
>
|
|
99
|
-
|
|
100
|
-
</
|
|
101
|
-
</
|
|
102
|
-
</TooltipTrigger>
|
|
103
|
-
{isHeadingTruncated ? (
|
|
104
|
-
<TooltipContent>
|
|
105
|
-
<Text as="span" size="xs" className="max-w-[360px] break-words">
|
|
106
|
-
{run.name}
|
|
107
|
-
</Text>
|
|
108
|
-
</TooltipContent>
|
|
89
|
+
Runs
|
|
90
|
+
</Link>
|
|
91
|
+
</li>
|
|
109
92
|
) : null}
|
|
110
|
-
|
|
111
|
-
|
|
93
|
+
{workspaceSlug && projectSlug ? (
|
|
94
|
+
<li aria-hidden="true" className="shrink-0 text-xs text-foreground-neutral-subtle">
|
|
95
|
+
/
|
|
96
|
+
</li>
|
|
97
|
+
) : null}
|
|
98
|
+
<li aria-current="page" className="flex min-w-0 items-center gap-8">
|
|
99
|
+
<Badge variant={status.badge} size="xs">
|
|
100
|
+
<span className="text-center" style={{width: `${STATUS_BADGE_LABEL_WIDTH_CH}ch`}}>
|
|
101
|
+
{status.label}
|
|
102
|
+
</span>
|
|
103
|
+
</Badge>
|
|
112
104
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
105
|
+
<Tooltip>
|
|
106
|
+
<TooltipTrigger asChild>
|
|
107
|
+
<Header id={headingId} variant="h3" className="min-w-0 truncate">
|
|
108
|
+
<span
|
|
109
|
+
ref={headingTextRef}
|
|
110
|
+
title={isHeadingTruncated ? run.name : undefined}
|
|
111
|
+
className="block min-w-0 truncate"
|
|
112
|
+
>
|
|
113
|
+
{run.name}
|
|
114
|
+
</span>
|
|
115
|
+
</Header>
|
|
116
|
+
</TooltipTrigger>
|
|
117
|
+
{isHeadingTruncated ? (
|
|
118
|
+
<TooltipContent>
|
|
119
|
+
<Text as="span" size="xs" className="max-w-[360px] break-words">
|
|
120
|
+
{run.name}
|
|
121
|
+
</Text>
|
|
122
|
+
</TooltipContent>
|
|
123
|
+
) : null}
|
|
124
|
+
</Tooltip>
|
|
125
|
+
</li>
|
|
126
|
+
</ol>
|
|
127
|
+
</nav>
|
|
135
128
|
|
|
136
|
-
|
|
129
|
+
{hasAction ? (
|
|
130
|
+
<div className="col-start-2 row-start-1 flex min-w-max items-center gap-6 justify-self-end max-[480px]:col-start-auto max-[480px]:row-start-auto max-[480px]:justify-self-start">
|
|
131
|
+
<WorkflowRunActionSlot
|
|
132
|
+
action={action}
|
|
133
|
+
cancelling={cancelling}
|
|
134
|
+
onCancel={onCancel}
|
|
135
|
+
rerunPending={rerunPending}
|
|
136
|
+
onRerun={onRerun}
|
|
137
|
+
/>
|
|
138
|
+
</div>
|
|
139
|
+
) : null}
|
|
140
|
+
|
|
141
|
+
<div className="col-span-2 row-start-2 flex min-w-0 items-center gap-12 overflow-hidden text-foreground-neutral-subtle max-[480px]:col-span-1 max-[480px]:row-start-auto">
|
|
137
142
|
{run.number !== null ? (
|
|
138
143
|
<>
|
|
139
144
|
<WorkflowRunNumberLabel run={run} />
|
|
@@ -159,7 +164,7 @@ export function WorkflowRunSummary({
|
|
|
159
164
|
<button
|
|
160
165
|
type="button"
|
|
161
166
|
aria-label={run.triggerLabel}
|
|
162
|
-
className="inline-flex max-w-full min-w-0 items-center gap-4 rounded-6 border-0 bg-transparent p-0 text-left text-foreground-neutral-
|
|
167
|
+
className="inline-flex max-w-full min-w-0 cursor-help items-center gap-4 rounded-6 border-0 bg-transparent p-0 text-left text-foreground-neutral-subtle outline-none focus-visible:shadow-button-neutral-focus"
|
|
163
168
|
>
|
|
164
169
|
<TriggerSourceIcon
|
|
165
170
|
provider={run.triggerProvider}
|
|
@@ -187,7 +192,7 @@ export function WorkflowRunSummary({
|
|
|
187
192
|
) : null}
|
|
188
193
|
<RelativeTime
|
|
189
194
|
value={run.runAttempt.createdAt}
|
|
190
|
-
className="shrink-0 whitespace-nowrap text-xs leading-20 text-foreground-neutral-
|
|
195
|
+
className="shrink-0 whitespace-nowrap text-xs leading-20 text-foreground-neutral-subtle"
|
|
191
196
|
/>
|
|
192
197
|
|
|
193
198
|
{displayDuration ? (
|
|
@@ -195,7 +200,7 @@ export function WorkflowRunSummary({
|
|
|
195
200
|
<MetadataSeparator />
|
|
196
201
|
<WorkflowRunDurationLabel
|
|
197
202
|
duration={displayDuration}
|
|
198
|
-
className="text-foreground-neutral-
|
|
203
|
+
className="text-foreground-neutral-subtle"
|
|
199
204
|
/>
|
|
200
205
|
</>
|
|
201
206
|
) : null}
|
|
@@ -290,6 +295,16 @@ function workflowRunActionForRun(run: WorkflowRunDetail): WorkflowRunAction {
|
|
|
290
295
|
return 'rerun-menu';
|
|
291
296
|
}
|
|
292
297
|
|
|
298
|
+
function canRenderWorkflowRunAction(
|
|
299
|
+
action: WorkflowRunAction,
|
|
300
|
+
onCancel: (() => void) | undefined,
|
|
301
|
+
onRerun: ((mode: WorkflowRunRerunMode) => void) | undefined,
|
|
302
|
+
): boolean {
|
|
303
|
+
if (action === 'cancel') return onCancel !== undefined;
|
|
304
|
+
if (action === 'rerun-all' || action === 'rerun-menu') return onRerun !== undefined;
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
|
|
293
308
|
function hasFailedOrCancelledJobs(run: WorkflowRunDetail): boolean {
|
|
294
309
|
if (!workflowRunHasJobs(run)) return false;
|
|
295
310
|
|
|
@@ -300,6 +315,6 @@ function workflowRunHasJobs(run: WorkflowRunDetail): run is WorkflowRunDetail &
|
|
|
300
315
|
return 'jobs' in run && Array.isArray(run.jobs);
|
|
301
316
|
}
|
|
302
317
|
|
|
303
|
-
function MetadataSeparator() {
|
|
318
|
+
export function MetadataSeparator() {
|
|
304
319
|
return <span aria-hidden="true" className="h-12 w-px shrink-0 bg-border-neutral-base" />;
|
|
305
320
|
}
|