@shipfox/client-workflows 5.0.0 → 6.0.2
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/CHANGELOG.md +68 -0
- package/dist/components/workflow-run-list/types.d.ts +12 -0
- 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-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 +32 -11
- 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 +7 -2
- package/dist/components/workflow-run-list/workflow-run-list.js.map +1 -1
- package/dist/components/workflow-run-summary/workflow-run-summary.d.ts +2 -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.map +1 -1
- package/dist/components/workflow-run-view/job-card.d.ts.map +1 -1
- package/dist/components/workflow-run-view/job-card.js +9 -8
- package/dist/components/workflow-run-view/job-card.js.map +1 -1
- package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
- package/dist/core/entities/job-execution.d.ts +11 -6
- package/dist/core/entities/job-execution.d.ts.map +1 -1
- package/dist/core/entities/job-execution.js +0 -19
- package/dist/core/entities/job-execution.js.map +1 -1
- package/dist/core/entities/job.d.ts +32 -10
- package/dist/core/entities/job.d.ts.map +1 -1
- package/dist/core/entities/job.js +0 -22
- package/dist/core/entities/job.js.map +1 -1
- package/dist/core/entities/step-attempt.d.ts +28 -3
- package/dist/core/entities/step-attempt.d.ts.map +1 -1
- package/dist/core/entities/step-attempt.js +0 -17
- package/dist/core/entities/step-attempt.js.map +1 -1
- package/dist/core/entities/step.d.ts +6 -6
- package/dist/core/entities/step.d.ts.map +1 -1
- package/dist/core/entities/step.js +19 -49
- package/dist/core/entities/step.js.map +1 -1
- package/dist/core/entities/workflow-run-attempt.d.ts +0 -2
- package/dist/core/entities/workflow-run-attempt.d.ts.map +1 -1
- package/dist/core/entities/workflow-run-attempt.js +0 -12
- package/dist/core/entities/workflow-run-attempt.js.map +1 -1
- package/dist/core/entities/workflow-run.d.ts +6 -7
- package/dist/core/entities/workflow-run.d.ts.map +1 -1
- package/dist/core/entities/workflow-run.js +1 -67
- package/dist/core/entities/workflow-run.js.map +1 -1
- package/dist/core/workflow-run.d.ts +9 -9
- package/dist/core/workflow-run.d.ts.map +1 -1
- package/dist/core/workflow-run.js +6 -6
- package/dist/core/workflow-run.js.map +1 -1
- package/dist/feature.d.ts +13 -0
- package/dist/feature.d.ts.map +1 -1
- package/dist/feature.js +17 -16
- package/dist/feature.js.map +1 -1
- package/dist/hooks/api/workflow-run-mapper.d.ts +12 -0
- package/dist/hooks/api/workflow-run-mapper.d.ts.map +1 -0
- package/dist/hooks/api/workflow-run-mapper.js +207 -0
- package/dist/hooks/api/workflow-run-mapper.js.map +1 -0
- package/dist/hooks/api/workflow-runs.d.ts +25 -55
- package/dist/hooks/api/workflow-runs.d.ts.map +1 -1
- package/dist/hooks/api/workflow-runs.js +95 -51
- package/dist/hooks/api/workflow-runs.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pages/project-workflows-page.d.ts.map +1 -1
- package/dist/pages/project-workflows-page.js +14 -14
- package/dist/pages/project-workflows-page.js.map +1 -1
- package/dist/pages/workflow-run-page.d.ts +3 -1
- package/dist/pages/workflow-run-page.d.ts.map +1 -1
- package/dist/pages/workflow-run-page.js +21 -14
- package/dist/pages/workflow-run-page.js.map +1 -1
- package/dist/routes/inputs.d.ts +22 -0
- package/dist/routes/inputs.d.ts.map +1 -0
- package/dist/routes/inputs.js +76 -0
- package/dist/routes/inputs.js.map +1 -0
- package/dist/routes/run-detail.d.ts +2 -0
- package/dist/routes/run-detail.d.ts.map +1 -1
- package/dist/routes/run-detail.js +6 -6
- package/dist/routes/run-detail.js.map +1 -1
- package/dist/routes/runs.d.ts +2 -0
- package/dist/routes/runs.d.ts.map +1 -1
- package/dist/routes/runs.js +6 -6
- package/dist/routes/runs.js.map +1 -1
- package/dist/routes/workflows.js +3 -5
- package/dist/routes/workflows.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +13 -14
- package/src/components/workflow-run-list/types.ts +6 -0
- package/src/components/workflow-run-list/workflow-run-list-view.tsx +27 -11
- package/src/components/workflow-run-list/workflow-run-list.tsx +6 -0
- package/src/components/workflow-run-summary/workflow-run-attempt-switcher.test.tsx +5 -6
- package/src/components/workflow-run-summary/workflow-run-summary.tsx +3 -3
- package/src/components/workflow-run-view/job-card.tsx +20 -11
- package/src/components/workflow-run-view/workflow-run-view.test.tsx +3 -3
- package/src/components/workflow-run-view/workflow-run-view.tsx +2 -2
- package/src/components/workflow-source-panel/workflow-source-panel.stories.tsx +4 -0
- package/src/core/entities/job-execution.ts +12 -28
- package/src/core/entities/job.ts +44 -45
- package/src/core/entities/step-attempt.ts +9 -20
- package/src/core/entities/step.ts +38 -66
- package/src/core/entities/workflow-run-attempt.ts +0 -14
- package/src/core/entities/workflow-run.ts +8 -86
- package/src/core/workflow-run.test.ts +13 -11
- package/src/core/workflow-run.ts +8 -13
- package/src/feature.ts +19 -17
- package/src/hooks/api/workflow-run-mapper.ts +246 -0
- package/src/hooks/api/workflow-runs.test.tsx +57 -59
- package/src/hooks/api/workflow-runs.ts +187 -88
- package/src/index.ts +4 -0
- package/src/pages/project-workflows-page.test.tsx +1 -1
- package/src/pages/project-workflows-page.tsx +31 -26
- package/src/pages/workflow-run-page.test.tsx +4 -2
- package/src/pages/workflow-run-page.tsx +22 -17
- package/src/routes/inputs.test.ts +22 -0
- package/src/routes/inputs.ts +58 -0
- package/src/routes/run-detail.tsx +12 -8
- package/src/routes/runs.tsx +11 -4
- package/src/routes/workflows.tsx +3 -3
- package/test/fixtures/workflow-run.ts +9 -7
- package/test/pages.tsx +20 -5
- package/tsconfig.build.tsbuildinfo +1 -1
- package/vercel.json +5 -0
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
WorkflowRunDetailResponseDto,
|
|
3
|
-
WorkflowRunListResponseDto,
|
|
4
|
-
} from '@shipfox/api-workflows-dto';
|
|
5
1
|
import {configureApiClient} from '@shipfox/client-api';
|
|
6
2
|
import {type InfiniteData, QueryClient, QueryClientProvider} from '@tanstack/react-query';
|
|
7
3
|
import {act, cleanup, renderHook, waitFor} from '@testing-library/react';
|
|
8
4
|
import type {ReactNode} from 'react';
|
|
9
|
-
import {
|
|
5
|
+
import type {WorkflowRunListPage} from '#core/workflow-run.js';
|
|
10
6
|
import {
|
|
11
7
|
runAttemptsResponseDto,
|
|
12
8
|
workflowJobDto,
|
|
@@ -15,6 +11,7 @@ import {
|
|
|
15
11
|
workflowRunDto,
|
|
16
12
|
workflowRunListResponseDto,
|
|
17
13
|
} from '#test/fixtures/workflow-run.js';
|
|
14
|
+
import {toWorkflowRun, toWorkflowRunListPage} from './workflow-run-mapper.js';
|
|
18
15
|
import {
|
|
19
16
|
fireManualWorkflow,
|
|
20
17
|
useCancelWorkflowRunMutation,
|
|
@@ -55,7 +52,7 @@ describe('workflow run API hooks', () => {
|
|
|
55
52
|
configureApiClient({baseUrl: '', fetchImpl: undefined});
|
|
56
53
|
});
|
|
57
54
|
|
|
58
|
-
test('maps list DTO pages to workflow run models
|
|
55
|
+
test('maps list DTO pages to workflow run models before caching', async () => {
|
|
59
56
|
const body = workflowRunListResponseDto({
|
|
60
57
|
runs: [
|
|
61
58
|
workflowRunDto({
|
|
@@ -92,19 +89,19 @@ describe('workflow run API hooks', () => {
|
|
|
92
89
|
expect(result.current.data?.pages[0]?.nextCursor).toBe('cursor-2');
|
|
93
90
|
expect(result.current.data?.pages[0]?.filteredTotalCount).toBe(8);
|
|
94
91
|
|
|
95
|
-
const cached = queryClient.getQueryData<
|
|
96
|
-
|
|
97
|
-
|
|
92
|
+
const cached = queryClient.getQueryData<InfiniteData<WorkflowRunListPage, string | undefined>>(
|
|
93
|
+
workflowRunsQueryKeys.list(PROJECT_ID, {}),
|
|
94
|
+
);
|
|
98
95
|
expect(cached?.pages[0]?.runs[0]).toMatchObject({
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
96
|
+
triggerProvider: 'github',
|
|
97
|
+
triggerSource: 'github_acme',
|
|
98
|
+
triggerEvent: 'push',
|
|
99
|
+
updatedAt: '2026-05-07T01:02:00.000Z',
|
|
103
100
|
});
|
|
104
|
-
expect(cached?.pages[0]).
|
|
101
|
+
expect(cached?.pages[0]).toHaveProperty('nextCursor', 'cursor-2');
|
|
105
102
|
});
|
|
106
103
|
|
|
107
|
-
test('maps detail DTOs to nested workflow run detail models
|
|
104
|
+
test('maps detail DTOs to nested workflow run detail models before caching', async () => {
|
|
108
105
|
const body = workflowRunDetailDto({
|
|
109
106
|
id: RUN_ID,
|
|
110
107
|
trigger_source: 'manual',
|
|
@@ -126,16 +123,25 @@ describe('workflow run API hooks', () => {
|
|
|
126
123
|
jobs: [{name: 'build', runAttemptId: RUN_ID}],
|
|
127
124
|
});
|
|
128
125
|
|
|
129
|
-
const cached = queryClient.getQueryData
|
|
130
|
-
workflowRunsQueryKeys.detail(RUN_ID),
|
|
131
|
-
);
|
|
126
|
+
const cached = queryClient.getQueryData(workflowRunsQueryKeys.detail(RUN_ID));
|
|
132
127
|
expect(cached).toMatchObject({
|
|
133
128
|
id: RUN_ID,
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
jobs: [{name: 'build',
|
|
129
|
+
triggerSource: 'manual',
|
|
130
|
+
triggerEvent: 'fire',
|
|
131
|
+
jobs: [{name: 'build', runAttemptId: RUN_ID}],
|
|
137
132
|
});
|
|
138
|
-
expect(cached).
|
|
133
|
+
expect(cached).toHaveProperty('triggerSource', 'manual');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('rejects malformed detail responses before they reach the query cache', async () => {
|
|
137
|
+
const fetchImpl = vi.fn(async () => jsonResponse({id: RUN_ID, trigger_source: 'manual'}));
|
|
138
|
+
configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
|
|
139
|
+
|
|
140
|
+
const {result, queryClient} = renderWithQueryClient(() => useWorkflowRunQuery(RUN_ID));
|
|
141
|
+
|
|
142
|
+
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
143
|
+
expect(result.current.error).toMatchObject({code: 'invalid-response'});
|
|
144
|
+
expect(queryClient.getQueryData(workflowRunsQueryKeys.detail(RUN_ID))).toBeUndefined();
|
|
139
145
|
});
|
|
140
146
|
|
|
141
147
|
test('maps run attempts and caches them by workflow run id', async () => {
|
|
@@ -173,7 +179,9 @@ describe('workflow run API hooks', () => {
|
|
|
173
179
|
expect(firstRequest(fetchImpl).url).toBe(
|
|
174
180
|
`https://api.example.test/workflows/runs/${RUN_ID}/attempts`,
|
|
175
181
|
);
|
|
176
|
-
expect(queryClient.getQueryData(workflowRunsQueryKeys.attempts(RUN_ID))).toEqual(
|
|
182
|
+
expect(queryClient.getQueryData(workflowRunsQueryKeys.attempts(RUN_ID))).toEqual(
|
|
183
|
+
result.current.data,
|
|
184
|
+
);
|
|
177
185
|
});
|
|
178
186
|
|
|
179
187
|
test('posts manual fire requests with and without inputs', async () => {
|
|
@@ -190,8 +198,8 @@ describe('workflow run API hooks', () => {
|
|
|
190
198
|
inputs: {env: 'production'},
|
|
191
199
|
});
|
|
192
200
|
|
|
193
|
-
expect(withoutInputs.
|
|
194
|
-
expect(withInputs.
|
|
201
|
+
expect(withoutInputs.workflowRunId).toBe(RUN_ID);
|
|
202
|
+
expect(withInputs.workflowRunId).toBe(RUN_ID);
|
|
195
203
|
expect(postBodies).toEqual([{}, {inputs: {env: 'production'}}]);
|
|
196
204
|
expect(firstRequest(fetchImpl).url).toBe(
|
|
197
205
|
`https://api.example.test/workflow-definitions/${DEFINITION_ID}/fire-manual`,
|
|
@@ -210,13 +218,12 @@ describe('workflow run API hooks', () => {
|
|
|
210
218
|
configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
|
|
211
219
|
const {result, queryClient} = renderWithQueryClient(() => useFireManualWorkflowMutation());
|
|
212
220
|
const listKey = workflowRunsQueryKeys.list(PROJECT_ID, {});
|
|
213
|
-
queryClient.setQueryData<InfiniteData<
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
);
|
|
221
|
+
queryClient.setQueryData<InfiniteData<WorkflowRunListPage, string | undefined>>(listKey, {
|
|
222
|
+
pages: [
|
|
223
|
+
toWorkflowRunListPage(workflowRunListResponseDto({runs: [], filtered_total_count: 0})),
|
|
224
|
+
],
|
|
225
|
+
pageParams: [undefined],
|
|
226
|
+
});
|
|
220
227
|
|
|
221
228
|
const railListEntries = queryClient.getQueriesData({
|
|
222
229
|
queryKey: workflowRunsQueryKeys.lists(PROJECT_ID),
|
|
@@ -229,18 +236,16 @@ describe('workflow run API hooks', () => {
|
|
|
229
236
|
|
|
230
237
|
await waitFor(() => {
|
|
231
238
|
const cached =
|
|
232
|
-
queryClient.getQueryData<InfiniteData<
|
|
233
|
-
listKey,
|
|
234
|
-
);
|
|
239
|
+
queryClient.getQueryData<InfiniteData<WorkflowRunListPage, string | undefined>>(listKey);
|
|
235
240
|
expect(cached?.pages[0]?.runs[0]).toMatchObject({
|
|
236
|
-
|
|
237
|
-
|
|
241
|
+
projectId: PROJECT_ID,
|
|
242
|
+
definitionId: DEFINITION_ID,
|
|
238
243
|
status: 'pending',
|
|
239
|
-
|
|
240
|
-
|
|
244
|
+
triggerSource: 'manual',
|
|
245
|
+
triggerProvider: null,
|
|
241
246
|
});
|
|
242
247
|
expect(cached?.pages[0]?.runs[0]?.id).toMatch(TEMP_RUN_ID_PATTERN);
|
|
243
|
-
expect(cached?.pages[0]?.
|
|
248
|
+
expect(cached?.pages[0]?.filteredTotalCount).toBe(1);
|
|
244
249
|
});
|
|
245
250
|
|
|
246
251
|
if (!resolveFire) throw new Error('Expected manual fire request');
|
|
@@ -262,13 +267,12 @@ describe('workflow run API hooks', () => {
|
|
|
262
267
|
configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
|
|
263
268
|
const {result, queryClient} = renderWithQueryClient(() => useFireManualWorkflowMutation());
|
|
264
269
|
const listKey = workflowRunsQueryKeys.list(PROJECT_ID, {});
|
|
265
|
-
queryClient.setQueryData<InfiniteData<
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
);
|
|
270
|
+
queryClient.setQueryData<InfiniteData<WorkflowRunListPage, string | undefined>>(listKey, {
|
|
271
|
+
pages: [
|
|
272
|
+
toWorkflowRunListPage(workflowRunListResponseDto({runs: [], filtered_total_count: 0})),
|
|
273
|
+
],
|
|
274
|
+
pageParams: [undefined],
|
|
275
|
+
});
|
|
272
276
|
|
|
273
277
|
act(() => {
|
|
274
278
|
result.current.mutate({projectId: PROJECT_ID, definitionId: DEFINITION_ID});
|
|
@@ -279,11 +283,9 @@ describe('workflow run API hooks', () => {
|
|
|
279
283
|
|
|
280
284
|
await waitFor(() => {
|
|
281
285
|
const cached =
|
|
282
|
-
queryClient.getQueryData<InfiniteData<
|
|
283
|
-
listKey,
|
|
284
|
-
);
|
|
286
|
+
queryClient.getQueryData<InfiniteData<WorkflowRunListPage, string | undefined>>(listKey);
|
|
285
287
|
expect(cached?.pages[0]?.runs).toHaveLength(1);
|
|
286
|
-
expect(cached?.pages[0]?.
|
|
288
|
+
expect(cached?.pages[0]?.filteredTotalCount).toBe(1);
|
|
287
289
|
});
|
|
288
290
|
|
|
289
291
|
act(() => {
|
|
@@ -296,11 +298,9 @@ describe('workflow run API hooks', () => {
|
|
|
296
298
|
let secondTempWorkflowRunId: string | undefined;
|
|
297
299
|
await waitFor(() => {
|
|
298
300
|
const cached =
|
|
299
|
-
queryClient.getQueryData<InfiniteData<
|
|
300
|
-
listKey,
|
|
301
|
-
);
|
|
301
|
+
queryClient.getQueryData<InfiniteData<WorkflowRunListPage, string | undefined>>(listKey);
|
|
302
302
|
expect(cached?.pages[0]?.runs).toHaveLength(2);
|
|
303
|
-
expect(cached?.pages[0]?.
|
|
303
|
+
expect(cached?.pages[0]?.filteredTotalCount).toBe(2);
|
|
304
304
|
secondTempWorkflowRunId = cached?.pages[0]?.runs[0]?.id;
|
|
305
305
|
expect(secondTempWorkflowRunId).toMatch(TEMP_RUN_ID_PATTERN);
|
|
306
306
|
});
|
|
@@ -311,11 +311,9 @@ describe('workflow run API hooks', () => {
|
|
|
311
311
|
|
|
312
312
|
await waitFor(() => {
|
|
313
313
|
const cached =
|
|
314
|
-
queryClient.getQueryData<InfiniteData<
|
|
315
|
-
listKey,
|
|
316
|
-
);
|
|
314
|
+
queryClient.getQueryData<InfiniteData<WorkflowRunListPage, string | undefined>>(listKey);
|
|
317
315
|
expect(cached?.pages[0]?.runs.map((run) => run.id)).toEqual([secondTempWorkflowRunId]);
|
|
318
|
-
expect(cached?.pages[0]?.
|
|
316
|
+
expect(cached?.pages[0]?.filteredTotalCount).toBe(1);
|
|
319
317
|
});
|
|
320
318
|
|
|
321
319
|
act(() => {
|
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
import
|
|
2
|
-
RerunWorkflowRunBodyDto,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import {
|
|
2
|
+
type RerunWorkflowRunBodyDto,
|
|
3
|
+
type WorkflowRunRerunModeDto,
|
|
4
|
+
workflowRunAttemptsResponseSchema,
|
|
5
|
+
workflowRunDetailResponseSchema,
|
|
6
|
+
workflowRunDtoSchema,
|
|
7
|
+
workflowRunListResponseSchema,
|
|
8
|
+
workflowRunResponseSchema,
|
|
9
9
|
} from '@shipfox/api-workflows-dto';
|
|
10
|
-
import {
|
|
10
|
+
import {checkedApiRequest, type StandardSchema} from '@shipfox/client-api';
|
|
11
11
|
import {
|
|
12
12
|
type InfiniteData,
|
|
13
|
+
infiniteQueryOptions,
|
|
13
14
|
keepPreviousData,
|
|
15
|
+
queryOptions,
|
|
16
|
+
type UseInfiniteQueryOptions,
|
|
17
|
+
type UseQueryOptions,
|
|
14
18
|
useInfiniteQuery,
|
|
15
19
|
useMutation,
|
|
16
20
|
useQuery,
|
|
@@ -18,16 +22,21 @@ import {
|
|
|
18
22
|
} from '@tanstack/react-query';
|
|
19
23
|
import {
|
|
20
24
|
isWorkflowRunTerminal,
|
|
21
|
-
|
|
22
|
-
toWorkflowRunDetail,
|
|
23
|
-
toWorkflowRunListItem,
|
|
24
|
-
toWorkflowRunListPage,
|
|
25
|
+
type ManualWorkflowLaunch,
|
|
25
26
|
type WorkflowRun,
|
|
26
27
|
type WorkflowRunAttempt,
|
|
28
|
+
WorkflowRunAttemptSummary,
|
|
27
29
|
type WorkflowRunDetail,
|
|
30
|
+
type WorkflowRunListItem,
|
|
28
31
|
type WorkflowRunListPage,
|
|
29
32
|
type WorkflowRunStatus,
|
|
30
33
|
} from '#core/workflow-run.js';
|
|
34
|
+
import {
|
|
35
|
+
toWorkflowRunAttempt,
|
|
36
|
+
toWorkflowRunDetail,
|
|
37
|
+
toWorkflowRunListItem,
|
|
38
|
+
toWorkflowRunListPage,
|
|
39
|
+
} from './workflow-run-mapper.js';
|
|
31
40
|
|
|
32
41
|
export interface WorkflowRunFilters {
|
|
33
42
|
status?: WorkflowRunStatus | undefined;
|
|
@@ -48,6 +57,35 @@ export const workflowRunsQueryKeys = {
|
|
|
48
57
|
[...workflowRunsQueryKeys.all, 'attempts', workflowRunId] as const,
|
|
49
58
|
};
|
|
50
59
|
|
|
60
|
+
type WorkflowRunsListQueryKey =
|
|
61
|
+
| ReturnType<typeof workflowRunsQueryKeys.list>
|
|
62
|
+
| readonly ['workflow-runs', 'list'];
|
|
63
|
+
type WorkflowRunDetailQueryKey =
|
|
64
|
+
| ReturnType<typeof workflowRunsQueryKeys.detail>
|
|
65
|
+
| readonly ['workflow-runs', 'detail'];
|
|
66
|
+
type WorkflowRunAttemptsQueryKey =
|
|
67
|
+
| ReturnType<typeof workflowRunsQueryKeys.attempts>
|
|
68
|
+
| readonly ['workflow-runs', 'attempts'];
|
|
69
|
+
type WorkflowRunsInfiniteQueryOptions = UseInfiniteQueryOptions<
|
|
70
|
+
WorkflowRunListPage,
|
|
71
|
+
Error,
|
|
72
|
+
InfiniteData<WorkflowRunListPage, string | undefined>,
|
|
73
|
+
WorkflowRunsListQueryKey,
|
|
74
|
+
string | undefined
|
|
75
|
+
>;
|
|
76
|
+
type WorkflowRunDetailQueryOptions = UseQueryOptions<
|
|
77
|
+
WorkflowRunDetail,
|
|
78
|
+
Error,
|
|
79
|
+
WorkflowRunDetail,
|
|
80
|
+
WorkflowRunDetailQueryKey
|
|
81
|
+
>;
|
|
82
|
+
type WorkflowRunAttemptsQueryOptions = UseQueryOptions<
|
|
83
|
+
WorkflowRunAttempt[],
|
|
84
|
+
Error,
|
|
85
|
+
WorkflowRunAttempt[],
|
|
86
|
+
WorkflowRunAttemptsQueryKey
|
|
87
|
+
>;
|
|
88
|
+
|
|
51
89
|
function normalizeFilters(filters: WorkflowRunFilters) {
|
|
52
90
|
return {
|
|
53
91
|
status: filters.status ?? null,
|
|
@@ -66,7 +104,7 @@ function appendFilters(params: URLSearchParams, filters: WorkflowRunFilters) {
|
|
|
66
104
|
if (filters.createdTo) params.set('created_to', filters.createdTo);
|
|
67
105
|
}
|
|
68
106
|
|
|
69
|
-
async function
|
|
107
|
+
async function listWorkflowRuns({
|
|
70
108
|
projectId,
|
|
71
109
|
filters,
|
|
72
110
|
limit = 50,
|
|
@@ -78,13 +116,16 @@ async function listWorkflowRunsDto({
|
|
|
78
116
|
limit?: number;
|
|
79
117
|
cursor?: string | undefined;
|
|
80
118
|
signal?: AbortSignal;
|
|
81
|
-
}) {
|
|
119
|
+
}): Promise<WorkflowRunListPage> {
|
|
82
120
|
const params = new URLSearchParams({project_id: projectId, limit: String(limit)});
|
|
83
121
|
if (cursor) params.set('cursor', cursor);
|
|
84
122
|
appendFilters(params, filters);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
123
|
+
const response = await checkedApiRequest(
|
|
124
|
+
workflowRunListResponseSchema,
|
|
125
|
+
`/workflows/runs?${params.toString()}`,
|
|
126
|
+
{signal},
|
|
127
|
+
);
|
|
128
|
+
return toWorkflowRunListPage(response);
|
|
88
129
|
}
|
|
89
130
|
|
|
90
131
|
/**
|
|
@@ -100,35 +141,36 @@ export async function fireManualWorkflow({
|
|
|
100
141
|
}: {
|
|
101
142
|
definitionId: string;
|
|
102
143
|
inputs?: Record<string, unknown>;
|
|
103
|
-
}) {
|
|
104
|
-
|
|
144
|
+
}): Promise<ManualWorkflowLaunch> {
|
|
145
|
+
const response = await checkedApiRequest(
|
|
146
|
+
manualWorkflowResponseSchema,
|
|
105
147
|
`/workflow-definitions/${definitionId}/fire-manual`,
|
|
106
148
|
{
|
|
107
149
|
method: 'POST',
|
|
108
150
|
body: inputs ? {inputs} : {},
|
|
109
151
|
},
|
|
110
152
|
);
|
|
153
|
+
return {workflowRunId: response.workflow_run_id};
|
|
111
154
|
}
|
|
112
155
|
|
|
113
156
|
const ACTIVE_POLL_MS = 4_000;
|
|
114
157
|
const IDLE_POLL_MS = 30_000;
|
|
115
158
|
|
|
116
|
-
type RunListInfinite = InfiniteData<
|
|
117
|
-
|
|
118
|
-
function toWorkflowRunInfiniteData(
|
|
119
|
-
data: InfiniteData<WorkflowRunListResponseDto, string | undefined>,
|
|
120
|
-
): InfiniteData<WorkflowRunListPage, string | undefined> {
|
|
121
|
-
return {
|
|
122
|
-
...data,
|
|
123
|
-
pages: data.pages.map(toWorkflowRunListPage),
|
|
124
|
-
};
|
|
125
|
-
}
|
|
159
|
+
type RunListInfinite = InfiniteData<WorkflowRunListPage, string | undefined>;
|
|
126
160
|
|
|
127
161
|
export function useWorkflowRunsInfiniteQuery(
|
|
128
162
|
projectId: string | undefined,
|
|
129
163
|
filters: WorkflowRunFilters,
|
|
130
164
|
limit = 50,
|
|
131
165
|
) {
|
|
166
|
+
return useInfiniteQuery(workflowRunsInfiniteQueryOptions(projectId, filters, limit));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function workflowRunsInfiniteQueryOptions(
|
|
170
|
+
projectId: string | undefined,
|
|
171
|
+
filters: WorkflowRunFilters,
|
|
172
|
+
limit = 50,
|
|
173
|
+
): WorkflowRunsInfiniteQueryOptions {
|
|
132
174
|
// Polling is owned by react-query, not the page. Polling fast (4s) while
|
|
133
175
|
// any non-terminal run is visible covers state transitions; polling slow
|
|
134
176
|
// (30s) when idle covers brand-new external runs (webhook/schedule
|
|
@@ -140,16 +182,15 @@ export function useWorkflowRunsInfiniteQuery(
|
|
|
140
182
|
// of rows can drop into a between-pages gap. Users who scrolled into
|
|
141
183
|
// history opted into "reading mode": pause until they refocus, filter,
|
|
142
184
|
// or scroll back.
|
|
143
|
-
return
|
|
185
|
+
return infiniteQueryOptions({
|
|
144
186
|
queryKey: projectId
|
|
145
187
|
? workflowRunsQueryKeys.list(projectId, filters)
|
|
146
|
-
: [...workflowRunsQueryKeys.all, 'list'],
|
|
188
|
+
: ([...workflowRunsQueryKeys.all, 'list'] as const),
|
|
147
189
|
enabled: Boolean(projectId),
|
|
148
190
|
initialPageParam: undefined as string | undefined,
|
|
149
191
|
queryFn: ({pageParam, signal}) =>
|
|
150
|
-
|
|
151
|
-
getNextPageParam: (lastPage) => lastPage.
|
|
152
|
-
select: toWorkflowRunInfiniteData,
|
|
192
|
+
listWorkflowRuns({projectId: projectId ?? '', filters, limit, cursor: pageParam, signal}),
|
|
193
|
+
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
|
|
153
194
|
placeholderData: keepPreviousData,
|
|
154
195
|
staleTime: 2_000,
|
|
155
196
|
refetchOnWindowFocus: true,
|
|
@@ -165,7 +206,7 @@ export function useWorkflowRunsInfiniteQuery(
|
|
|
165
206
|
});
|
|
166
207
|
}
|
|
167
208
|
|
|
168
|
-
async function
|
|
209
|
+
async function getWorkflowRun({
|
|
169
210
|
workflowRunId,
|
|
170
211
|
runAttempt,
|
|
171
212
|
signal,
|
|
@@ -173,37 +214,48 @@ async function getWorkflowRunDto({
|
|
|
173
214
|
workflowRunId: string;
|
|
174
215
|
runAttempt?: number | undefined;
|
|
175
216
|
signal?: AbortSignal;
|
|
176
|
-
}) {
|
|
217
|
+
}): Promise<WorkflowRunDetail> {
|
|
177
218
|
const params = new URLSearchParams();
|
|
178
219
|
if (runAttempt) params.set('attempt', String(runAttempt));
|
|
179
220
|
const query = params.size > 0 ? `?${params.toString()}` : '';
|
|
180
|
-
return
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
221
|
+
return toWorkflowRunDetail(
|
|
222
|
+
await checkedApiRequest(
|
|
223
|
+
workflowRunDetailResponseSchema,
|
|
224
|
+
`/workflows/runs/${workflowRunId}${query}`,
|
|
225
|
+
{
|
|
226
|
+
signal,
|
|
227
|
+
},
|
|
228
|
+
),
|
|
185
229
|
);
|
|
186
230
|
}
|
|
187
231
|
|
|
188
|
-
async function
|
|
232
|
+
async function getWorkflowRunAttempts({
|
|
189
233
|
workflowRunId,
|
|
190
234
|
signal,
|
|
191
235
|
}: {
|
|
192
236
|
workflowRunId: string;
|
|
193
237
|
signal?: AbortSignal;
|
|
194
|
-
}) {
|
|
195
|
-
|
|
238
|
+
}): Promise<WorkflowRunAttempt[]> {
|
|
239
|
+
const response = await checkedApiRequest(
|
|
240
|
+
workflowRunAttemptsResponseSchema,
|
|
196
241
|
`/workflows/runs/${workflowRunId}/attempts`,
|
|
197
242
|
{
|
|
198
243
|
signal,
|
|
199
244
|
},
|
|
200
245
|
);
|
|
246
|
+
return response.attempts.map(toWorkflowRunAttempt);
|
|
201
247
|
}
|
|
202
248
|
|
|
203
|
-
async function
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
249
|
+
async function cancelWorkflowRun({
|
|
250
|
+
workflowRunId,
|
|
251
|
+
}: {
|
|
252
|
+
workflowRunId: string;
|
|
253
|
+
}): Promise<WorkflowRunListItem> {
|
|
254
|
+
return toWorkflowRunListItem(
|
|
255
|
+
await checkedApiRequest(workflowRunDtoSchema, `/workflows/runs/${workflowRunId}/cancel`, {
|
|
256
|
+
method: 'POST',
|
|
257
|
+
}),
|
|
258
|
+
);
|
|
207
259
|
}
|
|
208
260
|
|
|
209
261
|
export async function rerunWorkflowRun({
|
|
@@ -212,13 +264,32 @@ export async function rerunWorkflowRun({
|
|
|
212
264
|
}: {
|
|
213
265
|
workflowRunId: string;
|
|
214
266
|
mode: WorkflowRunRerunModeDto;
|
|
215
|
-
}) {
|
|
216
|
-
return
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
267
|
+
}): Promise<WorkflowRunListItem> {
|
|
268
|
+
return toWorkflowRunListItem(
|
|
269
|
+
await checkedApiRequest(workflowRunResponseSchema, `/workflows/runs/${workflowRunId}/rerun`, {
|
|
270
|
+
method: 'POST',
|
|
271
|
+
body: {mode} satisfies RerunWorkflowRunBodyDto,
|
|
272
|
+
}),
|
|
273
|
+
);
|
|
220
274
|
}
|
|
221
275
|
|
|
276
|
+
const manualWorkflowResponseSchema: StandardSchema<unknown, {workflow_run_id: string}> = {
|
|
277
|
+
'~standard': {
|
|
278
|
+
version: 1,
|
|
279
|
+
vendor: 'client-workflows',
|
|
280
|
+
validate: (value) => {
|
|
281
|
+
if (
|
|
282
|
+
typeof value === 'object' &&
|
|
283
|
+
value !== null &&
|
|
284
|
+
'workflow_run_id' in value &&
|
|
285
|
+
typeof value.workflow_run_id === 'string'
|
|
286
|
+
)
|
|
287
|
+
return {value: {workflow_run_id: value.workflow_run_id}};
|
|
288
|
+
return {issues: [{message: 'Expected workflow_run_id.'}]};
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
|
|
222
293
|
export function useRerunWorkflowRunMutation(projectId: string) {
|
|
223
294
|
const queryClient = useQueryClient();
|
|
224
295
|
|
|
@@ -261,25 +332,36 @@ function buildTempRun({
|
|
|
261
332
|
definitionId: string;
|
|
262
333
|
name: string;
|
|
263
334
|
createdAt: string;
|
|
264
|
-
}):
|
|
335
|
+
}): WorkflowRunListItem {
|
|
336
|
+
const id = `temp-${cryptoRandomId()}`;
|
|
265
337
|
return {
|
|
266
|
-
id
|
|
267
|
-
|
|
268
|
-
|
|
338
|
+
id,
|
|
339
|
+
projectId,
|
|
340
|
+
definitionId,
|
|
269
341
|
name,
|
|
270
342
|
status: 'pending',
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
343
|
+
currentAttempt: 1,
|
|
344
|
+
latestAttempt: 1,
|
|
345
|
+
triggerProvider: null,
|
|
346
|
+
triggerSource: 'manual',
|
|
347
|
+
triggerEvent: 'fire',
|
|
348
|
+
triggerPayload: {source: 'manual', event: 'fire'},
|
|
349
|
+
triggerDisplayLabel: 'fire',
|
|
350
|
+
triggerLabel: 'manual · fire',
|
|
277
351
|
inputs: null,
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
352
|
+
sourceSnapshot: null,
|
|
353
|
+
createdAt,
|
|
354
|
+
updatedAt: createdAt,
|
|
355
|
+
shortId: id.slice(0, 8),
|
|
356
|
+
isTemporary: true,
|
|
357
|
+
runAttempt: new WorkflowRunAttemptSummary({
|
|
358
|
+
workflowRunId: id,
|
|
359
|
+
attempt: 1,
|
|
360
|
+
status: 'pending',
|
|
361
|
+
createdAt,
|
|
362
|
+
startedAt: null,
|
|
363
|
+
finishedAt: null,
|
|
364
|
+
}),
|
|
283
365
|
};
|
|
284
366
|
}
|
|
285
367
|
|
|
@@ -334,11 +416,11 @@ export function useFireManualWorkflowMutation() {
|
|
|
334
416
|
if (!current || current.pages.length === 0) return current;
|
|
335
417
|
const firstPage = current.pages[0];
|
|
336
418
|
if (!firstPage) return current;
|
|
337
|
-
const nextFirstPage:
|
|
419
|
+
const nextFirstPage: WorkflowRunListPage = {
|
|
338
420
|
...firstPage,
|
|
339
421
|
runs: [tempRun, ...firstPage.runs],
|
|
340
|
-
|
|
341
|
-
firstPage.
|
|
422
|
+
filteredTotalCount:
|
|
423
|
+
firstPage.filteredTotalCount != null ? firstPage.filteredTotalCount + 1 : null,
|
|
342
424
|
};
|
|
343
425
|
return {...current, pages: [nextFirstPage, ...current.pages.slice(1)]};
|
|
344
426
|
});
|
|
@@ -362,9 +444,9 @@ export function useFireManualWorkflowMutation() {
|
|
|
362
444
|
return {
|
|
363
445
|
...page,
|
|
364
446
|
runs,
|
|
365
|
-
|
|
366
|
-
page.
|
|
367
|
-
? Math.max(0, page.
|
|
447
|
+
filteredTotalCount:
|
|
448
|
+
page.filteredTotalCount != null
|
|
449
|
+
? Math.max(0, page.filteredTotalCount - removedCount)
|
|
368
450
|
: null,
|
|
369
451
|
};
|
|
370
452
|
});
|
|
@@ -401,7 +483,7 @@ function lookupDefinitionName(
|
|
|
401
483
|
definitionId: string,
|
|
402
484
|
): string | undefined {
|
|
403
485
|
const entries = queryClient.getQueriesData<
|
|
404
|
-
InfiniteData<{definitions: Array<{id: string;
|
|
486
|
+
InfiniteData<{definitions: Array<{id: string; name: string}>}>
|
|
405
487
|
>({queryKey: ['definitions', 'list', projectId]});
|
|
406
488
|
for (const [, data] of entries) {
|
|
407
489
|
if (!data) continue;
|
|
@@ -424,21 +506,29 @@ export function useWorkflowRunAttemptQuery({
|
|
|
424
506
|
workflowRunId: string | undefined;
|
|
425
507
|
runAttempt?: number | undefined;
|
|
426
508
|
}) {
|
|
509
|
+
return useQuery(workflowRunQueryOptions({workflowRunId, runAttempt}));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export function workflowRunQueryOptions({
|
|
513
|
+
workflowRunId,
|
|
514
|
+
runAttempt,
|
|
515
|
+
}: {
|
|
516
|
+
workflowRunId: string | undefined;
|
|
517
|
+
runAttempt?: number | undefined;
|
|
518
|
+
}): WorkflowRunDetailQueryOptions {
|
|
427
519
|
// Poll a non-terminal run so the open run detail stays live (same cadence as the run
|
|
428
520
|
// list); stop once the run is terminal.
|
|
429
|
-
return
|
|
521
|
+
return queryOptions({
|
|
430
522
|
queryKey: workflowRunId
|
|
431
523
|
? workflowRunsQueryKeys.detail(workflowRunId, runAttempt)
|
|
432
|
-
: [...workflowRunsQueryKeys.all, 'detail'],
|
|
524
|
+
: ([...workflowRunsQueryKeys.all, 'detail'] as const),
|
|
433
525
|
enabled: Boolean(workflowRunId),
|
|
434
|
-
queryFn: ({signal}) =>
|
|
435
|
-
getWorkflowRunDto({workflowRunId: workflowRunId ?? '', runAttempt, signal}),
|
|
436
|
-
select: toWorkflowRunDetail,
|
|
526
|
+
queryFn: ({signal}) => getWorkflowRun({workflowRunId: workflowRunId ?? '', runAttempt, signal}),
|
|
437
527
|
staleTime: 2_000,
|
|
438
528
|
refetchOnWindowFocus: true,
|
|
439
529
|
refetchInterval: (query) => {
|
|
440
530
|
const status: WorkflowRunDetail['runAttempt']['status'] | undefined =
|
|
441
|
-
query.state.data?.
|
|
531
|
+
query.state.data?.runAttempt.status;
|
|
442
532
|
if (!status) return false;
|
|
443
533
|
return isWorkflowRunTerminal(status) ? false : ACTIVE_POLL_MS;
|
|
444
534
|
},
|
|
@@ -453,13 +543,22 @@ export function useWorkflowRunAttemptsQuery({
|
|
|
453
543
|
workflowRunId: string | undefined;
|
|
454
544
|
enabled: boolean;
|
|
455
545
|
}) {
|
|
456
|
-
return useQuery({
|
|
546
|
+
return useQuery(workflowRunAttemptsQueryOptions({workflowRunId, enabled}));
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export function workflowRunAttemptsQueryOptions({
|
|
550
|
+
workflowRunId,
|
|
551
|
+
enabled,
|
|
552
|
+
}: {
|
|
553
|
+
workflowRunId: string | undefined;
|
|
554
|
+
enabled: boolean;
|
|
555
|
+
}): WorkflowRunAttemptsQueryOptions {
|
|
556
|
+
return queryOptions({
|
|
457
557
|
queryKey: workflowRunId
|
|
458
558
|
? workflowRunsQueryKeys.attempts(workflowRunId)
|
|
459
|
-
: [...workflowRunsQueryKeys.all, 'attempts'],
|
|
559
|
+
: ([...workflowRunsQueryKeys.all, 'attempts'] as const),
|
|
460
560
|
enabled: Boolean(workflowRunId) && enabled,
|
|
461
|
-
queryFn: ({signal}) =>
|
|
462
|
-
select: (dto): WorkflowRunAttempt[] => dto.attempts.map(toWorkflowRunAttempt),
|
|
561
|
+
queryFn: ({signal}) => getWorkflowRunAttempts({workflowRunId: workflowRunId ?? '', signal}),
|
|
463
562
|
staleTime: 0,
|
|
464
563
|
refetchOnMount: true,
|
|
465
564
|
refetchOnWindowFocus: false,
|
|
@@ -469,9 +568,9 @@ export function useWorkflowRunAttemptsQuery({
|
|
|
469
568
|
export function useCancelWorkflowRunMutation(run: WorkflowRun | undefined) {
|
|
470
569
|
const queryClient = useQueryClient();
|
|
471
570
|
return useMutation({
|
|
472
|
-
mutationFn: async () => {
|
|
571
|
+
mutationFn: async (): Promise<WorkflowRunListItem> => {
|
|
473
572
|
if (!run) throw new Error('Workflow run is not loaded');
|
|
474
|
-
return
|
|
573
|
+
return await cancelWorkflowRun({workflowRunId: run.id});
|
|
475
574
|
},
|
|
476
575
|
onSuccess: async () => {
|
|
477
576
|
if (!run) return;
|