@shipfox/client-workflows 5.0.0 → 6.0.1
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 +53 -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/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
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ export type {
|
|
|
5
5
|
JobExecutionStatus,
|
|
6
6
|
JobExecutionTime,
|
|
7
7
|
JobStatus,
|
|
8
|
+
ManualWorkflowLaunch,
|
|
8
9
|
Step,
|
|
9
10
|
StepAttempt,
|
|
10
11
|
StepAttemptDisplayDuration,
|
|
@@ -40,6 +41,9 @@ export {
|
|
|
40
41
|
useWorkflowRunQuery,
|
|
41
42
|
useWorkflowRunsInfiniteQuery,
|
|
42
43
|
type WorkflowRunFilters,
|
|
44
|
+
workflowRunAttemptsQueryOptions,
|
|
45
|
+
workflowRunQueryOptions,
|
|
46
|
+
workflowRunsInfiniteQueryOptions,
|
|
43
47
|
workflowRunsQueryKeys,
|
|
44
48
|
} from './hooks/api/workflow-runs.js';
|
|
45
49
|
export {ProjectWorkflowsPage} from './pages/project-workflows-page.js';
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import type {DefinitionDto, DefinitionSyncSummaryDto} from '@shipfox/api-definitions-dto';
|
|
2
1
|
import {ApiError} from '@shipfox/client-api';
|
|
3
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type Definition,
|
|
4
|
+
type DefinitionSyncSummary,
|
|
5
|
+
SourceStrip,
|
|
6
|
+
useDefinitionsInfiniteQuery,
|
|
7
|
+
useProjectQuery,
|
|
8
|
+
} from '@shipfox/client-projects';
|
|
4
9
|
import {QueryLoadError} from '@shipfox/client-ui';
|
|
5
10
|
import {Button} from '@shipfox/react-ui/button';
|
|
6
11
|
import {Callout} from '@shipfox/react-ui/callout';
|
|
@@ -42,14 +47,14 @@ function ProjectWorkflowsPageInner({projectId}: {projectId: string}) {
|
|
|
42
47
|
const projectQuery = useProjectQuery(projectId);
|
|
43
48
|
const definitionsQuery = useDefinitionsInfiniteQuery(projectId);
|
|
44
49
|
const fireManual = useFireManualWorkflowMutation();
|
|
45
|
-
const [selectedDefinition, setSelectedDefinition] = useState<
|
|
50
|
+
const [selectedDefinition, setSelectedDefinition] = useState<Definition | null>(null);
|
|
46
51
|
const [runError, setRunError] = useState<{definitionId: string; message: string} | null>(null);
|
|
47
52
|
const definitions = definitionsQuery.data?.pages.flatMap((page) => page.definitions) ?? [];
|
|
48
53
|
const sync = definitionsQuery.data?.pages[0]?.sync;
|
|
49
54
|
|
|
50
|
-
async function handleRun(definition:
|
|
55
|
+
async function handleRun(definition: Definition) {
|
|
51
56
|
setRunError(null);
|
|
52
|
-
if (!definition.
|
|
57
|
+
if (!definition.manualTrigger) return;
|
|
53
58
|
try {
|
|
54
59
|
await fireManual.mutateAsync({projectId, definitionId: definition.id});
|
|
55
60
|
toast.success('Run queued');
|
|
@@ -91,8 +96,8 @@ function ProjectWorkflowsPageInner({projectId}: {projectId: string}) {
|
|
|
91
96
|
</header>
|
|
92
97
|
|
|
93
98
|
<SourceStrip
|
|
94
|
-
connectionId={projectQuery.data.source.
|
|
95
|
-
externalRepositoryId={projectQuery.data.source.
|
|
99
|
+
connectionId={projectQuery.data.source.connectionId}
|
|
100
|
+
externalRepositoryId={projectQuery.data.source.externalRepositoryId}
|
|
96
101
|
sync={sync}
|
|
97
102
|
isPending={definitionsQuery.isPending}
|
|
98
103
|
/>
|
|
@@ -148,10 +153,10 @@ function WorkflowDefinitionsList({
|
|
|
148
153
|
onOpenDefinition,
|
|
149
154
|
onRun,
|
|
150
155
|
}: {
|
|
151
|
-
definitions:
|
|
156
|
+
definitions: Definition[];
|
|
152
157
|
isPending: boolean;
|
|
153
158
|
isError: boolean;
|
|
154
|
-
sync:
|
|
159
|
+
sync: DefinitionSyncSummary | null;
|
|
155
160
|
runError: {definitionId: string; message: string} | null;
|
|
156
161
|
runningDefinitionId: string | null;
|
|
157
162
|
hasNextPage: boolean;
|
|
@@ -159,8 +164,8 @@ function WorkflowDefinitionsList({
|
|
|
159
164
|
isFetchNextPageError: boolean;
|
|
160
165
|
onRetry: () => void;
|
|
161
166
|
onLoadMore: () => void;
|
|
162
|
-
onOpenDefinition: (definition:
|
|
163
|
-
onRun: (definition:
|
|
167
|
+
onOpenDefinition: (definition: Definition) => void;
|
|
168
|
+
onRun: (definition: Definition) => void;
|
|
164
169
|
}) {
|
|
165
170
|
if (isPending) {
|
|
166
171
|
return (
|
|
@@ -232,7 +237,7 @@ function WorkflowDefinitionsList({
|
|
|
232
237
|
{definition.name}
|
|
233
238
|
</Text>
|
|
234
239
|
<Code className="truncate text-foreground-neutral-muted">
|
|
235
|
-
{definition.
|
|
240
|
+
{definition.configPath ?? 'Manual definition'}
|
|
236
241
|
</Code>
|
|
237
242
|
</button>
|
|
238
243
|
{runErrorMessage ? (
|
|
@@ -243,10 +248,10 @@ function WorkflowDefinitionsList({
|
|
|
243
248
|
</div>
|
|
244
249
|
</TableCell>
|
|
245
250
|
<TableCell className="text-foreground-neutral-muted">
|
|
246
|
-
<RelativeTime value={definition.
|
|
251
|
+
<RelativeTime value={definition.updatedAt} />
|
|
247
252
|
</TableCell>
|
|
248
253
|
<TableCell>
|
|
249
|
-
{definition.
|
|
254
|
+
{definition.manualTrigger ? (
|
|
250
255
|
<div className="flex justify-end opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100">
|
|
251
256
|
<Button size="xs" isLoading={isRunning} onClick={() => onRun(definition)}>
|
|
252
257
|
Run
|
|
@@ -287,15 +292,15 @@ function WorkflowDefinitionsList({
|
|
|
287
292
|
{definition.name}
|
|
288
293
|
</Text>
|
|
289
294
|
<Code className="break-words text-foreground-neutral-muted">
|
|
290
|
-
{definition.
|
|
295
|
+
{definition.configPath ?? 'Manual definition'}
|
|
291
296
|
</Code>
|
|
292
297
|
</div>
|
|
293
298
|
</button>
|
|
294
299
|
<div className="flex items-center justify-between gap-8">
|
|
295
300
|
<Text size="xs" className="text-foreground-neutral-muted">
|
|
296
|
-
Updated <RelativeTime value={definition.
|
|
301
|
+
Updated <RelativeTime value={definition.updatedAt} />
|
|
297
302
|
</Text>
|
|
298
|
-
{definition.
|
|
303
|
+
{definition.manualTrigger ? (
|
|
299
304
|
<Button size="sm" isLoading={isRunning} onClick={() => onRun(definition)}>
|
|
300
305
|
Run
|
|
301
306
|
</Button>
|
|
@@ -337,12 +342,12 @@ function sourceIcon(source: 'manual' | 'vcs'): IconName {
|
|
|
337
342
|
return source === 'vcs' ? ('gitBranchLine' as IconName) : ('terminalLine' as IconName);
|
|
338
343
|
}
|
|
339
344
|
|
|
340
|
-
function WorkflowEmptyState({sync}: {sync:
|
|
345
|
+
function WorkflowEmptyState({sync}: {sync: DefinitionSyncSummary | null}) {
|
|
341
346
|
const message =
|
|
342
|
-
sync?.status === 'failed' && sync.
|
|
347
|
+
sync?.status === 'failed' && sync.lastErrorCode === 'no-workflow-files'
|
|
343
348
|
? 'No workflow files found under .shipfox/workflows/.'
|
|
344
349
|
: sync?.status === 'failed'
|
|
345
|
-
? (sync.
|
|
350
|
+
? (sync.lastErrorMessage ?? 'Workflow definitions could not be synced.')
|
|
346
351
|
: sync?.status === 'syncing'
|
|
347
352
|
? 'Workflow definitions are being discovered.'
|
|
348
353
|
: sync?.status === 'succeeded'
|
|
@@ -352,7 +357,7 @@ function WorkflowEmptyState({sync}: {sync: DefinitionSyncSummaryDto | null}) {
|
|
|
352
357
|
return <EmptyState icon="flowChart" title="No workflows" description={message} />;
|
|
353
358
|
}
|
|
354
359
|
|
|
355
|
-
function WorkflowSyncAlert({sync}: {sync:
|
|
360
|
+
function WorkflowSyncAlert({sync}: {sync: DefinitionSyncSummary | null | undefined}) {
|
|
356
361
|
if (sync?.status !== 'failed') return null;
|
|
357
362
|
|
|
358
363
|
return (
|
|
@@ -362,7 +367,7 @@ function WorkflowSyncAlert({sync}: {sync: DefinitionSyncSummaryDto | null | unde
|
|
|
362
367
|
Workflow sync failed
|
|
363
368
|
</Text>
|
|
364
369
|
<Text size="sm">
|
|
365
|
-
{sync.
|
|
370
|
+
{sync.lastErrorMessage ?? 'The latest workflow sync failed before definitions updated.'}
|
|
366
371
|
</Text>
|
|
367
372
|
</div>
|
|
368
373
|
</Callout>
|
|
@@ -373,14 +378,14 @@ function DefinitionSheet({
|
|
|
373
378
|
definition,
|
|
374
379
|
onOpenChange,
|
|
375
380
|
}: {
|
|
376
|
-
definition:
|
|
381
|
+
definition: Definition | null;
|
|
377
382
|
onOpenChange: (open: boolean) => void;
|
|
378
383
|
}) {
|
|
379
384
|
const normalizedJson = definition
|
|
380
385
|
? JSON.stringify(
|
|
381
386
|
{
|
|
382
|
-
workflow_document: definition.
|
|
383
|
-
workflow_model: definition.
|
|
387
|
+
workflow_document: definition.workflowDocument,
|
|
388
|
+
workflow_model: definition.workflowModel,
|
|
384
389
|
},
|
|
385
390
|
null,
|
|
386
391
|
2,
|
|
@@ -395,7 +400,7 @@ function DefinitionSheet({
|
|
|
395
400
|
<SheetHeader>
|
|
396
401
|
<SheetTitle>{definition.name}</SheetTitle>
|
|
397
402
|
<SheetDescription>
|
|
398
|
-
{definition.
|
|
403
|
+
{definition.configPath ?? 'Manual workflow definition'}
|
|
399
404
|
</SheetDescription>
|
|
400
405
|
</SheetHeader>
|
|
401
406
|
<SheetBody className="gap-18">
|
|
@@ -257,11 +257,12 @@ describe('WorkflowRunPage', () => {
|
|
|
257
257
|
function renderRunsPath(search = '') {
|
|
258
258
|
return renderProjectPage(
|
|
259
259
|
`/workspaces/${PROJECT_TEST_WID}/projects/${PROJECT_ID}/runs${search}`,
|
|
260
|
-
({workflowRunId}) => (
|
|
260
|
+
({workflowRunId, search}) => (
|
|
261
261
|
<WorkflowRunPage
|
|
262
262
|
workspaceId={PROJECT_TEST_WID}
|
|
263
263
|
projectId={PROJECT_ID}
|
|
264
264
|
workflowRunId={workflowRunId}
|
|
265
|
+
search={search}
|
|
265
266
|
/>
|
|
266
267
|
),
|
|
267
268
|
);
|
|
@@ -270,11 +271,12 @@ function renderRunsPath(search = '') {
|
|
|
270
271
|
function renderRunPath(search = '') {
|
|
271
272
|
return renderProjectPage(
|
|
272
273
|
`/workspaces/${PROJECT_TEST_WID}/projects/${PROJECT_ID}/runs/${RUN_ID}${search}`,
|
|
273
|
-
({workflowRunId}) => (
|
|
274
|
+
({workflowRunId, search}) => (
|
|
274
275
|
<WorkflowRunPage
|
|
275
276
|
workspaceId={PROJECT_TEST_WID}
|
|
276
277
|
projectId={PROJECT_ID}
|
|
277
278
|
workflowRunId={workflowRunId}
|
|
279
|
+
search={search}
|
|
278
280
|
/>
|
|
279
281
|
),
|
|
280
282
|
);
|
|
@@ -1,20 +1,17 @@
|
|
|
1
|
-
import {useNavigate
|
|
1
|
+
import {useNavigate} from '@tanstack/react-router';
|
|
2
2
|
import {useCallback, useEffect} from 'react';
|
|
3
3
|
import {WorkflowRunList} from '#components/workflow-run-list/workflow-run-list.js';
|
|
4
4
|
import {WorkflowRunView} from '#components/workflow-run-view/index.js';
|
|
5
|
-
import {
|
|
6
|
-
type WorkflowRunSelectionInput,
|
|
7
|
-
withoutWorkflowRunSelectionSearch,
|
|
8
|
-
withWorkflowRunSelectionSearch,
|
|
9
|
-
workflowRunSelectionFromSearch,
|
|
10
|
-
} from '#core/workflow-run-url-state.js';
|
|
5
|
+
import type {WorkflowRunSelectionInput} from '#core/workflow-run-url-state.js';
|
|
11
6
|
import {useWorkflowRunsInfiniteQuery} from '#hooks/api/workflow-runs.js';
|
|
7
|
+
import {type WorkflowRunsSearch, workflowRunSearchParams} from '#routes/inputs.js';
|
|
12
8
|
import {WorkflowRunFirstTimeUse} from './workflow-run-first-time-use.js';
|
|
13
9
|
|
|
14
10
|
interface WorkflowRunPageProps {
|
|
15
11
|
workspaceId: string;
|
|
16
12
|
projectId: string;
|
|
17
13
|
workflowRunId?: string | undefined;
|
|
14
|
+
search?: WorkflowRunsSearch;
|
|
18
15
|
}
|
|
19
16
|
|
|
20
17
|
/**
|
|
@@ -28,6 +25,7 @@ function useWorkflowRunPageTarget(
|
|
|
28
25
|
workspaceId: string,
|
|
29
26
|
projectId: string,
|
|
30
27
|
workflowRunId: string | undefined,
|
|
28
|
+
search: WorkflowRunsSearch,
|
|
31
29
|
) {
|
|
32
30
|
const navigate = useNavigate();
|
|
33
31
|
const {data, isPending} = useWorkflowRunsInfiniteQuery(projectId, {});
|
|
@@ -42,28 +40,30 @@ function useWorkflowRunPageTarget(
|
|
|
42
40
|
navigate({
|
|
43
41
|
to: '/workspaces/$wid/projects/$pid/runs/$workflowRunId',
|
|
44
42
|
params: {wid: workspaceId, pid: projectId, workflowRunId: firstWorkflowRunId},
|
|
45
|
-
search: (
|
|
46
|
-
withoutWorkflowRunSelectionSearch(previous)) as never,
|
|
43
|
+
search: workflowRunSearchParams(search, {}),
|
|
47
44
|
replace: true,
|
|
48
45
|
});
|
|
49
|
-
}, [navigate, workspaceId, projectId, workflowRunId, isLoaded, firstWorkflowRunId]);
|
|
46
|
+
}, [navigate, workspaceId, projectId, workflowRunId, isLoaded, firstWorkflowRunId, search]);
|
|
50
47
|
|
|
51
48
|
return {hasNoRuns: isLoaded && firstWorkflowRunId === undefined};
|
|
52
49
|
}
|
|
53
50
|
|
|
54
|
-
export function WorkflowRunPage({
|
|
55
|
-
|
|
51
|
+
export function WorkflowRunPage({
|
|
52
|
+
workspaceId,
|
|
53
|
+
projectId,
|
|
54
|
+
workflowRunId,
|
|
55
|
+
search = {},
|
|
56
|
+
}: WorkflowRunPageProps) {
|
|
57
|
+
const {hasNoRuns} = useWorkflowRunPageTarget(workspaceId, projectId, workflowRunId, search);
|
|
56
58
|
const navigate = useNavigate();
|
|
57
|
-
const
|
|
58
|
-
const selection = workflowRunSelectionFromSearch(search);
|
|
59
|
+
const selection: WorkflowRunSelectionInput = search;
|
|
59
60
|
const onSelectionChange = useCallback(
|
|
60
61
|
(nextSelection: WorkflowRunSelectionInput) => {
|
|
61
62
|
navigate({
|
|
62
|
-
search: (
|
|
63
|
-
withWorkflowRunSelectionSearch(previous, nextSelection)) as never,
|
|
63
|
+
search: workflowRunSearchParams(search, nextSelection) as never,
|
|
64
64
|
});
|
|
65
65
|
},
|
|
66
|
-
[navigate],
|
|
66
|
+
[navigate, search],
|
|
67
67
|
);
|
|
68
68
|
|
|
69
69
|
if (!workflowRunId && hasNoRuns) {
|
|
@@ -76,6 +76,11 @@ export function WorkflowRunPage({workspaceId, projectId, workflowRunId}: Workflo
|
|
|
76
76
|
workspaceId={workspaceId}
|
|
77
77
|
projectId={projectId}
|
|
78
78
|
selectedWorkflowRunId={workflowRunId}
|
|
79
|
+
search={search.search ?? ''}
|
|
80
|
+
statusFilter={search.status ?? 'all'}
|
|
81
|
+
onFiltersChange={(filters) =>
|
|
82
|
+
navigate({search: workflowRunSearchParams({...search, ...filters}) as never})
|
|
83
|
+
}
|
|
79
84
|
/>
|
|
80
85
|
<WorkflowRunView
|
|
81
86
|
workspaceId={workspaceId}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {validateWorkflowRunsSearch, workflowRouteParams} from './inputs.js';
|
|
2
|
+
|
|
3
|
+
describe('workflow route inputs', () => {
|
|
4
|
+
it('normalizes malformed search values to safe defaults', () => {
|
|
5
|
+
expect(validateWorkflowRunsSearch({search: ['unexpected'], status: 'unknown'})).toEqual({});
|
|
6
|
+
expect(validateWorkflowRunsSearch({status: 'failed', runAttempt: '2'})).toEqual({
|
|
7
|
+
status: 'failed',
|
|
8
|
+
runAttempt: 2,
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('requires workspace and project path parameters', () => {
|
|
13
|
+
expect(workflowRouteParams({wid: 'workspace-1', pid: 'project-1'})).toEqual({
|
|
14
|
+
wid: 'workspace-1',
|
|
15
|
+
pid: 'project-1',
|
|
16
|
+
});
|
|
17
|
+
expect(() => workflowRouteParams({wid: 'workspace-1'})).toThrow(
|
|
18
|
+
'Workflow route is missing required path parameters.',
|
|
19
|
+
);
|
|
20
|
+
expect(() => workflowRouteParams({wid: 'workspace-1', pid: null})).toThrow();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type {WorkflowRunListStatusFilter} from '#components/workflow-run-list/types.js';
|
|
2
|
+
import type {WorkflowRunSelectionInput} from '#core/workflow-run-url-state.js';
|
|
3
|
+
|
|
4
|
+
export interface WorkflowRunsSearch extends WorkflowRunSelectionInput {
|
|
5
|
+
search?: string;
|
|
6
|
+
status?: WorkflowRunListStatusFilter;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function validateWorkflowRunsSearch(input: Record<string, unknown>): WorkflowRunsSearch {
|
|
10
|
+
const search = string(input.search);
|
|
11
|
+
const status = input.status === 'failed' || input.status === 'running' ? input.status : 'all';
|
|
12
|
+
const runAttempt = positiveInteger(input.runAttempt);
|
|
13
|
+
return {
|
|
14
|
+
...(search ? {search} : {}),
|
|
15
|
+
...(status === 'all' ? {} : {status}),
|
|
16
|
+
...(string(input.job) ? {jobId: string(input.job)} : {}),
|
|
17
|
+
...(string(input.jobExecution) ? {jobExecutionId: string(input.jobExecution)} : {}),
|
|
18
|
+
...(string(input.step) ? {stepId: string(input.step)} : {}),
|
|
19
|
+
...(string(input.stepAttempt) ? {stepAttemptId: string(input.stepAttempt)} : {}),
|
|
20
|
+
...(runAttempt ? {runAttempt} : {}),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function workflowRunSearchParams(
|
|
25
|
+
search: WorkflowRunsSearch,
|
|
26
|
+
selection: WorkflowRunSelectionInput = search,
|
|
27
|
+
) {
|
|
28
|
+
return {
|
|
29
|
+
...(search.search ? {search: search.search} : {}),
|
|
30
|
+
...(search.status && search.status !== 'all' ? {status: search.status} : {}),
|
|
31
|
+
...(selection.jobId ? {job: selection.jobId} : {}),
|
|
32
|
+
...(selection.jobExecutionId ? {jobExecution: selection.jobExecutionId} : {}),
|
|
33
|
+
...(selection.stepId ? {step: selection.stepId} : {}),
|
|
34
|
+
...(selection.stepAttemptId ? {stepAttempt: selection.stepAttemptId} : {}),
|
|
35
|
+
...(selection.runAttempt ? {runAttempt: String(selection.runAttempt)} : {}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function workflowRouteParams(input: Record<string, unknown>): {
|
|
40
|
+
wid: string;
|
|
41
|
+
pid: string;
|
|
42
|
+
workflowRunId?: string;
|
|
43
|
+
} {
|
|
44
|
+
const wid = string(input.wid);
|
|
45
|
+
const pid = string(input.pid);
|
|
46
|
+
if (!wid || !pid) throw new Error('Workflow route is missing required path parameters.');
|
|
47
|
+
const workflowRunId = string(input.workflowRunId);
|
|
48
|
+
return workflowRunId ? {wid, pid, workflowRunId} : {wid, pid};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function string(value: unknown): string | undefined {
|
|
52
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function positiveInteger(value: unknown): number | undefined {
|
|
56
|
+
const number = typeof value === 'string' ? Number(value) : value;
|
|
57
|
+
return typeof number === 'number' && Number.isInteger(number) && number > 0 ? number : undefined;
|
|
58
|
+
}
|
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import {defineRoute} from '@shipfox/client-shell/runtime';
|
|
2
|
-
import {useParams} from '@tanstack/react-router';
|
|
1
|
+
import {defineRoute, useRouteParams, useRouteSearch} from '@shipfox/client-shell/runtime';
|
|
3
2
|
import {WorkflowRunPage} from '#pages/workflow-run-page.js';
|
|
3
|
+
import {validateWorkflowRunsSearch, workflowRouteParams} from './inputs.js';
|
|
4
4
|
|
|
5
5
|
export default defineRoute({
|
|
6
6
|
staticData: {layout: 'full-bleed'},
|
|
7
|
+
validateSearch: validateWorkflowRunsSearch,
|
|
7
8
|
component: () => {
|
|
8
|
-
const {wid, pid, workflowRunId} =
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
const {wid, pid, workflowRunId} = useRouteParams(workflowRouteParams);
|
|
10
|
+
return (
|
|
11
|
+
<WorkflowRunPage
|
|
12
|
+
workspaceId={wid}
|
|
13
|
+
projectId={pid}
|
|
14
|
+
workflowRunId={workflowRunId}
|
|
15
|
+
search={useRouteSearch(validateWorkflowRunsSearch)}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
14
18
|
},
|
|
15
19
|
});
|
package/src/routes/runs.tsx
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import {defineRoute} from '@shipfox/client-shell/runtime';
|
|
2
|
-
import {useParams} from '@tanstack/react-router';
|
|
1
|
+
import {defineRoute, useRouteParams, useRouteSearch} from '@shipfox/client-shell/runtime';
|
|
3
2
|
import {WorkflowRunPage} from '#pages/workflow-run-page.js';
|
|
3
|
+
import {validateWorkflowRunsSearch, workflowRouteParams} from './inputs.js';
|
|
4
4
|
|
|
5
5
|
export default defineRoute({
|
|
6
6
|
staticData: {layout: 'full-bleed'},
|
|
7
|
+
validateSearch: validateWorkflowRunsSearch,
|
|
7
8
|
component: () => {
|
|
8
|
-
const {wid, pid} =
|
|
9
|
-
return
|
|
9
|
+
const {wid, pid} = useRouteParams(workflowRouteParams);
|
|
10
|
+
return (
|
|
11
|
+
<WorkflowRunPage
|
|
12
|
+
workspaceId={wid}
|
|
13
|
+
projectId={pid}
|
|
14
|
+
search={useRouteSearch(validateWorkflowRunsSearch)}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
10
17
|
},
|
|
11
18
|
});
|
package/src/routes/workflows.tsx
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {defineRoute} from '@shipfox/client-shell/runtime';
|
|
2
|
-
import {useParams} from '@tanstack/react-router';
|
|
1
|
+
import {defineRoute, useRouteParams} from '@shipfox/client-shell/runtime';
|
|
3
2
|
import {ProjectWorkflowsPage} from '#pages/project-workflows-page.js';
|
|
3
|
+
import {workflowRouteParams} from './inputs.js';
|
|
4
4
|
|
|
5
5
|
export default defineRoute({
|
|
6
6
|
component: () => {
|
|
7
|
-
const {pid} =
|
|
7
|
+
const {pid} = useRouteParams(workflowRouteParams);
|
|
8
8
|
return <ProjectWorkflowsPage projectId={pid} />;
|
|
9
9
|
},
|
|
10
10
|
});
|
|
@@ -11,20 +11,22 @@ import type {
|
|
|
11
11
|
WorkflowRunStatusDto,
|
|
12
12
|
WorkflowRunStepDetailDto,
|
|
13
13
|
} from '@shipfox/api-workflows-dto';
|
|
14
|
+
import type {
|
|
15
|
+
Job,
|
|
16
|
+
Step,
|
|
17
|
+
StepAttempt,
|
|
18
|
+
WorkflowRunDetail,
|
|
19
|
+
WorkflowRunListItem,
|
|
20
|
+
WorkflowRunListPage,
|
|
21
|
+
} from '#core/workflow-run.js';
|
|
14
22
|
import {
|
|
15
|
-
type Job,
|
|
16
|
-
type Step,
|
|
17
|
-
type StepAttempt,
|
|
18
23
|
toJob,
|
|
19
24
|
toStep,
|
|
20
25
|
toStepAttempt,
|
|
21
26
|
toWorkflowRunDetail,
|
|
22
27
|
toWorkflowRunListItem,
|
|
23
28
|
toWorkflowRunListPage,
|
|
24
|
-
|
|
25
|
-
type WorkflowRunListItem,
|
|
26
|
-
type WorkflowRunListPage,
|
|
27
|
-
} from '#core/workflow-run.js';
|
|
29
|
+
} from '#hooks/api/workflow-run-mapper.js';
|
|
28
30
|
|
|
29
31
|
const RUN_ID = '11111111-1111-4111-8111-111111111111';
|
|
30
32
|
const RUN_ATTEMPT_ID = '11111111-1111-4111-8111-111111111112';
|
package/test/pages.tsx
CHANGED
|
@@ -10,10 +10,12 @@ import {
|
|
|
10
10
|
Outlet,
|
|
11
11
|
RouterProvider,
|
|
12
12
|
useParams,
|
|
13
|
+
useSearch,
|
|
13
14
|
} from '@tanstack/react-router';
|
|
14
15
|
import {type RenderResult, render} from '@testing-library/react';
|
|
15
16
|
import {createStore, Provider as JotaiProvider} from 'jotai';
|
|
16
17
|
import type {ReactElement} from 'react';
|
|
18
|
+
import {validateWorkflowRunsSearch} from '#routes/inputs.js';
|
|
17
19
|
|
|
18
20
|
// The workflow run page navigates with the router (run rows are links and the page redirects
|
|
19
21
|
// to the first run), so the harness mounts the page under a memory router whose route tree
|
|
@@ -41,26 +43,36 @@ export function jsonResponse(body: unknown, init: ResponseInit = {}) {
|
|
|
41
43
|
|
|
42
44
|
function createTestRouter(
|
|
43
45
|
path: string,
|
|
44
|
-
renderPage: (params: {
|
|
46
|
+
renderPage: (params: {
|
|
47
|
+
workflowRunId?: string | undefined;
|
|
48
|
+
search: ReturnType<typeof validateWorkflowRunsSearch>;
|
|
49
|
+
}) => ReactElement,
|
|
45
50
|
) {
|
|
46
51
|
const rootRoute = createRootRoute({component: Outlet});
|
|
47
52
|
const runsRoute = createRoute({
|
|
48
53
|
getParentRoute: () => rootRoute,
|
|
49
54
|
path: '/workspaces/$wid/projects/$pid/runs',
|
|
50
|
-
component: ()
|
|
55
|
+
component: function RunsRoute() {
|
|
56
|
+
return renderPage({
|
|
57
|
+
search: validateWorkflowRunsSearch(useSearch({strict: false}) as Record<string, unknown>),
|
|
58
|
+
});
|
|
59
|
+
},
|
|
51
60
|
});
|
|
52
61
|
const runDetailRoute = createRoute({
|
|
53
62
|
getParentRoute: () => rootRoute,
|
|
54
63
|
path: '/workspaces/$wid/projects/$pid/runs/$workflowRunId',
|
|
55
64
|
component: function RunDetailRoute() {
|
|
56
65
|
const {workflowRunId} = useParams({strict: false}) as {workflowRunId?: string};
|
|
57
|
-
return renderPage({
|
|
66
|
+
return renderPage({
|
|
67
|
+
workflowRunId,
|
|
68
|
+
search: validateWorkflowRunsSearch(useSearch({strict: false}) as Record<string, unknown>),
|
|
69
|
+
});
|
|
58
70
|
},
|
|
59
71
|
});
|
|
60
72
|
const workflowsRoute = createRoute({
|
|
61
73
|
getParentRoute: () => rootRoute,
|
|
62
74
|
path: '/workspaces/$wid/projects/$pid/workflows',
|
|
63
|
-
component: () => renderPage({}),
|
|
75
|
+
component: () => renderPage({search: {}}),
|
|
64
76
|
});
|
|
65
77
|
const modelProviderSettingsRoute = createRoute({
|
|
66
78
|
getParentRoute: () => rootRoute,
|
|
@@ -81,7 +93,10 @@ function createTestRouter(
|
|
|
81
93
|
|
|
82
94
|
export function renderProjectPage(
|
|
83
95
|
path: string,
|
|
84
|
-
renderPage: (params: {
|
|
96
|
+
renderPage: (params: {
|
|
97
|
+
workflowRunId?: string | undefined;
|
|
98
|
+
search: ReturnType<typeof validateWorkflowRunsSearch>;
|
|
99
|
+
}) => ReactElement,
|
|
85
100
|
): RenderResult & {
|
|
86
101
|
queryClient: QueryClient;
|
|
87
102
|
router: ReturnType<typeof createTestRouter>;
|