@shipfox/client-workflows 12.0.2 → 13.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.tsx +3 -3
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +56 -0
- package/dist/components/job-graph/job-duration-format.d.ts +2 -2
- package/dist/components/job-graph/job-duration-format.d.ts.map +1 -1
- package/dist/components/job-graph/job-duration-format.js +4 -2
- package/dist/components/job-graph/job-duration-format.js.map +1 -1
- package/dist/components/job-graph/job-node.d.ts +1 -1
- package/dist/components/job-graph/job-node.d.ts.map +1 -1
- package/dist/components/job-graph/job-node.js +5 -4
- package/dist/components/job-graph/job-node.js.map +1 -1
- package/dist/components/step-list/step-list.d.ts +2 -2
- package/dist/components/step-list/step-list.d.ts.map +1 -1
- package/dist/components/step-list/step-list.js.map +1 -1
- package/dist/components/workflow-run-list/job-status-strip.d.ts +21 -0
- package/dist/components/workflow-run-list/job-status-strip.d.ts.map +1 -0
- package/dist/components/workflow-run-list/job-status-strip.js +185 -0
- package/dist/components/workflow-run-list/job-status-strip.js.map +1 -0
- package/dist/components/workflow-run-list/run-display.d.ts +27 -3
- package/dist/components/workflow-run-list/run-display.d.ts.map +1 -1
- package/dist/components/workflow-run-list/run-display.js +78 -7
- package/dist/components/workflow-run-list/run-display.js.map +1 -1
- package/dist/components/workflow-run-list/types.d.ts +23 -24
- package/dist/components/workflow-run-list/types.d.ts.map +1 -1
- package/dist/components/workflow-run-list/types.js.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-filter-menu.d.ts +22 -0
- package/dist/components/workflow-run-list/workflow-run-filter-menu.d.ts.map +1 -0
- package/dist/components/workflow-run-list/workflow-run-filter-menu.js +68 -0
- package/dist/components/workflow-run-list/workflow-run-filter-menu.js.map +1 -0
- package/dist/components/workflow-run-list/workflow-run-filters.d.ts +18 -0
- package/dist/components/workflow-run-list/workflow-run-filters.d.ts.map +1 -0
- package/dist/components/workflow-run-list/workflow-run-filters.js +236 -0
- package/dist/components/workflow-run-list/workflow-run-filters.js.map +1 -0
- package/dist/components/workflow-run-list/workflow-run-list-content.d.ts +8 -4
- package/dist/components/workflow-run-list/workflow-run-list-content.d.ts.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-list-content.js +40 -12
- package/dist/components/workflow-run-list/workflow-run-list-content.js.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-list-states.d.ts +29 -2
- package/dist/components/workflow-run-list/workflow-run-list-states.d.ts.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-list-states.js +103 -44
- package/dist/components/workflow-run-list/workflow-run-list-states.js.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-list-view.d.ts +1 -1
- package/dist/components/workflow-run-list/workflow-run-list-view.d.ts.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-list-view.js +60 -43
- package/dist/components/workflow-run-list/workflow-run-list-view.js.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-list.d.ts +1 -1
- package/dist/components/workflow-run-list/workflow-run-list.d.ts.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-list.js +20 -8
- package/dist/components/workflow-run-list/workflow-run-list.js.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-row.d.ts +19 -9
- package/dist/components/workflow-run-list/workflow-run-row.d.ts.map +1 -1
- package/dist/components/workflow-run-list/workflow-run-row.js +179 -112
- package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
- package/dist/components/workflow-run-number-label.d.ts +15 -0
- package/dist/components/workflow-run-number-label.d.ts.map +1 -0
- package/dist/components/workflow-run-number-label.js +41 -0
- package/dist/components/workflow-run-number-label.js.map +1 -0
- package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.d.ts +3 -3
- package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.d.ts.map +1 -1
- package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js +8 -7
- 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 +3 -3
- package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
- package/dist/components/workflow-run-summary/workflow-run-summary.js +16 -7
- package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
- package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts +2 -2
- package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts.map +1 -1
- package/dist/components/workflow-run-view/agent-config-failure-callout.js +4 -4
- package/dist/components/workflow-run-view/agent-config-failure-callout.js.map +1 -1
- package/dist/components/workflow-run-view/job-card.d.ts +2 -2
- package/dist/components/workflow-run-view/job-card.d.ts.map +1 -1
- package/dist/components/workflow-run-view/job-card.js +51 -32
- package/dist/components/workflow-run-view/job-card.js.map +1 -1
- package/dist/components/workflow-run-view/job-execution-switcher.d.ts +1 -1
- package/dist/components/workflow-run-view/job-execution-switcher.d.ts.map +1 -1
- package/dist/components/workflow-run-view/job-execution-switcher.js +20 -8
- package/dist/components/workflow-run-view/job-execution-switcher.js.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-view.d.ts +3 -2
- package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-view.js +15 -11
- package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
- package/dist/components/workflow-status/status-visuals.d.ts +4 -4
- package/dist/components/workflow-status/status-visuals.d.ts.map +1 -1
- package/dist/components/workflow-status/status-visuals.js +7 -0
- package/dist/components/workflow-status/status-visuals.js.map +1 -1
- package/dist/components/workflow-status/workflow-status-icon.d.ts +3 -4
- package/dist/components/workflow-status/workflow-status-icon.d.ts.map +1 -1
- package/dist/components/workflow-status/workflow-status-icon.js +6 -4
- package/dist/components/workflow-status/workflow-status-icon.js.map +1 -1
- package/dist/core/entities/job-execution.d.ts +10 -0
- package/dist/core/entities/job-execution.d.ts.map +1 -1
- package/dist/core/entities/job-execution.js +11 -0
- package/dist/core/entities/job-execution.js.map +1 -1
- package/dist/core/entities/job.d.ts +4 -2
- package/dist/core/entities/job.d.ts.map +1 -1
- package/dist/core/entities/job.js +8 -1
- package/dist/core/entities/job.js.map +1 -1
- package/dist/core/entities/workflow-run.d.ts +52 -4
- package/dist/core/entities/workflow-run.d.ts.map +1 -1
- package/dist/core/entities/workflow-run.js +22 -3
- package/dist/core/entities/workflow-run.js.map +1 -1
- package/dist/core/workflow-run.d.ts +6 -6
- package/dist/core/workflow-run.d.ts.map +1 -1
- package/dist/core/workflow-run.js +3 -3
- package/dist/core/workflow-run.js.map +1 -1
- package/dist/feature.d.ts +7 -7
- package/dist/feature.js +5 -5
- package/dist/feature.js.map +1 -1
- package/dist/hooks/api/workflow-run-mapper.d.ts +4 -3
- package/dist/hooks/api/workflow-run-mapper.d.ts.map +1 -1
- package/dist/hooks/api/workflow-run-mapper.js +31 -3
- package/dist/hooks/api/workflow-run-mapper.js.map +1 -1
- package/dist/hooks/api/workflow-runs.d.ts +4 -4
- package/dist/hooks/api/workflow-runs.d.ts.map +1 -1
- package/dist/hooks/api/workflow-runs.js +12 -4
- package/dist/hooks/api/workflow-runs.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/pages/project-workflows-page.js +46 -1
- package/dist/pages/project-workflows-page.js.map +1 -1
- package/dist/pages/workflow-run-detail-page.d.ts +11 -0
- package/dist/pages/workflow-run-detail-page.d.ts.map +1 -0
- package/dist/pages/workflow-run-detail-page.js +30 -0
- package/dist/pages/workflow-run-detail-page.js.map +1 -0
- package/dist/pages/workflow-run-list-page.d.ts +10 -0
- package/dist/pages/workflow-run-list-page.d.ts.map +1 -0
- package/dist/pages/workflow-run-list-page.js +43 -0
- package/dist/pages/workflow-run-list-page.js.map +1 -0
- package/dist/routes/inputs.d.ts +61 -5
- package/dist/routes/inputs.d.ts.map +1 -1
- package/dist/routes/inputs.js +150 -13
- package/dist/routes/inputs.js.map +1 -1
- package/dist/routes/project-route.d.ts +6 -0
- package/dist/routes/project-route.d.ts.map +1 -0
- package/dist/routes/project-route.js +25 -0
- package/dist/routes/project-route.js.map +1 -0
- package/dist/routes/run-detail.d.ts.map +1 -1
- package/dist/routes/run-detail.js +12 -7
- package/dist/routes/run-detail.js.map +1 -1
- package/dist/routes/runs.d.ts.map +1 -1
- package/dist/routes/runs.js +11 -6
- package/dist/routes/runs.js.map +1 -1
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/routes/workflows.js +6 -5
- package/dist/routes/workflows.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/src/components/job-graph/job-duration-format.ts +11 -2
- package/src/components/job-graph/job-graph-view.test.tsx +19 -8
- package/src/components/job-graph/job-graph.stories.tsx +23 -19
- package/src/components/job-graph/job-node.stories.tsx +10 -5
- package/src/components/job-graph/job-node.test.tsx +55 -10
- package/src/components/job-graph/job-node.tsx +16 -9
- package/src/components/step-list/step-list.stories.tsx +2 -3
- package/src/components/step-list/step-list.tsx +3 -2
- package/src/components/workflow-run-list/job-status-strip.tsx +189 -0
- package/src/components/workflow-run-list/run-display.test.ts +169 -30
- package/src/components/workflow-run-list/run-display.ts +116 -9
- package/src/components/workflow-run-list/types.ts +21 -17
- package/src/components/workflow-run-list/workflow-run-filter-menu.tsx +100 -0
- package/src/components/workflow-run-list/workflow-run-filters.tsx +243 -0
- package/src/components/workflow-run-list/workflow-run-list-content.tsx +44 -16
- package/src/components/workflow-run-list/workflow-run-list-states.tsx +128 -27
- package/src/components/workflow-run-list/workflow-run-list-view.test.tsx +441 -104
- package/src/components/workflow-run-list/workflow-run-list-view.tsx +73 -43
- package/src/components/workflow-run-list/workflow-run-list.stories.tsx +140 -20
- package/src/components/workflow-run-list/workflow-run-list.tsx +20 -10
- package/src/components/workflow-run-list/workflow-run-row.tsx +173 -106
- package/src/components/workflow-run-number-label.test.tsx +45 -0
- package/src/components/workflow-run-number-label.tsx +47 -0
- package/src/components/workflow-run-summary/workflow-run-attempt-switcher.test.tsx +7 -8
- package/src/components/workflow-run-summary/workflow-run-attempt-switcher.tsx +13 -12
- package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +6 -8
- package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +27 -2
- package/src/components/workflow-run-summary/workflow-run-summary.tsx +19 -9
- package/src/components/workflow-run-view/agent-config-failure-callout.stories.tsx +2 -3
- package/src/components/workflow-run-view/agent-config-failure-callout.tsx +4 -4
- package/src/components/workflow-run-view/job-card.stories.tsx +11 -10
- package/src/components/workflow-run-view/job-card.test.tsx +115 -0
- package/src/components/workflow-run-view/job-card.tsx +74 -35
- package/src/components/workflow-run-view/job-execution-switcher.tsx +31 -9
- package/src/components/workflow-run-view/workflow-run-view.test.tsx +88 -30
- package/src/components/workflow-run-view/workflow-run-view.tsx +24 -14
- package/src/components/workflow-status/status-visuals.test.ts +9 -0
- package/src/components/workflow-status/status-visuals.ts +6 -4
- package/src/components/workflow-status/workflow-status-icon.stories.tsx +10 -2
- package/src/components/workflow-status/workflow-status-icon.test.tsx +18 -0
- package/src/components/workflow-status/workflow-status-icon.tsx +17 -24
- package/src/core/entities/job-execution.ts +22 -0
- package/src/core/entities/job-status.test.ts +104 -0
- package/src/core/entities/job.ts +18 -3
- package/src/core/entities/workflow-run.ts +76 -7
- package/src/core/workflow-run.test.ts +24 -9
- package/src/core/workflow-run.ts +17 -2
- package/src/feature.ts +5 -5
- package/src/hooks/api/workflow-run-mapper.ts +30 -3
- package/src/hooks/api/workflow-runs.test.tsx +2 -0
- package/src/hooks/api/workflow-runs.ts +12 -7
- package/src/index.ts +7 -2
- package/src/page-harness-budget.test.ts +1 -1
- package/src/pages/project-workflows-page.test.tsx +59 -5
- package/src/pages/project-workflows-page.tsx +34 -1
- package/src/pages/workflow-run-detail-page.tsx +45 -0
- package/src/pages/workflow-run-list-page.tsx +65 -0
- package/src/pages/{workflow-run-page.test.tsx → workflow-run-pages.test.tsx} +211 -47
- package/src/routes/inputs.test.ts +193 -12
- package/src/routes/inputs.ts +170 -11
- package/src/routes/project-route.tsx +25 -0
- package/src/routes/run-detail.tsx +15 -8
- package/src/routes/runs.tsx +14 -7
- package/src/routes/workflows.tsx +5 -4
- package/test/fixtures/workflow-run.ts +76 -7
- package/test/pages.tsx +18 -8
- package/test/render.tsx +8 -2
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/components/workflow-run-list/workflow-run-list-header.d.ts +0 -10
- package/dist/components/workflow-run-list/workflow-run-list-header.d.ts.map +0 -1
- package/dist/components/workflow-run-list/workflow-run-list-header.js +0 -55
- package/dist/components/workflow-run-list/workflow-run-list-header.js.map +0 -1
- package/dist/pages/workflow-run-first-time-use.d.ts +0 -6
- package/dist/pages/workflow-run-first-time-use.d.ts.map +0 -1
- package/dist/pages/workflow-run-first-time-use.js +0 -17
- package/dist/pages/workflow-run-first-time-use.js.map +0 -1
- package/dist/pages/workflow-run-page.d.ts +0 -10
- package/dist/pages/workflow-run-page.d.ts.map +0 -1
- package/dist/pages/workflow-run-page.js +0 -90
- package/dist/pages/workflow-run-page.js.map +0 -1
- package/src/components/workflow-run-list/workflow-run-list-header.tsx +0 -53
- package/src/pages/workflow-run-first-time-use.tsx +0 -17
- package/src/pages/workflow-run-page.tsx +0 -94
|
@@ -14,6 +14,7 @@ import {Tooltip, TooltipContent, TooltipTrigger} from '@shipfox/react-ui/tooltip
|
|
|
14
14
|
import {Header, Text} from '@shipfox/react-ui/typography';
|
|
15
15
|
import type {Ref} from 'react';
|
|
16
16
|
import {useId} from 'react';
|
|
17
|
+
import {WorkflowRunNumberLabel} from '#components/workflow-run-number-label.js';
|
|
17
18
|
import {
|
|
18
19
|
isWorkflowRunTerminal,
|
|
19
20
|
type Job,
|
|
@@ -32,8 +33,8 @@ const STATUS_BADGE_LABEL_WIDTH_CH = Math.max(
|
|
|
32
33
|
type WorkflowRunAction = 'cancel' | 'rerun-all' | 'rerun-menu' | 'none';
|
|
33
34
|
|
|
34
35
|
export interface WorkflowRunSummaryProps {
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
workspaceSlug?: string | undefined;
|
|
37
|
+
projectSlug?: string | undefined;
|
|
37
38
|
run: WorkflowRunDetail;
|
|
38
39
|
sourceAvailable?: boolean | undefined;
|
|
39
40
|
sourceOpen?: boolean | undefined;
|
|
@@ -48,8 +49,8 @@ export interface WorkflowRunSummaryProps {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
export function WorkflowRunSummary({
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
workspaceSlug,
|
|
53
|
+
projectSlug,
|
|
53
54
|
run,
|
|
54
55
|
sourceAvailable = false,
|
|
55
56
|
sourceOpen = false,
|
|
@@ -66,8 +67,8 @@ export function WorkflowRunSummary({
|
|
|
66
67
|
const status = getWorkflowStatusVisual(run.runAttempt.status);
|
|
67
68
|
const action = workflowRunActionForRun(run);
|
|
68
69
|
const attemptSwitcher =
|
|
69
|
-
latestAttempt && latestAttempt > 1 &&
|
|
70
|
-
? {
|
|
70
|
+
latestAttempt && latestAttempt > 1 && workspaceSlug && projectSlug
|
|
71
|
+
? {workspaceSlug, projectSlug, latestAttempt}
|
|
71
72
|
: null;
|
|
72
73
|
const displayDuration = run.runAttempt.displayDuration;
|
|
73
74
|
const {ref: headingTextRef, isTruncated: isHeadingTruncated} =
|
|
@@ -133,10 +134,17 @@ export function WorkflowRunSummary({
|
|
|
133
134
|
</div>
|
|
134
135
|
|
|
135
136
|
<div className="col-span-2 row-start-2 flex min-w-0 items-center gap-12 overflow-hidden text-foreground-neutral-muted">
|
|
137
|
+
{run.number !== null ? (
|
|
138
|
+
<>
|
|
139
|
+
<WorkflowRunNumberLabel run={run} />
|
|
140
|
+
{attemptSwitcher || run.triggerDisplayLabel ? <MetadataSeparator /> : null}
|
|
141
|
+
</>
|
|
142
|
+
) : null}
|
|
143
|
+
|
|
136
144
|
{attemptSwitcher ? (
|
|
137
145
|
<WorkflowRunAttemptSwitcher
|
|
138
|
-
|
|
139
|
-
|
|
146
|
+
workspaceSlug={attemptSwitcher.workspaceSlug}
|
|
147
|
+
projectSlug={attemptSwitcher.projectSlug}
|
|
140
148
|
run={run}
|
|
141
149
|
latestAttempt={attemptSwitcher.latestAttempt}
|
|
142
150
|
/>
|
|
@@ -174,7 +182,9 @@ export function WorkflowRunSummary({
|
|
|
174
182
|
</>
|
|
175
183
|
) : null}
|
|
176
184
|
|
|
177
|
-
{attemptSwitcher || run.triggerDisplayLabel ?
|
|
185
|
+
{run.number !== null || attemptSwitcher || run.triggerDisplayLabel ? (
|
|
186
|
+
<MetadataSeparator />
|
|
187
|
+
) : null}
|
|
178
188
|
<RelativeTime
|
|
179
189
|
value={run.runAttempt.createdAt}
|
|
180
190
|
className="shrink-0 whitespace-nowrap text-xs leading-20 text-foreground-neutral-muted"
|
|
@@ -12,7 +12,6 @@ import {within} from 'storybook/test';
|
|
|
12
12
|
import type {AgentStepConfig, StepError} from '#core/workflow-run.js';
|
|
13
13
|
import {AgentConfigFailureCallout} from './agent-config-failure-callout.js';
|
|
14
14
|
|
|
15
|
-
const WORKSPACE_ID = '44444444-4444-4444-8444-444444444444';
|
|
16
15
|
const AGENTS_LINK_NAME = 'Configure Agents';
|
|
17
16
|
|
|
18
17
|
const config: AgentStepConfig = {
|
|
@@ -42,7 +41,7 @@ const meta = {
|
|
|
42
41
|
});
|
|
43
42
|
const agentSettingsRoute = createRoute({
|
|
44
43
|
getParentRoute: () => rootRoute,
|
|
45
|
-
path: '/
|
|
44
|
+
path: '/w/$workspaceSlug/settings/agents',
|
|
46
45
|
component: () => null,
|
|
47
46
|
});
|
|
48
47
|
const router = createRouter({
|
|
@@ -54,7 +53,7 @@ const meta = {
|
|
|
54
53
|
},
|
|
55
54
|
],
|
|
56
55
|
args: {
|
|
57
|
-
|
|
56
|
+
workspaceSlug: 'acme',
|
|
58
57
|
config,
|
|
59
58
|
error: makeError('provider_not_configured'),
|
|
60
59
|
},
|
|
@@ -10,11 +10,11 @@ import {Link} from '@tanstack/react-router';
|
|
|
10
10
|
import type {AgentStepConfig, StepError} from '#core/workflow-run.js';
|
|
11
11
|
|
|
12
12
|
export function AgentConfigFailureCallout({
|
|
13
|
-
|
|
13
|
+
workspaceSlug,
|
|
14
14
|
config,
|
|
15
15
|
error,
|
|
16
16
|
}: {
|
|
17
|
-
|
|
17
|
+
workspaceSlug?: string | undefined;
|
|
18
18
|
config: AgentStepConfig | null;
|
|
19
19
|
error: StepError | null;
|
|
20
20
|
}) {
|
|
@@ -25,10 +25,10 @@ export function AgentConfigFailureCallout({
|
|
|
25
25
|
<AlertContent>
|
|
26
26
|
<AlertTitle>{copy.title}</AlertTitle>
|
|
27
27
|
<AlertDescription>{copy.description}</AlertDescription>
|
|
28
|
-
{copy.showProviderCta ? (
|
|
28
|
+
{copy.showProviderCta && workspaceSlug ? (
|
|
29
29
|
<AlertActions>
|
|
30
30
|
<Button asChild size="2xs" variant="secondary" iconRight="chevronRight">
|
|
31
|
-
<Link to="/
|
|
31
|
+
<Link to="/w/$workspaceSlug/settings/agents" params={{workspaceSlug}}>
|
|
32
32
|
Configure Agents
|
|
33
33
|
</Link>
|
|
34
34
|
</Button>
|
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
import type {StepExpandedContext} from '../step-list/index.js';
|
|
19
19
|
import {JobCard} from './job-card.js';
|
|
20
20
|
|
|
21
|
-
const WORKSPACE_ID = '44444444-4444-4444-8444-444444444444';
|
|
22
21
|
const SWITCH_EXECUTION_PATTERN = /Switch job execution/;
|
|
23
22
|
const EXECUTION_FIVE_PATTERN = /#5/;
|
|
24
23
|
|
|
@@ -41,7 +40,6 @@ const meta = {
|
|
|
41
40
|
),
|
|
42
41
|
],
|
|
43
42
|
args: {
|
|
44
|
-
workspaceId: WORKSPACE_ID,
|
|
45
43
|
job: singleExecutionJob(),
|
|
46
44
|
selectedJobExecution: singleExecutionJob().jobExecutions[0],
|
|
47
45
|
selectedAttemptId: null,
|
|
@@ -73,7 +71,7 @@ export const States: Story = {
|
|
|
73
71
|
<StorySection label="single execution">
|
|
74
72
|
<JobCardStory job={singleExecutionJob()} />
|
|
75
73
|
</StorySection>
|
|
76
|
-
<StorySection label="
|
|
74
|
+
<StorySection label="execution name with missing job name">
|
|
77
75
|
<JobCardStory job={keyFallbackJob()} />
|
|
78
76
|
</StorySection>
|
|
79
77
|
<StorySection label="queueing execution">
|
|
@@ -126,13 +124,13 @@ export const ListeningProgression: Story = {
|
|
|
126
124
|
<StorySection label="pending, no executions">
|
|
127
125
|
<JobCardStory job={listeningJobPending()} />
|
|
128
126
|
</StorySection>
|
|
129
|
-
<StorySection label="
|
|
127
|
+
<StorySection label="listening, 0 executions">
|
|
130
128
|
<JobCardStory job={listeningJobNoExecutions()} />
|
|
131
129
|
</StorySection>
|
|
132
|
-
<StorySection label="
|
|
130
|
+
<StorySection label="listening, 1 running execution">
|
|
133
131
|
<JobCardStory job={listeningJobOneExecution()} initialJobExecutionId="exec-listen-one-1" />
|
|
134
132
|
</StorySection>
|
|
135
|
-
<StorySection label="
|
|
133
|
+
<StorySection label="listening, mixed executions">
|
|
136
134
|
<JobCardStory job={listeningJobMixedExecutions()} initialJobExecutionId="exec-listen-6" />
|
|
137
135
|
</StorySection>
|
|
138
136
|
<StorySection label="failed, resolved">
|
|
@@ -145,19 +143,19 @@ export const ListeningProgression: Story = {
|
|
|
145
143
|
export const ManyExecutions: Story = {
|
|
146
144
|
render: () => (
|
|
147
145
|
<div className="flex flex-col gap-16">
|
|
148
|
-
<StorySection label="
|
|
146
|
+
<StorySection label="listening, 64 executions, short name">
|
|
149
147
|
<JobCardStory
|
|
150
148
|
job={manyExecutionsListeningJob('gate', 'job-many-short')}
|
|
151
149
|
initialJobExecutionId="exec-many-job-many-short-64"
|
|
152
150
|
/>
|
|
153
151
|
</StorySection>
|
|
154
|
-
<StorySection label="
|
|
152
|
+
<StorySection label="listening, 64 executions, medium name">
|
|
155
153
|
<JobCardStory
|
|
156
154
|
job={manyExecutionsListeningJob('release-gates', 'job-many-medium')}
|
|
157
155
|
initialJobExecutionId="exec-many-job-many-medium-64"
|
|
158
156
|
/>
|
|
159
157
|
</StorySection>
|
|
160
|
-
<StorySection label="
|
|
158
|
+
<StorySection label="listening, 64 executions, long name">
|
|
161
159
|
<JobCardStory
|
|
162
160
|
job={manyExecutionsListeningJob(
|
|
163
161
|
'release-production-multi-region-with-canary-validation',
|
|
@@ -233,7 +231,6 @@ function JobCardStory({
|
|
|
233
231
|
|
|
234
232
|
return (
|
|
235
233
|
<JobCard
|
|
236
|
-
workspaceId={WORKSPACE_ID}
|
|
237
234
|
job={job}
|
|
238
235
|
selectedJobExecution={selectedJobExecution}
|
|
239
236
|
selectedAttemptId={selectedAttemptId}
|
|
@@ -715,6 +712,10 @@ function makeTriggerEvent(
|
|
|
715
712
|
event,
|
|
716
713
|
delivery_id: `delivery-${sequence}`,
|
|
717
714
|
received_at: listeningTimestamp(sequence, 1),
|
|
715
|
+
project: null,
|
|
716
|
+
repository: null,
|
|
717
|
+
ref: null,
|
|
718
|
+
commit: null,
|
|
718
719
|
data: {sequence},
|
|
719
720
|
};
|
|
720
721
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {render, screen} from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import {workflowJob, workflowJobExecutionDto} from '#test/fixtures/workflow-run.js';
|
|
4
|
+
import {JobCard} from './job-card.js';
|
|
5
|
+
import {JobExecutionSwitcher} from './job-execution-switcher.js';
|
|
6
|
+
|
|
7
|
+
const FIRST_EXECUTION_ACCESSIBLE_NAME =
|
|
8
|
+
/Execution #1: Review PR #476, Failed, validation failed, duration 1m 55s/;
|
|
9
|
+
const SECOND_EXECUTION_MENU_ITEM = /Execution #2: Review PR #479/;
|
|
10
|
+
|
|
11
|
+
describe('JobCard execution names', () => {
|
|
12
|
+
test('shows only the resolved execution name', () => {
|
|
13
|
+
const job = workflowJob({
|
|
14
|
+
key: 'deploy',
|
|
15
|
+
name: 'Deploy application',
|
|
16
|
+
job_executions: [
|
|
17
|
+
workflowJobExecutionDto({
|
|
18
|
+
name: 'Deploy production',
|
|
19
|
+
sequence: 1,
|
|
20
|
+
}),
|
|
21
|
+
],
|
|
22
|
+
});
|
|
23
|
+
const execution = job.jobExecutions[0];
|
|
24
|
+
if (!execution) throw new Error('Expected a job execution');
|
|
25
|
+
|
|
26
|
+
render(
|
|
27
|
+
<JobCard
|
|
28
|
+
job={job}
|
|
29
|
+
selectedJobExecution={execution}
|
|
30
|
+
selectedAttemptId={null}
|
|
31
|
+
onSelectedJobExecutionChange={undefined}
|
|
32
|
+
onSelectedAttemptChange={undefined}
|
|
33
|
+
/>,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
expect(screen.getByRole('heading', {name: 'Deploy production'})).toBeInTheDocument();
|
|
37
|
+
expect(screen.queryByText('Deploy application')).not.toBeInTheDocument();
|
|
38
|
+
expect(screen.queryByText('Execution #1')).not.toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('uses the static job name only when no execution exists', () => {
|
|
42
|
+
const job = workflowJob({
|
|
43
|
+
key: 'deploy',
|
|
44
|
+
name: 'Deploy application',
|
|
45
|
+
job_executions: [],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
render(
|
|
49
|
+
<JobCard
|
|
50
|
+
job={job}
|
|
51
|
+
selectedJobExecution={undefined}
|
|
52
|
+
selectedAttemptId={null}
|
|
53
|
+
onSelectedJobExecutionChange={undefined}
|
|
54
|
+
onSelectedAttemptChange={undefined}
|
|
55
|
+
/>,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(screen.getByRole('heading', {name: 'Deploy application'})).toBeInTheDocument();
|
|
59
|
+
expect(screen.getByText('Waiting for this job to start')).toBeInTheDocument();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('JobExecutionSwitcher execution names', () => {
|
|
64
|
+
test('uses each listening execution name while keeping its sequence and status', async () => {
|
|
65
|
+
const user = userEvent.setup();
|
|
66
|
+
const job = workflowJob({
|
|
67
|
+
key: 'reviews',
|
|
68
|
+
name: 'Process review',
|
|
69
|
+
mode: 'listening',
|
|
70
|
+
status: 'running',
|
|
71
|
+
listener_status: 'listening',
|
|
72
|
+
job_executions: [
|
|
73
|
+
workflowJobExecutionDto({
|
|
74
|
+
sequence: 1,
|
|
75
|
+
name: 'Review PR #476',
|
|
76
|
+
status: 'failed',
|
|
77
|
+
status_reason: 'validation failed',
|
|
78
|
+
queued_at: '2026-06-21T12:00:00.000Z',
|
|
79
|
+
started_at: '2026-06-21T12:00:05.000Z',
|
|
80
|
+
finished_at: '2026-06-21T12:02:00.000Z',
|
|
81
|
+
}),
|
|
82
|
+
workflowJobExecutionDto({
|
|
83
|
+
sequence: 2,
|
|
84
|
+
name: 'Review PR #479',
|
|
85
|
+
status: 'running',
|
|
86
|
+
}),
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
const selected = job.jobExecutions[1];
|
|
90
|
+
if (!selected) throw new Error('Expected a selected job execution');
|
|
91
|
+
|
|
92
|
+
render(
|
|
93
|
+
<JobExecutionSwitcher
|
|
94
|
+
job={job}
|
|
95
|
+
selectedJobExecution={selected.id}
|
|
96
|
+
onSelectedJobExecutionChange={() => undefined}
|
|
97
|
+
variant="title"
|
|
98
|
+
/>,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const trigger = screen.getByRole('button', {
|
|
102
|
+
name: 'Switch job execution, currently execution 2: Review PR #479',
|
|
103
|
+
});
|
|
104
|
+
expect(trigger).toBeInTheDocument();
|
|
105
|
+
|
|
106
|
+
await user.click(trigger);
|
|
107
|
+
|
|
108
|
+
expect(
|
|
109
|
+
await screen.findByRole('menuitem', {
|
|
110
|
+
name: FIRST_EXECUTION_ACCESSIBLE_NAME,
|
|
111
|
+
}),
|
|
112
|
+
).toBeInTheDocument();
|
|
113
|
+
expect(screen.getByRole('menuitem', {name: SECOND_EXECUTION_MENU_ITEM})).toBeInTheDocument();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -12,7 +12,11 @@ import {Fragment, useId, useRef, useState} from 'react';
|
|
|
12
12
|
import {getWorkflowStatusVisual} from '#components/workflow-status/status-visuals.js';
|
|
13
13
|
import {
|
|
14
14
|
AGENT_CONFIG_ISSUES,
|
|
15
|
+
defaultJobExecution,
|
|
16
|
+
deriveJobDisplayStatus,
|
|
17
|
+
deriveJobExecutionDisplayStatus,
|
|
15
18
|
type Job,
|
|
19
|
+
type JobDisplayStatus,
|
|
16
20
|
type JobExecution,
|
|
17
21
|
type JobExecutionTime,
|
|
18
22
|
STEP_ERROR_REASONS,
|
|
@@ -30,7 +34,9 @@ import {formatJobExecutionTime, JobExecutionTimeText} from './job-execution-time
|
|
|
30
34
|
import {StepAttemptLogPanel} from './step-attempt-log-panel.js';
|
|
31
35
|
|
|
32
36
|
const STATUS_BADGE_LABEL_WIDTH_CH = Math.max(
|
|
33
|
-
...WORKFLOW_JOB_STATUSES.map(
|
|
37
|
+
...[...WORKFLOW_JOB_STATUSES, 'listening' as const].map(
|
|
38
|
+
(status) => getWorkflowStatusVisual(status).label.length,
|
|
39
|
+
),
|
|
34
40
|
);
|
|
35
41
|
|
|
36
42
|
interface LocalSelectedAttempt {
|
|
@@ -39,7 +45,7 @@ interface LocalSelectedAttempt {
|
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
export function JobCard({
|
|
42
|
-
|
|
48
|
+
workspaceSlug,
|
|
43
49
|
job,
|
|
44
50
|
selectedJobExecution,
|
|
45
51
|
selectedAttemptId,
|
|
@@ -51,7 +57,7 @@ export function JobCard({
|
|
|
51
57
|
focusedSourceStepId,
|
|
52
58
|
onOpenStepSource,
|
|
53
59
|
}: {
|
|
54
|
-
|
|
60
|
+
workspaceSlug?: string | undefined;
|
|
55
61
|
job: Job;
|
|
56
62
|
selectedJobExecution: JobExecution | undefined;
|
|
57
63
|
selectedAttemptId: string | null | undefined;
|
|
@@ -71,7 +77,12 @@ export function JobCard({
|
|
|
71
77
|
jobExecutionId: undefined,
|
|
72
78
|
attemptId: null,
|
|
73
79
|
});
|
|
74
|
-
const
|
|
80
|
+
const title = selectedJobExecution?.displayName ?? job.displayName;
|
|
81
|
+
const isNonDefaultExecutionSelected =
|
|
82
|
+
selectedJobExecution !== undefined && selectedJobExecution.id !== defaultJobExecution(job)?.id;
|
|
83
|
+
const selectedExecutionStatus = isNonDefaultExecutionSelected
|
|
84
|
+
? deriveJobExecutionDisplayStatus(selectedJobExecution)
|
|
85
|
+
: deriveJobDisplayStatus(job);
|
|
75
86
|
const effectiveSelectedAttemptId =
|
|
76
87
|
selectedAttemptId === undefined &&
|
|
77
88
|
localSelectedAttempt.jobExecutionId === selectedJobExecution?.id
|
|
@@ -93,7 +104,7 @@ export function JobCard({
|
|
|
93
104
|
<CarriedOverStepPanel />
|
|
94
105
|
) : (
|
|
95
106
|
<StepAttemptDetailPanel
|
|
96
|
-
|
|
107
|
+
workspaceSlug={workspaceSlug}
|
|
97
108
|
step={step}
|
|
98
109
|
stepId={stepId}
|
|
99
110
|
attempt={attempt}
|
|
@@ -125,7 +136,7 @@ export function JobCard({
|
|
|
125
136
|
onSelectedJobExecutionChange ? (
|
|
126
137
|
<>
|
|
127
138
|
<Text as="h2" id={titleId} size="sm" bold className="sr-only">
|
|
128
|
-
{
|
|
139
|
+
{title}
|
|
129
140
|
</Text>
|
|
130
141
|
<div className="flex min-w-0 items-center gap-8">
|
|
131
142
|
<JobStatusBadge status={selectedExecutionStatus} />
|
|
@@ -140,15 +151,17 @@ export function JobCard({
|
|
|
140
151
|
) : (
|
|
141
152
|
<div className="flex min-w-0 items-center gap-8">
|
|
142
153
|
<JobStatusBadge status={selectedExecutionStatus} />
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
154
|
+
<div className="flex min-w-0 items-center gap-8">
|
|
155
|
+
<Text
|
|
156
|
+
as="h2"
|
|
157
|
+
id={titleId}
|
|
158
|
+
size="sm"
|
|
159
|
+
bold
|
|
160
|
+
className="min-w-0 truncate text-foreground-neutral-base"
|
|
161
|
+
>
|
|
162
|
+
{title}
|
|
163
|
+
</Text>
|
|
164
|
+
</div>
|
|
152
165
|
</div>
|
|
153
166
|
)}
|
|
154
167
|
{sourceAvailable && selectedSourceAction && sourcePanelId && onOpenStepSource ? (
|
|
@@ -196,7 +209,7 @@ export function JobCard({
|
|
|
196
209
|
);
|
|
197
210
|
}
|
|
198
211
|
|
|
199
|
-
function JobStatusBadge({status}: {status:
|
|
212
|
+
function JobStatusBadge({status}: {status: JobDisplayStatus}) {
|
|
200
213
|
const visual = getWorkflowStatusVisual(status);
|
|
201
214
|
|
|
202
215
|
return (
|
|
@@ -252,7 +265,12 @@ function JobExecutionMetadata({execution}: {execution: JobExecution | undefined}
|
|
|
252
265
|
) : item.kind === 'trigger' ? (
|
|
253
266
|
<JobExecutionTriggerMetadata execution={execution} />
|
|
254
267
|
) : (
|
|
255
|
-
<JobExecutionMetadataItem
|
|
268
|
+
<JobExecutionMetadataItem
|
|
269
|
+
icon={item.icon}
|
|
270
|
+
kind={item.kind}
|
|
271
|
+
time={item.time}
|
|
272
|
+
displayStatus={deriveJobExecutionDisplayStatus(execution)}
|
|
273
|
+
/>
|
|
256
274
|
)}
|
|
257
275
|
</Fragment>
|
|
258
276
|
))}
|
|
@@ -309,14 +327,16 @@ function JobExecutionMetadataItem({
|
|
|
309
327
|
icon,
|
|
310
328
|
kind,
|
|
311
329
|
time,
|
|
330
|
+
displayStatus,
|
|
312
331
|
}: {
|
|
313
332
|
icon: 'hourglassLine' | 'timerLine';
|
|
314
333
|
kind: 'queue' | 'run';
|
|
315
334
|
time: JobExecutionTime;
|
|
335
|
+
displayStatus: JobExecution['status'];
|
|
316
336
|
}) {
|
|
317
337
|
useTimeTick();
|
|
318
338
|
|
|
319
|
-
const tooltip = `${jobExecutionTimeVerb(kind, time)} for ${formatJobExecutionTime(time)}`;
|
|
339
|
+
const tooltip = `${jobExecutionTimeVerb(kind, time, displayStatus)} for ${formatJobExecutionTime(time)}`;
|
|
320
340
|
|
|
321
341
|
return (
|
|
322
342
|
<Tooltip>
|
|
@@ -341,9 +361,15 @@ function JobExecutionMetadataItem({
|
|
|
341
361
|
);
|
|
342
362
|
}
|
|
343
363
|
|
|
344
|
-
function jobExecutionTimeVerb(
|
|
345
|
-
|
|
346
|
-
|
|
364
|
+
function jobExecutionTimeVerb(
|
|
365
|
+
kind: 'queue' | 'run',
|
|
366
|
+
time: JobExecutionTime,
|
|
367
|
+
displayStatus: JobExecution['status'],
|
|
368
|
+
): string {
|
|
369
|
+
if (kind === 'queue') return time.state === 'live' ? 'Queueing' : 'Queued';
|
|
370
|
+
if (time.state !== 'live') return 'Ran';
|
|
371
|
+
if (displayStatus === 'running') return 'Running';
|
|
372
|
+
return displayStatus === 'pending' ? 'Runner assigned' : 'Execution elapsed';
|
|
347
373
|
}
|
|
348
374
|
|
|
349
375
|
function MetadataSeparator() {
|
|
@@ -369,14 +395,14 @@ function selectedStepSourceAction(
|
|
|
369
395
|
}
|
|
370
396
|
|
|
371
397
|
function StepAttemptDetailPanel({
|
|
372
|
-
|
|
398
|
+
workspaceSlug,
|
|
373
399
|
step,
|
|
374
400
|
stepId,
|
|
375
401
|
attempt,
|
|
376
402
|
attemptError,
|
|
377
403
|
attemptStatus,
|
|
378
404
|
}: {
|
|
379
|
-
|
|
405
|
+
workspaceSlug?: string | undefined;
|
|
380
406
|
step: Step;
|
|
381
407
|
stepId: string;
|
|
382
408
|
attempt: number;
|
|
@@ -389,7 +415,7 @@ function StepAttemptDetailPanel({
|
|
|
389
415
|
<div className="flex min-w-0 flex-col gap-10">
|
|
390
416
|
{isAgentConfigFailure(step, selectedAttemptError) ? (
|
|
391
417
|
<AgentConfigFailureCallout
|
|
392
|
-
|
|
418
|
+
workspaceSlug={workspaceSlug}
|
|
393
419
|
config={step.agentConfig}
|
|
394
420
|
error={selectedAttemptError}
|
|
395
421
|
/>
|
|
@@ -432,7 +458,7 @@ function toSelectedAttemptError(
|
|
|
432
458
|
signal: typeof error.signal === 'string' ? error.signal : undefined,
|
|
433
459
|
reason: resolvedReason,
|
|
434
460
|
agentConfigIssue,
|
|
435
|
-
category: step.type === 'setup' ? 'setup' : 'user',
|
|
461
|
+
category: step.type === 'setup' || step.type === 'checkout' ? 'setup' : 'user',
|
|
436
462
|
};
|
|
437
463
|
}
|
|
438
464
|
|
|
@@ -449,35 +475,48 @@ function emptyStateForJob(job: Job, jobExecution: JobExecution): StepListEmptySt
|
|
|
449
475
|
};
|
|
450
476
|
}
|
|
451
477
|
|
|
452
|
-
|
|
478
|
+
const displayStatus =
|
|
479
|
+
job.mode === 'listening'
|
|
480
|
+
? deriveJobExecutionDisplayStatus(jobExecution)
|
|
481
|
+
: deriveJobDisplayStatus(job);
|
|
482
|
+
|
|
483
|
+
if (jobExecution.status === 'running' && displayStatus === 'pending') {
|
|
484
|
+
return {
|
|
485
|
+
title: 'Runner preparing job',
|
|
486
|
+
description: 'A runner is assigned. Steps will appear here when work begins.',
|
|
487
|
+
status: displayStatus,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (displayStatus === 'pending') {
|
|
453
492
|
return {
|
|
454
493
|
title: 'Waiting for this job to start',
|
|
455
494
|
description: 'Steps will appear here once the job starts.',
|
|
456
|
-
status:
|
|
495
|
+
status: displayStatus,
|
|
457
496
|
};
|
|
458
497
|
}
|
|
459
498
|
|
|
460
|
-
if (
|
|
499
|
+
if (displayStatus === 'running') {
|
|
461
500
|
return {
|
|
462
501
|
title: 'Waiting for the first step',
|
|
463
|
-
description: '
|
|
464
|
-
status:
|
|
502
|
+
description: 'The first running step will appear here shortly.',
|
|
503
|
+
status: displayStatus,
|
|
465
504
|
};
|
|
466
505
|
}
|
|
467
506
|
|
|
468
|
-
if (
|
|
507
|
+
if (displayStatus === 'cancelled') {
|
|
469
508
|
return {
|
|
470
509
|
title: 'Cancelled before start',
|
|
471
510
|
description: 'This job was cancelled before any step started.',
|
|
472
|
-
status:
|
|
511
|
+
status: displayStatus,
|
|
473
512
|
};
|
|
474
513
|
}
|
|
475
514
|
|
|
476
|
-
if (
|
|
515
|
+
if (displayStatus === 'succeeded' || displayStatus === 'failed') {
|
|
477
516
|
return {
|
|
478
517
|
title: 'No steps recorded',
|
|
479
518
|
description: `Execution #${jobExecution.sequence} finished without recorded steps.`,
|
|
480
|
-
status:
|
|
519
|
+
status: displayStatus,
|
|
481
520
|
};
|
|
482
521
|
}
|
|
483
522
|
|
|
@@ -497,7 +536,7 @@ function emptyStateForMissingExecution(job: Job): StepListEmptyState {
|
|
|
497
536
|
return {
|
|
498
537
|
title: 'Waiting for trigger events',
|
|
499
538
|
description: 'Matching trigger events will create job executions here.',
|
|
500
|
-
status:
|
|
539
|
+
status: deriveJobDisplayStatus(job),
|
|
501
540
|
};
|
|
502
541
|
}
|
|
503
542
|
|
|
@@ -9,9 +9,10 @@ import {Icon} from '@shipfox/react-ui/icon';
|
|
|
9
9
|
import {Text} from '@shipfox/react-ui/typography';
|
|
10
10
|
import {cn} from '@shipfox/react-ui/utils';
|
|
11
11
|
import {useMemo} from 'react';
|
|
12
|
+
import {getWorkflowStatusVisual} from '#components/workflow-status/status-visuals.js';
|
|
12
13
|
import {WorkflowStatusIcon} from '#components/workflow-status/workflow-status-icon.js';
|
|
13
|
-
import type
|
|
14
|
-
import {JobExecutionTimeText} from './job-execution-time-text.js';
|
|
14
|
+
import {deriveJobExecutionDisplayStatus, type Job, type JobExecution} from '#core/workflow-run.js';
|
|
15
|
+
import {formatJobExecutionTime, JobExecutionTimeText} from './job-execution-time-text.js';
|
|
15
16
|
|
|
16
17
|
export interface JobExecutionSwitcherProps {
|
|
17
18
|
job: Job;
|
|
@@ -62,10 +63,10 @@ export function JobExecutionSwitcher({
|
|
|
62
63
|
: 'min-h-28 gap-6 px-8 py-4 text-sm leading-20 text-foreground-neutral-subtle hover:bg-background-components-hover',
|
|
63
64
|
className,
|
|
64
65
|
)}
|
|
65
|
-
aria-label={`Switch job execution, currently execution ${selected.sequence}`}
|
|
66
|
+
aria-label={`Switch job execution, currently execution ${selected.sequence}: ${selected.displayName}`}
|
|
66
67
|
>
|
|
67
68
|
{variant === 'title' ? (
|
|
68
|
-
<TitleExecutionSummary
|
|
69
|
+
<TitleExecutionSummary execution={selected} />
|
|
69
70
|
) : (
|
|
70
71
|
<ExecutionSummary execution={selected} />
|
|
71
72
|
)}
|
|
@@ -87,14 +88,19 @@ export function JobExecutionSwitcher({
|
|
|
87
88
|
<button
|
|
88
89
|
type="button"
|
|
89
90
|
aria-current={isSelected ? 'true' : undefined}
|
|
91
|
+
aria-label={executionAccessibleLabel(jobExecution)}
|
|
90
92
|
className="w-full text-left"
|
|
91
93
|
>
|
|
92
|
-
<WorkflowStatusIcon
|
|
94
|
+
<WorkflowStatusIcon
|
|
95
|
+
status={deriveJobExecutionDisplayStatus(jobExecution)}
|
|
96
|
+
size={14}
|
|
97
|
+
tooltip={false}
|
|
98
|
+
/>
|
|
93
99
|
<span className="font-code text-xs leading-20 text-foreground-neutral-base tabular-nums">
|
|
94
100
|
#{jobExecution.sequence}
|
|
95
101
|
</span>
|
|
96
102
|
<span className="min-w-0 truncate text-xs leading-20 text-foreground-neutral-base">
|
|
97
|
-
{
|
|
103
|
+
{jobExecution.displayName}
|
|
98
104
|
</span>
|
|
99
105
|
{!isSelected && jobExecution.statusReason ? (
|
|
100
106
|
<span className="min-w-0 flex-1 truncate text-xs leading-20 text-foreground-neutral-muted">
|
|
@@ -119,23 +125,39 @@ export function JobExecutionSwitcher({
|
|
|
119
125
|
);
|
|
120
126
|
}
|
|
121
127
|
|
|
122
|
-
function
|
|
128
|
+
function executionAccessibleLabel(execution: JobExecution): string {
|
|
129
|
+
const status = getWorkflowStatusVisual(deriveJobExecutionDisplayStatus(execution));
|
|
130
|
+
return [
|
|
131
|
+
`Execution #${execution.sequence}: ${execution.displayName}`,
|
|
132
|
+
status.label,
|
|
133
|
+
execution.statusReason ?? undefined,
|
|
134
|
+
execution.displayDuration
|
|
135
|
+
? `duration ${formatJobExecutionTime(execution.displayDuration)}`
|
|
136
|
+
: undefined,
|
|
137
|
+
]
|
|
138
|
+
.filter((part): part is string => Boolean(part))
|
|
139
|
+
.join(', ');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function TitleExecutionSummary({execution}: {execution: JobExecution}) {
|
|
123
143
|
return (
|
|
124
144
|
<span className="flex min-w-0 items-center gap-8">
|
|
125
145
|
<Text as="span" size="sm" bold className="shrink-0 text-foreground-neutral-base">
|
|
126
146
|
#{execution.sequence}
|
|
127
147
|
</Text>
|
|
128
148
|
<Text as="span" size="sm" bold className="min-w-0 truncate text-foreground-neutral-base">
|
|
129
|
-
{
|
|
149
|
+
{execution.displayName}
|
|
130
150
|
</Text>
|
|
131
151
|
</span>
|
|
132
152
|
);
|
|
133
153
|
}
|
|
134
154
|
|
|
135
155
|
function ExecutionSummary({execution}: {execution: JobExecution}) {
|
|
156
|
+
const displayStatus = deriveJobExecutionDisplayStatus(execution);
|
|
157
|
+
|
|
136
158
|
return (
|
|
137
159
|
<span className="flex min-w-0 items-center gap-6">
|
|
138
|
-
<WorkflowStatusIcon status={
|
|
160
|
+
<WorkflowStatusIcon status={displayStatus} size={14} tooltip={false} />
|
|
139
161
|
<span className="shrink-0 font-code text-xs leading-20 text-foreground-neutral-base tabular-nums">
|
|
140
162
|
Execution #{execution.sequence}
|
|
141
163
|
</span>
|