@shipfox/client-workflows 16.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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +20 -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 +3 -1
- package/dist/components/job-detail/job-detail-header.d.ts.map +1 -1
- package/dist/components/job-detail/job-detail-header.js +54 -48
- 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 +80 -44
- 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 +8 -13
- package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
- package/dist/components/workflow-run-tabs/index.d.ts +1 -1
- package/dist/components/workflow-run-tabs/index.d.ts.map +1 -1
- package/dist/components/workflow-run-tabs/index.js.map +1 -1
- package/dist/components/workflow-run-tabs/run-annotation-count-chip.js +2 -2
- package/dist/components/workflow-run-tabs/run-annotation-count-chip.js.map +1 -1
- package/dist/components/workflow-run-tabs/run-annotation-list.d.ts +9 -2
- package/dist/components/workflow-run-tabs/run-annotation-list.d.ts.map +1 -1
- package/dist/components/workflow-run-tabs/run-annotation-list.js +28 -19
- package/dist/components/workflow-run-tabs/run-annotation-list.js.map +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 +5 -5
- package/dist/components/workflow-run-tabs/run-annotation-summary-line.js.map +1 -1
- package/dist/components/workflow-run-view/run-workspace-nav.js +7 -7
- 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.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-view.js +72 -12
- 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 +6 -0
- package/dist/core/run-annotation.d.ts.map +1 -1
- package/dist/core/run-annotation.js.map +1 -1
- 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/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/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +10 -10
- 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.tsx +52 -46
- package/src/components/job-detail/job-detail-view.stories.tsx +555 -53
- package/src/components/job-detail/job-detail-view.tsx +87 -45
- 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 +11 -11
- package/src/components/workflow-run-tabs/index.ts +5 -1
- package/src/components/workflow-run-tabs/run-annotation-count-chip.tsx +2 -2
- package/src/components/workflow-run-tabs/run-annotation-list.tsx +25 -10
- package/src/components/workflow-run-tabs/run-annotation-summary-line.tsx +5 -5
- package/src/components/workflow-run-view/run-workspace-nav.tsx +7 -7
- package/src/components/workflow-run-view/workflow-run-states.tsx +6 -6
- package/src/components/workflow-run-view/workflow-run-view.tsx +114 -14
- 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.ts +6 -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/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 +52 -1
- package/src/pages/workflow-run-list-page.tsx +1 -1
- package/test/fixtures/workflow-run.ts +7 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -9,9 +9,11 @@ import {Skeleton} from '@shipfox/react-ui/skeleton';
|
|
|
9
9
|
import {TimeTickerProvider} from '@shipfox/react-ui/time-ticker';
|
|
10
10
|
import {Text} from '@shipfox/react-ui/typography';
|
|
11
11
|
import {Link} from '@tanstack/react-router';
|
|
12
|
-
import {type RefObject, useEffect, useRef} from 'react';
|
|
12
|
+
import {type RefObject, useEffect, useRef, useState} from 'react';
|
|
13
|
+
import type {RunAnnotationSummary} from '#core/run-annotation.js';
|
|
13
14
|
import {summarizeJobAnnotations} from '#core/run-annotation.js';
|
|
14
|
-
import type
|
|
15
|
+
import {isWorkflowRunTerminal, type Job, type JobExecution} from '#core/workflow-run.js';
|
|
16
|
+
import {useWorkflowRunAnnotationSummaryQuery} from '#hooks/api/annotations.js';
|
|
15
17
|
import {useRunAnnotationsQuery} from '#hooks/api/run-annotations.js';
|
|
16
18
|
import type {useWorkflowRunAttemptQuery} from '#hooks/api/workflow-runs.js';
|
|
17
19
|
import {
|
|
@@ -24,15 +26,13 @@ import {
|
|
|
24
26
|
WorkflowRunNotFound,
|
|
25
27
|
WorkflowRunStaleError,
|
|
26
28
|
} from '../workflow-run-view/workflow-run-states.js';
|
|
27
|
-
import {
|
|
29
|
+
import {JobContextPanel} from './job-context-panel.js';
|
|
28
30
|
import {JobDetailHeader} from './job-detail-header.js';
|
|
29
31
|
import {
|
|
30
32
|
CarriedOverStepPanel,
|
|
31
33
|
emptyStateForJob,
|
|
32
34
|
emptyStateForMissingExecution,
|
|
33
|
-
isAgentConfigFailure,
|
|
34
35
|
jobSucceededSummary,
|
|
35
|
-
toSelectedAttemptError,
|
|
36
36
|
} from './job-empty-states.js';
|
|
37
37
|
import {
|
|
38
38
|
resolveWorkflowJobSelection,
|
|
@@ -40,6 +40,9 @@ import {
|
|
|
40
40
|
workflowJobLandingSelection,
|
|
41
41
|
} from './job-selection.js';
|
|
42
42
|
import {StepAttemptLogPanel} from './step-attempt-log-panel.js';
|
|
43
|
+
import {StepInspectorSheet} from './step-troubleshooting.js';
|
|
44
|
+
|
|
45
|
+
type InspectorState = {key: string; attemptId: string | null};
|
|
43
46
|
|
|
44
47
|
export interface JobDetailViewProps {
|
|
45
48
|
workspaceSlug: string;
|
|
@@ -68,7 +71,8 @@ export function JobDetailView({
|
|
|
68
71
|
const pageScrollRef = useRef<HTMLDivElement>(null);
|
|
69
72
|
const landingSelectionRef = useRef<FrozenLandingSelection | undefined>(undefined);
|
|
70
73
|
const hasLoadedData = query.data !== undefined;
|
|
71
|
-
//
|
|
74
|
+
// Reuse the run workspace's bounded annotation read for the job header chip. The separate
|
|
75
|
+
// summary query below stays counts-only and is scoped to the inspector's selected execution.
|
|
72
76
|
const annotations = useRunAnnotationsQuery({
|
|
73
77
|
workflowRunId,
|
|
74
78
|
runAttempt: query.data?.runAttempt.attempt,
|
|
@@ -78,6 +82,26 @@ export function JobDetailView({
|
|
|
78
82
|
truncated: annotations.summary?.truncated ?? false,
|
|
79
83
|
})
|
|
80
84
|
: undefined;
|
|
85
|
+
const inspectorResetKey = `${jobId}:${search.jobExecutionId ?? ''}`;
|
|
86
|
+
const [inspectorState, setInspectorState] = useState<InspectorState>(() => ({
|
|
87
|
+
key: inspectorResetKey,
|
|
88
|
+
attemptId: null,
|
|
89
|
+
}));
|
|
90
|
+
const inspectorOpenAttemptId =
|
|
91
|
+
inspectorState.key === inspectorResetKey ? inspectorState.attemptId : null;
|
|
92
|
+
const annotationJob = query.data?.jobs.find((candidate) => candidate.id === jobId);
|
|
93
|
+
const annotationExecutionId = annotationJob
|
|
94
|
+
? resolveWorkflowJobSelection({job: annotationJob, selection: search}).jobExecution?.id
|
|
95
|
+
: search.jobExecutionId;
|
|
96
|
+
const annotationPolling = query.data
|
|
97
|
+
? !isWorkflowRunTerminal(query.data.runAttempt.status)
|
|
98
|
+
: true;
|
|
99
|
+
const annotationSummaryQuery = useWorkflowRunAnnotationSummaryQuery(
|
|
100
|
+
query.data?.id,
|
|
101
|
+
query.data?.runAttempt.attempt,
|
|
102
|
+
annotationExecutionId,
|
|
103
|
+
{polling: annotationPolling},
|
|
104
|
+
);
|
|
81
105
|
|
|
82
106
|
useEffect(() => {
|
|
83
107
|
if (!jobId || !hasLoadedData) return;
|
|
@@ -185,6 +209,10 @@ export function JobDetailView({
|
|
|
185
209
|
});
|
|
186
210
|
}
|
|
187
211
|
|
|
212
|
+
function onInspectorOpenChange(attemptId: string | null) {
|
|
213
|
+
setInspectorState({key: inspectorResetKey, attemptId});
|
|
214
|
+
}
|
|
215
|
+
|
|
188
216
|
function retargetToRunningStep() {
|
|
189
217
|
if (!runningSelection || !selectedJobExecution) return;
|
|
190
218
|
onSelectionChange({
|
|
@@ -208,11 +236,8 @@ export function JobDetailView({
|
|
|
208
236
|
attempt={newerAttempt}
|
|
209
237
|
/>
|
|
210
238
|
) : null}
|
|
211
|
-
<div ref={pageScrollRef} className="@container min-h-0 flex-1 overflow-auto pb-
|
|
212
|
-
<section
|
|
213
|
-
aria-label={`${job.displayName} logs`}
|
|
214
|
-
className="mx-auto flex w-full max-w-[1120px] flex-col gap-8 px-24"
|
|
215
|
-
>
|
|
239
|
+
<div ref={pageScrollRef} className="@container min-h-0 flex-1 overflow-auto pb-panel">
|
|
240
|
+
<section aria-label={`${job.displayName} logs`} className="flex w-full flex-col">
|
|
216
241
|
<section className="min-w-0 overflow-hidden">
|
|
217
242
|
<JobDetailHeader
|
|
218
243
|
job={job}
|
|
@@ -220,11 +245,16 @@ export function JobDetailView({
|
|
|
220
245
|
onSelectedJobExecutionChange={selectExecution}
|
|
221
246
|
workspaceSlug={workspaceSlug}
|
|
222
247
|
projectSlug={projectSlug}
|
|
223
|
-
workflowRunId={
|
|
248
|
+
workflowRunId={run.id}
|
|
224
249
|
runAttempt={run.runAttempt.attempt}
|
|
225
250
|
annotationSummary={jobAnnotationSummary}
|
|
251
|
+
jobContext={
|
|
252
|
+
selectedJobExecution ? (
|
|
253
|
+
<JobContextPanel job={job} execution={selectedJobExecution} />
|
|
254
|
+
) : undefined
|
|
255
|
+
}
|
|
226
256
|
/>
|
|
227
|
-
<Text as="
|
|
257
|
+
<Text as="h2" className="sr-only">
|
|
228
258
|
Logs
|
|
229
259
|
</Text>
|
|
230
260
|
{selectedJobExecution ? (
|
|
@@ -234,15 +264,30 @@ export function JobDetailView({
|
|
|
234
264
|
selectedAttemptId={selectedAttemptId}
|
|
235
265
|
defaultSelectedAttemptId={landingSelection?.attemptId}
|
|
236
266
|
onSelectedAttemptChange={selectAttempt}
|
|
267
|
+
inspectorOpenAttemptId={inspectorOpenAttemptId}
|
|
268
|
+
onInspectorOpenChange={onInspectorOpenChange}
|
|
237
269
|
autoSelectActiveAttempt
|
|
238
270
|
emptyState={emptyStateForJob(job, selectedJobExecution)}
|
|
239
271
|
showHeader={false}
|
|
240
|
-
className="rounded-none border-0"
|
|
272
|
+
className="rounded-none border-0 bg-transparent"
|
|
241
273
|
renderExpandedStep={(context) => (
|
|
242
|
-
<ExpandedStep
|
|
243
|
-
|
|
274
|
+
<ExpandedStep context={context} pageScrollRef={pageScrollRef} />
|
|
275
|
+
)}
|
|
276
|
+
renderInspector={(entry) => (
|
|
277
|
+
<StepInspectorSheet
|
|
278
|
+
entry={entry}
|
|
279
|
+
open
|
|
280
|
+
onOpenChange={(open) => onInspectorOpenChange(open ? entry.id : null)}
|
|
244
281
|
workspaceSlug={workspaceSlug}
|
|
245
|
-
|
|
282
|
+
projectSlug={projectSlug}
|
|
283
|
+
workflowRunId={run.id}
|
|
284
|
+
runAttempt={run.runAttempt.attempt}
|
|
285
|
+
jobId={job.id}
|
|
286
|
+
annotationCount={annotationCountForStep(
|
|
287
|
+
annotationSummaryQuery.data,
|
|
288
|
+
entry.step.id,
|
|
289
|
+
entry.attempt,
|
|
290
|
+
)}
|
|
246
291
|
/>
|
|
247
292
|
)}
|
|
248
293
|
/>
|
|
@@ -254,7 +299,7 @@ export function JobDetailView({
|
|
|
254
299
|
<div
|
|
255
300
|
role="status"
|
|
256
301
|
aria-live="polite"
|
|
257
|
-
className="flex min-w-0 items-center justify-between gap-
|
|
302
|
+
className="flex min-w-0 items-center justify-between gap-inline border-t border-border-neutral-base px-row py-row"
|
|
258
303
|
>
|
|
259
304
|
<Text size="xs" className="min-w-0 text-foreground-neutral-muted">
|
|
260
305
|
Run moved on to{' '}
|
|
@@ -265,7 +310,7 @@ export function JobDetailView({
|
|
|
265
310
|
</Text>
|
|
266
311
|
<button
|
|
267
312
|
type="button"
|
|
268
|
-
className="shrink-0 rounded-4 px-
|
|
313
|
+
className="shrink-0 rounded-4 px-tight py-[4px] text-xs font-medium text-foreground-highlight-interactive outline-none focus-visible:shadow-border-interactive-with-active"
|
|
269
314
|
onClick={retargetToRunningStep}
|
|
270
315
|
>
|
|
271
316
|
Jump to it
|
|
@@ -273,7 +318,7 @@ export function JobDetailView({
|
|
|
273
318
|
</div>
|
|
274
319
|
) : null}
|
|
275
320
|
{succeededSummary ? (
|
|
276
|
-
<Text size="xs" className="px-
|
|
321
|
+
<Text size="xs" className="px-row py-row text-foreground-neutral-muted">
|
|
277
322
|
{succeededSummary}
|
|
278
323
|
</Text>
|
|
279
324
|
) : null}
|
|
@@ -286,39 +331,27 @@ export function JobDetailView({
|
|
|
286
331
|
|
|
287
332
|
function ExpandedStep({
|
|
288
333
|
context,
|
|
289
|
-
workspaceSlug,
|
|
290
334
|
pageScrollRef,
|
|
291
335
|
}: {
|
|
292
336
|
context: StepExpandedContext;
|
|
293
|
-
workspaceSlug: string;
|
|
294
337
|
pageScrollRef: RefObject<HTMLDivElement | null>;
|
|
295
338
|
}) {
|
|
296
339
|
if (context.carriedOver) return <CarriedOverStepPanel />;
|
|
297
340
|
|
|
298
|
-
const selectedAttemptError =
|
|
299
|
-
toSelectedAttemptError(context.step, context.attemptError) ?? context.step.error;
|
|
300
|
-
|
|
301
341
|
return (
|
|
302
342
|
<section
|
|
303
343
|
role="region"
|
|
304
344
|
tabIndex={0}
|
|
305
345
|
aria-label={`${context.stepLabel} output, attempt ${context.attempt}`}
|
|
306
|
-
className="flex min-w-0 flex-col
|
|
346
|
+
className="flex min-w-0 flex-col border-t border-border-neutral-base bg-background-neutral-base outline-none focus-visible:shadow-border-interactive-with-active dark:bg-background-contrast-subtle"
|
|
307
347
|
>
|
|
308
|
-
{isAgentConfigFailure(context.step, selectedAttemptError) ? (
|
|
309
|
-
<AgentConfigFailureCallout
|
|
310
|
-
workspaceSlug={workspaceSlug}
|
|
311
|
-
config={context.step.agentConfig}
|
|
312
|
-
error={selectedAttemptError}
|
|
313
|
-
/>
|
|
314
|
-
) : null}
|
|
315
348
|
<StepAttemptLogPanel
|
|
316
349
|
stepId={context.stepId}
|
|
317
350
|
attempt={context.attempt}
|
|
318
351
|
attemptStatus={context.attemptStatus}
|
|
319
352
|
attemptStartedAt={context.attemptStartedAt}
|
|
320
353
|
pageScrollRef={pageScrollRef}
|
|
321
|
-
surfaceClassName="rounded-
|
|
354
|
+
surfaceClassName="rounded-none border-0 bg-transparent shadow-none"
|
|
322
355
|
/>
|
|
323
356
|
</section>
|
|
324
357
|
);
|
|
@@ -328,7 +361,7 @@ function EmptyStateForMissingExecution({job}: {job: Job}) {
|
|
|
328
361
|
const emptyState = emptyStateForMissingExecution(job);
|
|
329
362
|
return (
|
|
330
363
|
<EmptyState
|
|
331
|
-
className="min-h-120
|
|
364
|
+
className="min-h-120 p-panel"
|
|
332
365
|
icon="componentLine"
|
|
333
366
|
title={emptyState.title}
|
|
334
367
|
description={emptyState.description}
|
|
@@ -349,7 +382,7 @@ function JobNotFoundState({
|
|
|
349
382
|
search: WorkflowJobSearch;
|
|
350
383
|
}) {
|
|
351
384
|
return (
|
|
352
|
-
<div className="flex min-h-0 min-w-0 flex-1 flex-col items-center justify-center gap-
|
|
385
|
+
<div className="flex min-h-0 min-w-0 flex-1 flex-col items-center justify-center gap-cluster p-panel">
|
|
353
386
|
<EmptyState
|
|
354
387
|
icon="componentLine"
|
|
355
388
|
title="Job not found"
|
|
@@ -364,7 +397,7 @@ function JobNotFoundState({
|
|
|
364
397
|
{runAttempt: search.runAttempt},
|
|
365
398
|
) as never
|
|
366
399
|
}
|
|
367
|
-
className="inline-flex items-center gap-
|
|
400
|
+
className="inline-flex items-center gap-inline rounded-6 px-tight py-[6px] text-sm text-foreground-highlight-interactive outline-none focus-visible:shadow-border-interactive-with-active"
|
|
368
401
|
>
|
|
369
402
|
<Icon name="arrowLeftLine" size={14} aria-hidden="true" />
|
|
370
403
|
Back to run summary
|
|
@@ -387,8 +420,8 @@ function NewerAttemptNotice({
|
|
|
387
420
|
attempt: number;
|
|
388
421
|
}) {
|
|
389
422
|
return (
|
|
390
|
-
<div role="status" className="border-b border-border-neutral-base px-
|
|
391
|
-
<div className="
|
|
423
|
+
<div role="status" className="border-b border-border-neutral-base px-row py-row">
|
|
424
|
+
<div className="flex items-center justify-between gap-inline">
|
|
392
425
|
<Text size="xs" className="text-foreground-neutral-muted">
|
|
393
426
|
A newer run attempt is available.
|
|
394
427
|
</Text>
|
|
@@ -396,7 +429,7 @@ function NewerAttemptNotice({
|
|
|
396
429
|
to="/w/$workspaceSlug/p/$projectSlug/runs/$workflowRunId/jobs/$jobId"
|
|
397
430
|
params={{workspaceSlug, projectSlug, workflowRunId: runId, jobId}}
|
|
398
431
|
search={workflowJobSearchParams({runAttempt: attempt}) as never}
|
|
399
|
-
className="shrink-0 rounded-4 px-
|
|
432
|
+
className="shrink-0 rounded-4 px-tight py-[4px] text-xs font-medium text-foreground-highlight-interactive outline-none focus-visible:shadow-border-interactive-with-active"
|
|
400
433
|
>
|
|
401
434
|
View attempt #{attempt}
|
|
402
435
|
</Link>
|
|
@@ -431,21 +464,30 @@ function findAttempt(jobExecution: JobExecution, attemptId: string) {
|
|
|
431
464
|
return undefined;
|
|
432
465
|
}
|
|
433
466
|
|
|
467
|
+
function annotationCountForStep(
|
|
468
|
+
summary: RunAnnotationSummary | undefined,
|
|
469
|
+
stepId: string,
|
|
470
|
+
attempt: number,
|
|
471
|
+
): number | undefined {
|
|
472
|
+
return summary?.stepCounts?.find((entry) => entry.stepId === stepId && entry.attempt === attempt)
|
|
473
|
+
?.total;
|
|
474
|
+
}
|
|
475
|
+
|
|
434
476
|
function JobDetailSkeleton() {
|
|
435
477
|
return (
|
|
436
|
-
<div className="min-h-0 min-w-0 flex-1 overflow-auto
|
|
437
|
-
<div className="
|
|
438
|
-
<header className="flex items-center gap-
|
|
478
|
+
<div className="min-h-0 min-w-0 flex-1 overflow-auto pb-panel">
|
|
479
|
+
<div className="w-full">
|
|
480
|
+
<header className="flex items-center gap-cluster border-b border-border-neutral-base px-row py-row">
|
|
439
481
|
<Skeleton className="size-20 rounded-full" />
|
|
440
482
|
<Skeleton className="h-20 w-160 rounded-4" />
|
|
441
483
|
<Skeleton className="h-24 w-72 rounded-6" />
|
|
442
484
|
<Skeleton className="h-24 w-180 rounded-4" />
|
|
443
485
|
</header>
|
|
444
|
-
<div className="p-
|
|
486
|
+
<div className="p-panel-compact">
|
|
445
487
|
{JOB_DETAIL_SKELETON_ROWS.map((row) => (
|
|
446
488
|
<div
|
|
447
489
|
key={row}
|
|
448
|
-
className="flex min-h-44 items-center gap-
|
|
490
|
+
className="flex min-h-44 items-center gap-inline border-b border-border-neutral-base last:border-b-0"
|
|
449
491
|
>
|
|
450
492
|
<Skeleton className="size-14 rounded-full" />
|
|
451
493
|
<Skeleton className="h-16 w-180 rounded-4" />
|
|
@@ -25,11 +25,14 @@ export function emptyStateForJob(
|
|
|
25
25
|
job.mode === 'listening'
|
|
26
26
|
? deriveJobExecutionDisplayStatus(jobExecution)
|
|
27
27
|
: deriveJobDisplayStatus(job);
|
|
28
|
+
const runner = jobExecution.runner?.length ? jobExecution.runner : job.runner;
|
|
28
29
|
|
|
29
30
|
if (jobExecution.status === 'running' && displayStatus === 'pending') {
|
|
30
31
|
return {
|
|
31
|
-
title: 'Runner preparing job',
|
|
32
|
-
description:
|
|
32
|
+
title: runner?.length ? 'Runner preparing job' : 'Waiting for a runner',
|
|
33
|
+
description: runner?.length
|
|
34
|
+
? `Runner ${runner.join(', ')} is preparing the job. Steps will appear here when work begins.`
|
|
35
|
+
: 'No runner has claimed this job yet. Steps will appear here when a runner starts work.',
|
|
33
36
|
status: displayStatus,
|
|
34
37
|
};
|
|
35
38
|
}
|
|
@@ -61,7 +64,7 @@ export function emptyStateForJob(
|
|
|
61
64
|
if (displayStatus === 'failed') {
|
|
62
65
|
return {
|
|
63
66
|
title: 'Job failed before its first step started',
|
|
64
|
-
description: preStepFailureDescription(jobExecution.statusReason ?? job.statusReason),
|
|
67
|
+
description: preStepFailureDescription(jobExecution.statusReason ?? job.statusReason, runner),
|
|
65
68
|
status: displayStatus,
|
|
66
69
|
};
|
|
67
70
|
}
|
|
@@ -126,6 +129,14 @@ export function emptyStateForMissingExecution(job: Job): StepListEmptyState {
|
|
|
126
129
|
};
|
|
127
130
|
}
|
|
128
131
|
|
|
132
|
+
if (job.status === 'failed') {
|
|
133
|
+
return {
|
|
134
|
+
title: 'Job failed before an execution was created',
|
|
135
|
+
description: preStepFailureDescription(job.statusReason, job.runner),
|
|
136
|
+
status: 'failed',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
129
140
|
return {
|
|
130
141
|
title: 'Execution details unavailable',
|
|
131
142
|
description: 'This job finished, but no job execution record is available.',
|
|
@@ -155,17 +166,19 @@ export function skippedJobDescription(reason: Job['statusReason']): string {
|
|
|
155
166
|
}
|
|
156
167
|
}
|
|
157
168
|
|
|
158
|
-
function preStepFailureDescription(reason: string | null): string {
|
|
169
|
+
function preStepFailureDescription(reason: string | null, runner: string[] | null = null): string {
|
|
170
|
+
const runnerCopy = runner?.length ? ` Required runner labels: ${runner.join(', ')}.` : '';
|
|
171
|
+
|
|
159
172
|
switch (reason) {
|
|
160
173
|
case 'runner_lost':
|
|
161
|
-
return
|
|
174
|
+
return `The runner stopped responding before work began.${runnerCopy} Check runner availability before re-running the workflow.`;
|
|
162
175
|
case 'timed_out':
|
|
163
|
-
return
|
|
176
|
+
return `The job timed out before work began.${runnerCopy} Check runner capacity and timeout configuration before re-running the workflow.`;
|
|
164
177
|
case 'user_cancelled':
|
|
165
178
|
case 'run_cancelled':
|
|
166
179
|
return 'The execution ended while the run was being cancelled.';
|
|
167
180
|
case 'step_failed':
|
|
168
|
-
return
|
|
181
|
+
return `The execution failed before step details were recorded.${runnerCopy} Review run annotations before re-running the workflow.`;
|
|
169
182
|
case 'condition_errored':
|
|
170
183
|
return 'The job condition could not be evaluated. Review run annotations before re-running the workflow.';
|
|
171
184
|
case 'dependency_not_completed':
|
|
@@ -176,9 +189,9 @@ function preStepFailureDescription(reason: string | null): string {
|
|
|
176
189
|
return 'The job condition did not match, so no work started.';
|
|
177
190
|
case 'unknown':
|
|
178
191
|
case null:
|
|
179
|
-
return
|
|
192
|
+
return `Shipfox did not record a failure reason.${runnerCopy} Re-run only after checking the runner and workflow configuration.`;
|
|
180
193
|
default:
|
|
181
|
-
return `The execution ended because ${humanizeFailureReason(reason)}
|
|
194
|
+
return `The execution ended because ${humanizeFailureReason(reason)}.${runnerCopy} Resolve that condition before re-running the workflow.`;
|
|
182
195
|
}
|
|
183
196
|
}
|
|
184
197
|
|
|
@@ -189,7 +202,7 @@ function humanizeFailureReason(reason: string): string {
|
|
|
189
202
|
export function CarriedOverStepPanel() {
|
|
190
203
|
return (
|
|
191
204
|
<EmptyState
|
|
192
|
-
className="min-h-120 rounded-8 border border-border-neutral-base bg-background-components-base
|
|
205
|
+
className="min-h-120 rounded-8 border border-border-neutral-base bg-background-components-base p-panel"
|
|
193
206
|
icon="componentLine"
|
|
194
207
|
title="Carried over from a previous attempt"
|
|
195
208
|
description="Not executed in this run."
|
|
@@ -44,7 +44,7 @@ export function JobExecutionSwitcher({
|
|
|
44
44
|
return (
|
|
45
45
|
<div
|
|
46
46
|
className={cn(
|
|
47
|
-
'flex min-w-0 items-center gap-
|
|
47
|
+
'flex min-w-0 items-center gap-inline text-sm leading-20 text-foreground-neutral-subtle',
|
|
48
48
|
className,
|
|
49
49
|
)}
|
|
50
50
|
>
|
|
@@ -59,8 +59,8 @@ export function JobExecutionSwitcher({
|
|
|
59
59
|
className={cn(
|
|
60
60
|
'inline-flex min-w-0 max-w-full items-center rounded-6 text-left transition-colors focus-visible:shadow-border-interactive-with-active focus-visible:outline-none',
|
|
61
61
|
variant === 'title'
|
|
62
|
-
? '-mx-
|
|
63
|
-
: 'min-h-28 gap-
|
|
62
|
+
? '-mx-inline -my-[4px] gap-inline px-tight py-[4px] hover:bg-background-components-hover'
|
|
63
|
+
: 'min-h-28 gap-inline px-tight py-[4px] text-sm leading-20 text-foreground-neutral-subtle hover:bg-background-components-hover',
|
|
64
64
|
className,
|
|
65
65
|
)}
|
|
66
66
|
aria-label={`Switch job execution, currently execution ${selected.sequence}: ${selected.displayName}`}
|
|
@@ -141,7 +141,7 @@ function executionAccessibleLabel(execution: JobExecution): string {
|
|
|
141
141
|
|
|
142
142
|
function TitleExecutionSummary({execution}: {execution: JobExecution}) {
|
|
143
143
|
return (
|
|
144
|
-
<span className="flex min-w-0 items-center gap-
|
|
144
|
+
<span className="flex min-w-0 items-center gap-inline">
|
|
145
145
|
<Text as="span" size="sm" bold className="shrink-0 text-foreground-neutral-base">
|
|
146
146
|
#{execution.sequence}
|
|
147
147
|
</Text>
|
|
@@ -156,7 +156,7 @@ function ExecutionSummary({execution}: {execution: JobExecution}) {
|
|
|
156
156
|
const displayStatus = deriveJobExecutionDisplayStatus(execution);
|
|
157
157
|
|
|
158
158
|
return (
|
|
159
|
-
<span className="flex min-w-0 items-center gap-
|
|
159
|
+
<span className="flex min-w-0 items-center gap-inline">
|
|
160
160
|
<WorkflowStatusIcon status={displayStatus} size={14} tooltip={false} />
|
|
161
161
|
<span className="shrink-0 font-code text-xs leading-20 text-foreground-neutral-base tabular-nums">
|
|
162
162
|
Execution #{execution.sequence}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CodeBlock,
|
|
3
|
+
CodeBlockBody,
|
|
4
|
+
CodeBlockContent,
|
|
5
|
+
CodeBlockCopyButton,
|
|
6
|
+
CodeBlockFilename,
|
|
7
|
+
CodeBlockFiles,
|
|
8
|
+
CodeBlockHeader,
|
|
9
|
+
CodeBlockItem,
|
|
10
|
+
CodeTabs,
|
|
11
|
+
} from '@shipfox/react-ui/code-block';
|
|
12
|
+
import {Text} from '@shipfox/react-ui/typography';
|
|
13
|
+
import {useState} from 'react';
|
|
14
|
+
|
|
15
|
+
export interface JsonCodeEntry {
|
|
16
|
+
filename: string;
|
|
17
|
+
label?: string | undefined;
|
|
18
|
+
value: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function JsonCode({
|
|
22
|
+
title,
|
|
23
|
+
value,
|
|
24
|
+
emptyMessage,
|
|
25
|
+
}: {
|
|
26
|
+
title?: string | undefined;
|
|
27
|
+
value: unknown;
|
|
28
|
+
emptyMessage?: string | undefined;
|
|
29
|
+
}) {
|
|
30
|
+
const filename = title ?? 'value.json';
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="flex min-w-0 flex-col gap-tight">
|
|
34
|
+
{title ? (
|
|
35
|
+
<Text size="xs" className="text-foreground-neutral-muted">
|
|
36
|
+
{title}
|
|
37
|
+
</Text>
|
|
38
|
+
) : null}
|
|
39
|
+
{emptyMessage && isEmptyObject(value) ? (
|
|
40
|
+
<Text size="xs" className="text-foreground-neutral-muted">
|
|
41
|
+
{emptyMessage}
|
|
42
|
+
</Text>
|
|
43
|
+
) : (
|
|
44
|
+
<JsonCodeBlock entries={[{filename, value}]} />
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function JsonCodeTabs({entries}: {entries: readonly JsonCodeEntry[]}) {
|
|
51
|
+
const entryKeys = entries.map((entry) => entry.label ?? entry.filename);
|
|
52
|
+
const [activeKey, setActiveKey] = useState(entryKeys[0] ?? '');
|
|
53
|
+
const activeValue = entryKeys.includes(activeKey) ? activeKey : (entryKeys[0] ?? '');
|
|
54
|
+
const codes = Object.fromEntries(
|
|
55
|
+
entries.map((entry) => [entry.label ?? entry.filename, stringifyJson(entry.value)]),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<CodeTabs
|
|
60
|
+
value={activeValue}
|
|
61
|
+
onValueChange={setActiveKey}
|
|
62
|
+
codes={codes}
|
|
63
|
+
lang="json"
|
|
64
|
+
syntaxHighlighting={false}
|
|
65
|
+
className="h-auto min-h-0 rounded-8 bg-background-contrast-base shadow-button-neutral"
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function JsonCodeBlock({entries}: {entries: readonly JsonCodeEntry[]}) {
|
|
71
|
+
const data = entries.map((entry) => ({
|
|
72
|
+
filename: entry.filename,
|
|
73
|
+
language: 'json',
|
|
74
|
+
code: stringifyJson(entry.value),
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<CodeBlock
|
|
79
|
+
data={data}
|
|
80
|
+
className="h-auto min-h-0 rounded-6 bg-background-contrast-base shadow-button-neutral"
|
|
81
|
+
>
|
|
82
|
+
<CodeBlockHeader className="bg-background-contrast-base p-tight">
|
|
83
|
+
<CodeBlockFiles>
|
|
84
|
+
{(item) => {
|
|
85
|
+
const entry = entries.find((candidate) => candidate.filename === item.filename);
|
|
86
|
+
return (
|
|
87
|
+
<CodeBlockFilename value={item.filename}>
|
|
88
|
+
{entry?.label ?? item.filename}
|
|
89
|
+
</CodeBlockFilename>
|
|
90
|
+
);
|
|
91
|
+
}}
|
|
92
|
+
</CodeBlockFiles>
|
|
93
|
+
<CodeBlockCopyButton />
|
|
94
|
+
</CodeBlockHeader>
|
|
95
|
+
<CodeBlockBody>
|
|
96
|
+
{(item) => (
|
|
97
|
+
<CodeBlockItem value={item.filename}>
|
|
98
|
+
<CodeBlockContent language="json" syntaxHighlighting={false}>
|
|
99
|
+
{item.code}
|
|
100
|
+
</CodeBlockContent>
|
|
101
|
+
</CodeBlockItem>
|
|
102
|
+
)}
|
|
103
|
+
</CodeBlockBody>
|
|
104
|
+
</CodeBlock>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isEmptyObject(value: unknown): boolean {
|
|
109
|
+
return (
|
|
110
|
+
typeof value === 'object' &&
|
|
111
|
+
value !== null &&
|
|
112
|
+
!Array.isArray(value) &&
|
|
113
|
+
Object.keys(value).length === 0
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function stringifyJson(value: unknown): string {
|
|
118
|
+
const serialized = JSON.stringify(value, null, 2);
|
|
119
|
+
return serialized === undefined ? String(value) : serialized;
|
|
120
|
+
}
|
|
@@ -203,6 +203,7 @@ describe('StepAttemptLogPanel', () => {
|
|
|
203
203
|
renderPanel({attemptStatus: 'succeeded'});
|
|
204
204
|
|
|
205
205
|
expect(await screen.findByText('hello')).toBeInTheDocument();
|
|
206
|
+
expect(screen.getByRole('log')).toHaveAttribute('aria-live', 'off');
|
|
206
207
|
});
|
|
207
208
|
|
|
208
209
|
test('renders a terminal closed empty stream as no output', async () => {
|
|
@@ -100,10 +100,15 @@ export function StepAttemptLogPanel({
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
return (
|
|
103
|
-
<div className="flex min-w-0 flex-col gap-
|
|
103
|
+
<div className="flex min-w-0 flex-col gap-inline">
|
|
104
104
|
{staleError ? (
|
|
105
|
-
<Callout
|
|
106
|
-
|
|
105
|
+
<Callout
|
|
106
|
+
role="alert"
|
|
107
|
+
type="warning"
|
|
108
|
+
variant="secondary"
|
|
109
|
+
className="rounded-none border-b border-border-neutral-base px-0 py-row shadow-none"
|
|
110
|
+
>
|
|
111
|
+
<div className="flex min-w-0 flex-1 items-center justify-between gap-inline">
|
|
107
112
|
<Text size="xs">Could not refresh logs.</Text>
|
|
108
113
|
<Button
|
|
109
114
|
type="button"
|
|
@@ -121,6 +126,7 @@ export function StepAttemptLogPanel({
|
|
|
121
126
|
records={records}
|
|
122
127
|
emptyState={query.data?.complete ? 'complete' : 'pending'}
|
|
123
128
|
anchorToFailure={anchorToFailure}
|
|
129
|
+
ariaLive={attemptStatus === 'running' ? 'polite' : 'off'}
|
|
124
130
|
className={surfaceClassName}
|
|
125
131
|
/>
|
|
126
132
|
</div>
|
|
@@ -138,7 +144,7 @@ function StepLogsLoadingSurface({label, className}: {label: string; className: s
|
|
|
138
144
|
function StepLogsWaitingSurface({startedAt, className}: {startedAt: string; className: string}) {
|
|
139
145
|
return (
|
|
140
146
|
<div role="status" aria-label="Waiting for logs" className={className}>
|
|
141
|
-
<Text size="xs" className="px-
|
|
147
|
+
<Text size="xs" className="px-tight py-row text-foreground-neutral-muted">
|
|
142
148
|
Waiting for output ·{' '}
|
|
143
149
|
<span className="font-code tabular-nums" aria-hidden="true">
|
|
144
150
|
<JobExecutionTimeText time={{state: 'live', fromIso: startedAt}} />
|
|
@@ -174,8 +180,13 @@ function isTerminalAttemptStatus(status: string): boolean {
|
|
|
174
180
|
|
|
175
181
|
function StepLogsError({retrying, onRetry}: {retrying: boolean; onRetry: () => void}) {
|
|
176
182
|
return (
|
|
177
|
-
<Callout
|
|
178
|
-
|
|
183
|
+
<Callout
|
|
184
|
+
role="alert"
|
|
185
|
+
type="error"
|
|
186
|
+
variant="secondary"
|
|
187
|
+
className="rounded-none border-b border-border-neutral-base px-0 py-row shadow-none"
|
|
188
|
+
>
|
|
189
|
+
<div className="flex min-w-0 flex-1 items-center justify-between gap-inline">
|
|
179
190
|
<Text size="xs">Could not load logs.</Text>
|
|
180
191
|
<Button type="button" size="2xs" variant="secondary" isLoading={retrying} onClick={onRetry}>
|
|
181
192
|
Retry
|