@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
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
workflowRunAnnotationSummaryQueryOptions,
|
|
3
|
+
workflowRunAnnotationsQueryOptions,
|
|
4
|
+
} from './annotations.js';
|
|
5
|
+
|
|
6
|
+
const WORKFLOW_RUN_ID = '11111111-1111-4111-8111-111111111111';
|
|
7
|
+
|
|
8
|
+
describe('annotation query polling', () => {
|
|
9
|
+
it('polls enabled list and summary queries by default', () => {
|
|
10
|
+
const summary = workflowRunAnnotationSummaryQueryOptions(WORKFLOW_RUN_ID, 1);
|
|
11
|
+
const list = workflowRunAnnotationsQueryOptions(WORKFLOW_RUN_ID, 1);
|
|
12
|
+
|
|
13
|
+
expect(summary.refetchInterval).toBe(5_000);
|
|
14
|
+
expect(refetchIntervalForPages(list, 1)).toBe(5_000);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('polls when enabled explicitly for both query builders', () => {
|
|
18
|
+
const summary = workflowRunAnnotationSummaryQueryOptions(WORKFLOW_RUN_ID, 1, undefined, {
|
|
19
|
+
polling: true,
|
|
20
|
+
});
|
|
21
|
+
const list = workflowRunAnnotationsQueryOptions(WORKFLOW_RUN_ID, 1, undefined, {
|
|
22
|
+
polling: true,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
expect(summary.refetchInterval).toBe(5_000);
|
|
26
|
+
expect(refetchIntervalForPages(list, 1)).toBe(5_000);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('does not poll disabled query builders', () => {
|
|
30
|
+
const summary = workflowRunAnnotationSummaryQueryOptions(WORKFLOW_RUN_ID, 1, undefined, {
|
|
31
|
+
enabled: false,
|
|
32
|
+
});
|
|
33
|
+
const list = workflowRunAnnotationsQueryOptions(WORKFLOW_RUN_ID, 1, undefined, {
|
|
34
|
+
enabled: false,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(summary.refetchInterval).toBe(false);
|
|
38
|
+
expect(list.refetchInterval).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('stops summary polling when the owning run is terminal', () => {
|
|
42
|
+
const options = workflowRunAnnotationSummaryQueryOptions(WORKFLOW_RUN_ID, 1, undefined, {
|
|
43
|
+
polling: false,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(options.refetchInterval).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('stops annotation list polling when the owning run is terminal', () => {
|
|
50
|
+
const options = workflowRunAnnotationsQueryOptions(WORKFLOW_RUN_ID, 1, undefined, {
|
|
51
|
+
polling: false,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(options.refetchInterval).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('stops annotation list polling after the first page has been loaded', () => {
|
|
58
|
+
const options = workflowRunAnnotationsQueryOptions(WORKFLOW_RUN_ID, 1);
|
|
59
|
+
|
|
60
|
+
expect(refetchIntervalForPages(options, 2)).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('keeps execution-scoped summaries on a distinct reusable query key', () => {
|
|
64
|
+
const runSummary = workflowRunAnnotationSummaryQueryOptions(WORKFLOW_RUN_ID, 1);
|
|
65
|
+
const executionSummary = workflowRunAnnotationSummaryQueryOptions(
|
|
66
|
+
WORKFLOW_RUN_ID,
|
|
67
|
+
1,
|
|
68
|
+
'22222222-2222-4222-8222-222222222222',
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(runSummary.queryKey).not.toEqual(executionSummary.queryKey);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
function refetchIntervalForPages(
|
|
76
|
+
options: ReturnType<typeof workflowRunAnnotationsQueryOptions>,
|
|
77
|
+
pageCount: number,
|
|
78
|
+
) {
|
|
79
|
+
if (typeof options.refetchInterval !== 'function') return options.refetchInterval;
|
|
80
|
+
|
|
81
|
+
return options.refetchInterval({
|
|
82
|
+
state: {data: {pages: Array.from({length: pageCount}, () => ({}))}},
|
|
83
|
+
} as never);
|
|
84
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnnotationStyleDto,
|
|
3
|
+
annotationSummaryResponseSchema,
|
|
4
|
+
readAnnotationsResponseSchema,
|
|
5
|
+
} from '@shipfox/annotations-dto';
|
|
6
|
+
import {checkedApiRequest} from '@shipfox/client-api';
|
|
7
|
+
import {
|
|
8
|
+
infiniteQueryOptions,
|
|
9
|
+
queryOptions,
|
|
10
|
+
useInfiniteQuery,
|
|
11
|
+
useQuery,
|
|
12
|
+
} from '@tanstack/react-query';
|
|
13
|
+
import type {RunAnnotationSummary} from '#core/run-annotation.js';
|
|
14
|
+
|
|
15
|
+
const ANNOTATIONS_PAGE_SIZE = 100;
|
|
16
|
+
const ANNOTATIONS_REFRESH_INTERVAL_MS = 5_000;
|
|
17
|
+
|
|
18
|
+
export interface WorkflowRunAnnotation {
|
|
19
|
+
id: string;
|
|
20
|
+
jobId: string;
|
|
21
|
+
jobExecutionId: string;
|
|
22
|
+
originStepId: string;
|
|
23
|
+
originStepAttempt: number;
|
|
24
|
+
context: string;
|
|
25
|
+
style: AnnotationStyleDto;
|
|
26
|
+
sequence: number;
|
|
27
|
+
body: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface WorkflowRunAnnotationsPage {
|
|
31
|
+
annotations: WorkflowRunAnnotation[];
|
|
32
|
+
hasMore: boolean;
|
|
33
|
+
nextCursor: string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const workflowRunAnnotationsQueryKeys = {
|
|
37
|
+
all: ['workflow-run-annotations'] as const,
|
|
38
|
+
list: (workflowRunId: string, attempt: number, jobExecutionId?: string) =>
|
|
39
|
+
[
|
|
40
|
+
...workflowRunAnnotationsQueryKeys.all,
|
|
41
|
+
'list',
|
|
42
|
+
workflowRunId,
|
|
43
|
+
attempt,
|
|
44
|
+
jobExecutionId ?? null,
|
|
45
|
+
] as const,
|
|
46
|
+
summary: (workflowRunId: string, attempt: number, jobExecutionId?: string) =>
|
|
47
|
+
[
|
|
48
|
+
...workflowRunAnnotationsQueryKeys.all,
|
|
49
|
+
'summary',
|
|
50
|
+
workflowRunId,
|
|
51
|
+
attempt,
|
|
52
|
+
jobExecutionId ?? null,
|
|
53
|
+
] as const,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function annotationParams({
|
|
57
|
+
workflowRunId,
|
|
58
|
+
attempt,
|
|
59
|
+
jobExecutionId,
|
|
60
|
+
cursor,
|
|
61
|
+
}: {
|
|
62
|
+
workflowRunId: string;
|
|
63
|
+
attempt: number;
|
|
64
|
+
jobExecutionId?: string | undefined;
|
|
65
|
+
cursor?: string | null | undefined;
|
|
66
|
+
}): URLSearchParams {
|
|
67
|
+
const params = new URLSearchParams({
|
|
68
|
+
workflow_run_id: workflowRunId,
|
|
69
|
+
attempt: String(attempt),
|
|
70
|
+
limit: String(ANNOTATIONS_PAGE_SIZE),
|
|
71
|
+
});
|
|
72
|
+
if (jobExecutionId) params.set('job_execution_id', jobExecutionId);
|
|
73
|
+
if (cursor) params.set('cursor', cursor);
|
|
74
|
+
return params;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function getWorkflowRunAnnotationsPage({
|
|
78
|
+
workflowRunId,
|
|
79
|
+
attempt,
|
|
80
|
+
jobExecutionId,
|
|
81
|
+
cursor,
|
|
82
|
+
signal,
|
|
83
|
+
}: {
|
|
84
|
+
workflowRunId: string;
|
|
85
|
+
attempt: number;
|
|
86
|
+
jobExecutionId?: string | undefined;
|
|
87
|
+
cursor?: string | null | undefined;
|
|
88
|
+
signal?: AbortSignal;
|
|
89
|
+
}): Promise<WorkflowRunAnnotationsPage> {
|
|
90
|
+
const response = await checkedApiRequest(
|
|
91
|
+
readAnnotationsResponseSchema,
|
|
92
|
+
`/annotations?${annotationParams({workflowRunId, attempt, jobExecutionId, cursor}).toString()}`,
|
|
93
|
+
{signal},
|
|
94
|
+
);
|
|
95
|
+
return {
|
|
96
|
+
annotations: response.annotations.map(toWorkflowRunAnnotation),
|
|
97
|
+
hasMore: response.has_more,
|
|
98
|
+
nextCursor: response.next_cursor,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function getWorkflowRunAnnotationSummary({
|
|
103
|
+
workflowRunId,
|
|
104
|
+
attempt,
|
|
105
|
+
jobExecutionId,
|
|
106
|
+
signal,
|
|
107
|
+
}: {
|
|
108
|
+
workflowRunId: string;
|
|
109
|
+
attempt: number;
|
|
110
|
+
jobExecutionId?: string | undefined;
|
|
111
|
+
signal?: AbortSignal;
|
|
112
|
+
}): Promise<RunAnnotationSummary> {
|
|
113
|
+
const params = annotationParams({workflowRunId, attempt, jobExecutionId});
|
|
114
|
+
params.delete('limit');
|
|
115
|
+
const response = await checkedApiRequest(
|
|
116
|
+
annotationSummaryResponseSchema,
|
|
117
|
+
`/annotations/summary?${params.toString()}`,
|
|
118
|
+
{signal},
|
|
119
|
+
);
|
|
120
|
+
return {
|
|
121
|
+
total: response.total,
|
|
122
|
+
error: response.error,
|
|
123
|
+
warning: response.warning,
|
|
124
|
+
info: response.info,
|
|
125
|
+
success: response.success,
|
|
126
|
+
truncated: false,
|
|
127
|
+
stepCounts: response.step_counts.map((step) => ({
|
|
128
|
+
stepId: step.origin_step_id,
|
|
129
|
+
attempt: step.origin_step_attempt,
|
|
130
|
+
total: step.total,
|
|
131
|
+
})),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function workflowRunAnnotationsQueryOptions(
|
|
136
|
+
workflowRunId: string | undefined,
|
|
137
|
+
attempt: number | undefined,
|
|
138
|
+
jobExecutionId?: string | undefined,
|
|
139
|
+
options?: {enabled?: boolean | undefined; polling?: boolean | undefined},
|
|
140
|
+
) {
|
|
141
|
+
const enabled = Boolean(workflowRunId) && attempt !== undefined && (options?.enabled ?? true);
|
|
142
|
+
const polling = options?.polling ?? true;
|
|
143
|
+
return infiniteQueryOptions({
|
|
144
|
+
queryKey:
|
|
145
|
+
workflowRunId && attempt !== undefined
|
|
146
|
+
? workflowRunAnnotationsQueryKeys.list(workflowRunId, attempt, jobExecutionId)
|
|
147
|
+
: ([...workflowRunAnnotationsQueryKeys.all, 'list'] as const),
|
|
148
|
+
enabled,
|
|
149
|
+
initialPageParam: null as string | null,
|
|
150
|
+
queryFn: ({pageParam, signal}) =>
|
|
151
|
+
getWorkflowRunAnnotationsPage({
|
|
152
|
+
workflowRunId: workflowRunId ?? '',
|
|
153
|
+
attempt: attempt ?? 0,
|
|
154
|
+
...(jobExecutionId ? {jobExecutionId} : {}),
|
|
155
|
+
cursor: pageParam,
|
|
156
|
+
signal,
|
|
157
|
+
}),
|
|
158
|
+
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
|
159
|
+
staleTime: 2_000,
|
|
160
|
+
refetchInterval:
|
|
161
|
+
enabled && polling
|
|
162
|
+
? (query) => {
|
|
163
|
+
const pages = query.state.data?.pages;
|
|
164
|
+
return pages && pages.length > 1 ? false : ANNOTATIONS_REFRESH_INTERVAL_MS;
|
|
165
|
+
}
|
|
166
|
+
: false,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function useWorkflowRunAnnotationsQuery(
|
|
171
|
+
workflowRunId: string | undefined,
|
|
172
|
+
attempt: number | undefined,
|
|
173
|
+
jobExecutionId?: string | undefined,
|
|
174
|
+
options?: {enabled?: boolean | undefined; polling?: boolean | undefined},
|
|
175
|
+
) {
|
|
176
|
+
return useInfiniteQuery(
|
|
177
|
+
workflowRunAnnotationsQueryOptions(workflowRunId, attempt, jobExecutionId, options),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function workflowRunAnnotationSummaryQueryOptions(
|
|
182
|
+
workflowRunId: string | undefined,
|
|
183
|
+
attempt: number | undefined,
|
|
184
|
+
jobExecutionId?: string | undefined,
|
|
185
|
+
options?: {enabled?: boolean | undefined; polling?: boolean | undefined},
|
|
186
|
+
) {
|
|
187
|
+
const enabled = Boolean(workflowRunId) && attempt !== undefined && (options?.enabled ?? true);
|
|
188
|
+
const polling = options?.polling ?? true;
|
|
189
|
+
return queryOptions({
|
|
190
|
+
queryKey:
|
|
191
|
+
workflowRunId && attempt !== undefined
|
|
192
|
+
? workflowRunAnnotationsQueryKeys.summary(workflowRunId, attempt, jobExecutionId)
|
|
193
|
+
: ([...workflowRunAnnotationsQueryKeys.all, 'summary'] as const),
|
|
194
|
+
enabled,
|
|
195
|
+
queryFn: ({signal}) =>
|
|
196
|
+
getWorkflowRunAnnotationSummary({
|
|
197
|
+
workflowRunId: workflowRunId ?? '',
|
|
198
|
+
attempt: attempt ?? 0,
|
|
199
|
+
...(jobExecutionId ? {jobExecutionId} : {}),
|
|
200
|
+
signal,
|
|
201
|
+
}),
|
|
202
|
+
staleTime: 2_000,
|
|
203
|
+
refetchInterval: enabled && polling ? ANNOTATIONS_REFRESH_INTERVAL_MS : false,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function useWorkflowRunAnnotationSummaryQuery(
|
|
208
|
+
workflowRunId: string | undefined,
|
|
209
|
+
attempt: number | undefined,
|
|
210
|
+
jobExecutionId?: string | undefined,
|
|
211
|
+
options?: {enabled?: boolean | undefined; polling?: boolean | undefined},
|
|
212
|
+
) {
|
|
213
|
+
return useQuery(
|
|
214
|
+
workflowRunAnnotationSummaryQueryOptions(workflowRunId, attempt, jobExecutionId, options),
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function flattenWorkflowRunAnnotations(
|
|
219
|
+
data: {pages: readonly WorkflowRunAnnotationsPage[]} | undefined,
|
|
220
|
+
): WorkflowRunAnnotation[] {
|
|
221
|
+
return data?.pages.flatMap((page) => page.annotations) ?? [];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function toWorkflowRunAnnotation(
|
|
225
|
+
annotation: Awaited<
|
|
226
|
+
ReturnType<typeof readAnnotationsResponseSchema.parse>
|
|
227
|
+
>['annotations'][number],
|
|
228
|
+
): WorkflowRunAnnotation {
|
|
229
|
+
return {
|
|
230
|
+
id: annotation.id,
|
|
231
|
+
jobId: annotation.job_id,
|
|
232
|
+
jobExecutionId: annotation.job_execution_id,
|
|
233
|
+
originStepId: annotation.origin_step_id,
|
|
234
|
+
originStepAttempt: annotation.origin_step_attempt,
|
|
235
|
+
context: annotation.context,
|
|
236
|
+
style: annotation.style,
|
|
237
|
+
sequence: annotation.sequence,
|
|
238
|
+
body: annotation.body,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {stepAttemptDetailResponseSchema} from '@shipfox/api-workflows-dto';
|
|
2
|
+
import {checkedApiRequest} from '@shipfox/client-api';
|
|
3
|
+
import {queryOptions, type UseQueryOptions, useQuery} from '@tanstack/react-query';
|
|
4
|
+
import type {StepAttemptDetail} from '#core/workflow-run.js';
|
|
5
|
+
import {toStepAttemptDetail} from './workflow-run-mapper.js';
|
|
6
|
+
|
|
7
|
+
export const stepAttemptDetailQueryKeys = {
|
|
8
|
+
all: ['workflow-step-attempt-details'] as const,
|
|
9
|
+
detail: (stepId: string, attempt: number) =>
|
|
10
|
+
[...stepAttemptDetailQueryKeys.all, stepId, attempt] as const,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type StepAttemptDetailQueryKey =
|
|
14
|
+
| ReturnType<typeof stepAttemptDetailQueryKeys.detail>
|
|
15
|
+
| readonly ['workflow-step-attempt-details'];
|
|
16
|
+
|
|
17
|
+
type StepAttemptDetailQueryOptions = UseQueryOptions<
|
|
18
|
+
StepAttemptDetail,
|
|
19
|
+
Error,
|
|
20
|
+
StepAttemptDetail,
|
|
21
|
+
StepAttemptDetailQueryKey
|
|
22
|
+
>;
|
|
23
|
+
|
|
24
|
+
async function getStepAttemptDetail({
|
|
25
|
+
stepId,
|
|
26
|
+
attempt,
|
|
27
|
+
signal,
|
|
28
|
+
}: {
|
|
29
|
+
stepId: string;
|
|
30
|
+
attempt: number;
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
}): Promise<StepAttemptDetail> {
|
|
33
|
+
const response = await checkedApiRequest(
|
|
34
|
+
stepAttemptDetailResponseSchema,
|
|
35
|
+
`/workflows/runs/steps/${stepId}/attempts/${attempt}`,
|
|
36
|
+
{signal},
|
|
37
|
+
);
|
|
38
|
+
return toStepAttemptDetail(response);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function stepAttemptDetailQueryOptions(
|
|
42
|
+
stepId: string | undefined,
|
|
43
|
+
attempt: number | undefined,
|
|
44
|
+
): StepAttemptDetailQueryOptions {
|
|
45
|
+
return queryOptions({
|
|
46
|
+
queryKey:
|
|
47
|
+
stepId && attempt !== undefined
|
|
48
|
+
? stepAttemptDetailQueryKeys.detail(stepId, attempt)
|
|
49
|
+
: ([...stepAttemptDetailQueryKeys.all] as const),
|
|
50
|
+
enabled: Boolean(stepId) && attempt !== undefined,
|
|
51
|
+
queryFn: ({signal}) =>
|
|
52
|
+
getStepAttemptDetail({stepId: stepId ?? '', attempt: attempt ?? 0, signal}),
|
|
53
|
+
staleTime: Infinity,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function useStepAttemptDetailQuery(
|
|
58
|
+
stepId: string | undefined,
|
|
59
|
+
attempt: number | undefined,
|
|
60
|
+
options?: {enabled?: boolean | undefined},
|
|
61
|
+
) {
|
|
62
|
+
return useQuery({
|
|
63
|
+
...stepAttemptDetailQueryOptions(stepId, attempt),
|
|
64
|
+
...options,
|
|
65
|
+
enabled: Boolean(stepId) && attempt !== undefined && (options?.enabled ?? true),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
EvaluationTraceDto,
|
|
2
3
|
JobListeningDto,
|
|
4
|
+
StepAttemptDetailResponseDto,
|
|
3
5
|
StepAttemptDto,
|
|
4
6
|
StepGateResultDto,
|
|
5
7
|
WorkflowExecutionEventDto,
|
|
@@ -13,6 +15,7 @@ import type {
|
|
|
13
15
|
WorkflowRunStepDetailDto,
|
|
14
16
|
} from '@shipfox/api-workflows-dto';
|
|
15
17
|
import {
|
|
18
|
+
type EvaluationTraceEntry,
|
|
16
19
|
Job,
|
|
17
20
|
JobExecution,
|
|
18
21
|
type JobListening,
|
|
@@ -139,6 +142,10 @@ export function toJob(dto: WorkflowRunJobDetailDto): Job {
|
|
|
139
142
|
status: dto.status,
|
|
140
143
|
statusReason: dto.status_reason,
|
|
141
144
|
carriedOver: dto.carried_over,
|
|
145
|
+
outputs: dto.outputs ?? null,
|
|
146
|
+
success: dto.success ?? null,
|
|
147
|
+
runner: dto.runner ?? null,
|
|
148
|
+
evaluationTrace: toEvaluationTrace(dto.evaluation_trace),
|
|
142
149
|
listening: dto.listening ? toJobListening(dto.listening) : null,
|
|
143
150
|
listenerStatus: dto.listener_status,
|
|
144
151
|
resolutionReason: dto.resolution_reason,
|
|
@@ -158,11 +165,14 @@ export function toJobExecution(dto: WorkflowRunJobExecutionDetailDto): JobExecut
|
|
|
158
165
|
name: dto.name,
|
|
159
166
|
status: dto.status,
|
|
160
167
|
statusReason: dto.status_reason,
|
|
168
|
+
runner: dto.runner ?? null,
|
|
169
|
+
outputs: dto.outputs ?? null,
|
|
161
170
|
triggerEvents: (dto.trigger_events ?? []).map(toWorkflowExecutionEvent),
|
|
162
171
|
queuedAt: dto.queued_at ?? null,
|
|
163
172
|
startedAt: dto.started_at ?? null,
|
|
164
173
|
finishedAt: dto.finished_at ?? null,
|
|
165
174
|
timedOutAt: dto.timed_out_at ?? null,
|
|
175
|
+
evaluationTrace: toEvaluationTrace(dto.evaluation_trace),
|
|
166
176
|
createdAt: dto.created_at,
|
|
167
177
|
updatedAt: dto.updated_at,
|
|
168
178
|
steps: dto.steps.map(toStep),
|
|
@@ -179,8 +189,10 @@ export function toStep(dto: WorkflowRunStepDetailDto): Step {
|
|
|
179
189
|
? {startLine: dto.source_location.start_line, endLine: dto.source_location.end_line}
|
|
180
190
|
: null,
|
|
181
191
|
status: dto.status,
|
|
192
|
+
statusReason: dto.status_reason,
|
|
182
193
|
type: dto.type,
|
|
183
194
|
config: dto.config,
|
|
195
|
+
evaluationTrace: toEvaluationTrace(dto.evaluation_trace),
|
|
184
196
|
agentConfig: toAgentStepConfig(dto),
|
|
185
197
|
error: dto.error
|
|
186
198
|
? {
|
|
@@ -210,6 +222,8 @@ export function toStepAttempt(dto: StepAttemptDto, jobExecutionId: string): Step
|
|
|
210
222
|
status: dto.status,
|
|
211
223
|
exitCode: dto.exit_code ?? null,
|
|
212
224
|
output: dto.output ?? null,
|
|
225
|
+
outputs: dto.outputs ?? dto.output ?? null,
|
|
226
|
+
response: dto.response ?? null,
|
|
213
227
|
error: dto.error ?? null,
|
|
214
228
|
gateResult: toStepGateResult(dto.gate_result),
|
|
215
229
|
restartFeedback: dto.restart_feedback ?? null,
|
|
@@ -218,6 +232,16 @@ export function toStepAttempt(dto: StepAttemptDto, jobExecutionId: string): Step
|
|
|
218
232
|
});
|
|
219
233
|
}
|
|
220
234
|
|
|
235
|
+
export function toStepAttemptDetail(dto: StepAttemptDetailResponseDto) {
|
|
236
|
+
return {
|
|
237
|
+
stepId: dto.step_id,
|
|
238
|
+
attempt: dto.attempt,
|
|
239
|
+
authoredConfig: dto.authored_config,
|
|
240
|
+
config: dto.config,
|
|
241
|
+
evaluationTrace: toEvaluationTrace(dto.evaluation_trace),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
221
245
|
function toJobListening(dto: JobListeningDto): JobListening {
|
|
222
246
|
return {
|
|
223
247
|
on: dto.on,
|
|
@@ -259,6 +283,28 @@ function toStepGateResult(dto: StepGateResultDto): StepGateResult {
|
|
|
259
283
|
return dto;
|
|
260
284
|
}
|
|
261
285
|
|
|
286
|
+
function toEvaluationTrace(trace: EvaluationTraceDto | null): EvaluationTraceEntry[] | null {
|
|
287
|
+
return (
|
|
288
|
+
trace?.map((entry) =>
|
|
289
|
+
'dropped' in entry
|
|
290
|
+
? entry
|
|
291
|
+
: {
|
|
292
|
+
expression: entry.expression,
|
|
293
|
+
roots: entry.roots,
|
|
294
|
+
fillTarget: entry.fill_target,
|
|
295
|
+
evaluatedAt: entry.evaluated_at,
|
|
296
|
+
field: entry.field,
|
|
297
|
+
...(entry.value === undefined ? {} : {value: entry.value}),
|
|
298
|
+
...(entry.truncated === undefined ? {} : {truncated: entry.truncated}),
|
|
299
|
+
...(entry.expr_truncated === undefined ? {} : {exprTruncated: entry.expr_truncated}),
|
|
300
|
+
...(entry.reference === undefined ? {} : {reference: entry.reference}),
|
|
301
|
+
...(entry.degraded === undefined ? {} : {degraded: entry.degraded}),
|
|
302
|
+
...(entry.env_key === undefined ? {} : {envKey: entry.env_key}),
|
|
303
|
+
},
|
|
304
|
+
) ?? null
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
262
308
|
function toAgentStepConfig(dto: WorkflowRunStepDetailDto): Step['agentConfig'] {
|
|
263
309
|
if (dto.type !== 'agent') return null;
|
|
264
310
|
return {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type {StepAttemptDetailResponseDto} from '@shipfox/api-workflows-dto';
|
|
1
2
|
import {configureApiClient} from '@shipfox/client-api';
|
|
2
3
|
import {type InfiniteData, QueryClient, QueryClientProvider} from '@tanstack/react-query';
|
|
3
4
|
import {act, cleanup, renderHook, waitFor} from '@testing-library/react';
|
|
@@ -11,7 +12,8 @@ import {
|
|
|
11
12
|
workflowRunDto,
|
|
12
13
|
workflowRunListResponseDto,
|
|
13
14
|
} from '#test/fixtures/workflow-run.js';
|
|
14
|
-
import {
|
|
15
|
+
import {useStepAttemptDetailQuery} from './step-attempt-detail.js';
|
|
16
|
+
import {toStepAttemptDetail, toWorkflowRun, toWorkflowRunListPage} from './workflow-run-mapper.js';
|
|
15
17
|
import {
|
|
16
18
|
fireManualWorkflow,
|
|
17
19
|
useCancelWorkflowRunMutation,
|
|
@@ -133,6 +135,64 @@ describe('workflow run API hooks', () => {
|
|
|
133
135
|
expect(cached).toHaveProperty('triggerSource', 'manual');
|
|
134
136
|
});
|
|
135
137
|
|
|
138
|
+
test('maps lazy step attempt details to authored and resolved troubleshooting data', () => {
|
|
139
|
+
const dto: StepAttemptDetailResponseDto = {
|
|
140
|
+
step_id: '88888888-8888-4888-8888-888888888888',
|
|
141
|
+
attempt: 2,
|
|
142
|
+
authored_config: {run: 'echo $' + '{{ inputs.message }}'},
|
|
143
|
+
config: {run: 'echo hello'},
|
|
144
|
+
evaluation_trace: [
|
|
145
|
+
{
|
|
146
|
+
expression: 'inputs.message',
|
|
147
|
+
roots: ['inputs.message'],
|
|
148
|
+
fill_target: 'run',
|
|
149
|
+
evaluated_at: '2026-08-05T12:00:00.000Z',
|
|
150
|
+
field: 'run',
|
|
151
|
+
value: 'hello',
|
|
152
|
+
degraded: false,
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
expect(toStepAttemptDetail(dto)).toEqual({
|
|
158
|
+
stepId: dto.step_id,
|
|
159
|
+
attempt: 2,
|
|
160
|
+
authoredConfig: dto.authored_config,
|
|
161
|
+
config: dto.config,
|
|
162
|
+
evaluationTrace: [
|
|
163
|
+
{
|
|
164
|
+
expression: 'inputs.message',
|
|
165
|
+
roots: ['inputs.message'],
|
|
166
|
+
fillTarget: 'run',
|
|
167
|
+
evaluatedAt: '2026-08-05T12:00:00.000Z',
|
|
168
|
+
field: 'run',
|
|
169
|
+
value: 'hello',
|
|
170
|
+
degraded: false,
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('does not fetch step attempt details while the inspector is closed', async () => {
|
|
177
|
+
const fetchImpl = vi.fn(async () =>
|
|
178
|
+
jsonResponse({
|
|
179
|
+
step_id: '88888888-8888-4888-8888-888888888888',
|
|
180
|
+
attempt: 1,
|
|
181
|
+
authored_config: null,
|
|
182
|
+
config: {},
|
|
183
|
+
evaluation_trace: null,
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
|
|
187
|
+
|
|
188
|
+
renderWithQueryClient(() =>
|
|
189
|
+
useStepAttemptDetailQuery('88888888-8888-4888-8888-888888888888', 1, {enabled: false}),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
await Promise.resolve();
|
|
193
|
+
expect(fetchImpl).not.toHaveBeenCalled();
|
|
194
|
+
});
|
|
195
|
+
|
|
136
196
|
test('rejects malformed detail responses before they reach the query cache', async () => {
|
|
137
197
|
const fetchImpl = vi.fn(async () => jsonResponse({id: RUN_ID, trigger_source: 'manual'}));
|
|
138
198
|
configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
|