orcheo-studio 0.23.2 → 0.24.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/.bumpversion.cfg +1 -1
- package/dist/assets/{arc-BokkJPzk.js → arc-DhTjQsml.js} +1 -1
- package/dist/assets/{architectureDiagram-VXUJARFQ-CzmPOZBu.js → architectureDiagram-VXUJARFQ-CGDm4iVL.js} +1 -1
- package/dist/assets/{blockDiagram-VD42YOAC-DG89LeH9.js → blockDiagram-VD42YOAC-B5xlwwyL.js} +1 -1
- package/dist/assets/{c4Diagram-YG6GDRKO-DymWpJZb.js → c4Diagram-YG6GDRKO-DtVXFxFX.js} +1 -1
- package/dist/assets/channel-CeR8j9Si.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-DulKpeK2.js → chunk-4BX2VUAB-CA8vZ6Tb.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-Bi6MBeyB.js → chunk-55IACEB6-CxNWLhGM.js} +1 -1
- package/dist/assets/{chunk-B4BG7PRW-teHvBEMz.js → chunk-B4BG7PRW-BW8UEmwi.js} +1 -1
- package/dist/assets/{chunk-DI55MBZ5-Clydm5zE.js → chunk-DI55MBZ5-_6YQcHqR.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-BGh0gUNB.js → chunk-FMBD7UC4-L-9FXYi4.js} +1 -1
- package/dist/assets/{chunk-QN33PNHL-D-9fma_8.js → chunk-QN33PNHL-DZtaWvmZ.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-Bux-KM8v.js → chunk-QZHKN3VN-B3c4-un7.js} +1 -1
- package/dist/assets/{chunk-TZMSLE5B-pgx8j0M-.js → chunk-TZMSLE5B-DmyAt3KD.js} +1 -1
- package/dist/assets/classDiagram-2ON5EDUG-B2tKUrqn.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-B2tKUrqn.js +1 -0
- package/dist/assets/clone-BN-HxSe-.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-CszLJ5HU.js → cose-bilkent-S5V4N54A-r3dremCG.js} +1 -1
- package/dist/assets/{dagre-6UL2VRFP-Cr25CaNe.js → dagre-6UL2VRFP-C6AizynV.js} +1 -1
- package/dist/assets/{diagram-PSM6KHXK-CnsZe68H.js → diagram-PSM6KHXK-CKTAJrFm.js} +1 -1
- package/dist/assets/{diagram-QEK2KX5R-CN8ov45N.js → diagram-QEK2KX5R-B4BLbqZ6.js} +1 -1
- package/dist/assets/{diagram-S2PKOQOG-v2bUyBL7.js → diagram-S2PKOQOG-CLy2S4hE.js} +1 -1
- package/dist/assets/{erDiagram-Q2GNP2WA-BxxatPhi.js → erDiagram-Q2GNP2WA-DlemVcfS.js} +1 -1
- package/dist/assets/{flowDiagram-NV44I4VS-DIKLSwgl.js → flowDiagram-NV44I4VS-CpBPPeuN.js} +1 -1
- package/dist/assets/{ganttDiagram-JELNMOA3-D_0u3tXB.js → ganttDiagram-JELNMOA3-BY4cJPdg.js} +1 -1
- package/dist/assets/{gitGraphDiagram-V2S2FVAM-B_nu7ypr.js → gitGraphDiagram-V2S2FVAM-D7RJFr3i.js} +1 -1
- package/dist/assets/{graph-DmMuvtfO.js → graph-CJZvtDnb.js} +1 -1
- package/dist/assets/index-CARUdnbv.js +795 -0
- package/dist/assets/index-DcCYJX3c.css +1 -0
- package/dist/assets/{infoDiagram-HS3SLOUP-DLLSS3lr.js → infoDiagram-HS3SLOUP-CcSzbqKG.js} +1 -1
- package/dist/assets/{isUndefined-u8cwznUN.js → isUndefined-BbMiFvTc.js} +1 -1
- package/dist/assets/{journeyDiagram-XKPGCS4Q-fF1HZxKQ.js → journeyDiagram-XKPGCS4Q-DTmP5k2H.js} +1 -1
- package/dist/assets/{kanban-definition-3W4ZIXB7-CcmlUntf.js → kanban-definition-3W4ZIXB7-BSA4gDhk.js} +1 -1
- package/dist/assets/{layout-CuCF7WdL.js → layout-D3BxIqM1.js} +1 -1
- package/dist/assets/{linear-BVXj8lVL.js → linear-CemsbS51.js} +1 -1
- package/dist/assets/{min-DEoUCRl0.js → min-DkRMnH62.js} +1 -1
- package/dist/assets/{mindmap-definition-VGOIOE7T-DtNTD5Hm.js → mindmap-definition-VGOIOE7T-CNj0QY6e.js} +1 -1
- package/dist/assets/{pieDiagram-ADFJNKIX-a07QMF4s.js → pieDiagram-ADFJNKIX-CPQsqbb4.js} +1 -1
- package/dist/assets/{quadrantDiagram-AYHSOK5B-NxQVfG-F.js → quadrantDiagram-AYHSOK5B-CPX25nd3.js} +1 -1
- package/dist/assets/{requirementDiagram-UZGBJVZJ-DfwcBFwx.js → requirementDiagram-UZGBJVZJ-7mxxRNiB.js} +1 -1
- package/dist/assets/{sankeyDiagram-TZEHDZUN-BeRNGGtP.js → sankeyDiagram-TZEHDZUN-CPhitUZ9.js} +1 -1
- package/dist/assets/{sequenceDiagram-WL72ISMW-dTp9Xfca.js → sequenceDiagram-WL72ISMW-BwLMsC7F.js} +1 -1
- package/dist/assets/{stateDiagram-FKZM4ZOC-uXrzw1Ia.js → stateDiagram-FKZM4ZOC-Crx9CUqD.js} +1 -1
- package/dist/assets/stateDiagram-v2-4FDKWEC3-elh6u9Fl.js +1 -0
- package/dist/assets/{timeline-definition-IT6M3QCI-Jx4tDH4r.js → timeline-definition-IT6M3QCI-DDh1Iim0.js} +1 -1
- package/dist/assets/{treemap-GDKQZRPO-CYRu_RLQ.js → treemap-GDKQZRPO-Bc6lNY0z.js} +1 -1
- package/dist/assets/{xychartDiagram-PRI3JC2R-DVYMDr6g.js → xychartDiagram-PRI3JC2R-BAGZwI3e.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +2 -74
- package/src/features/account/pages/service-tokens.tsx +13 -60
- package/src/features/workflow/components/panels/rjsf-cron-widget.tsx +421 -0
- package/src/features/workflow/components/panels/rjsf-theme.tsx +2 -0
- package/src/features/workflow/data/templates/candidate-badges.ts +11 -1
- package/src/features/workflow/data/workflow-types.ts +3 -0
- package/src/features/workflow/lib/workflow-storage-api.ts +118 -2
- package/src/features/workflow/lib/workflow-storage-helpers.ts +65 -0
- package/src/features/workflow/lib/workflow-storage.ts +17 -0
- package/src/features/workflow/lib/workflow-storage.types.ts +16 -0
- package/src/features/workflow/pages/workflow/components/workflow-config-sheet.tsx +26 -5
- package/src/features/workflow/pages/workflow/components/workflow-tab-content.tsx +64 -1
- package/src/features/workflow/pages/workflow/hooks/controller/build-layout-props.ts +5 -0
- package/src/features/workflow/pages/workflow-gallery/candidate-updates.ts +107 -0
- package/src/features/workflow/pages/workflow-gallery/use-workflow-gallery-actions.ts +29 -4
- package/src/features/workflow/pages/workflow-gallery/use-workflow-gallery-state.ts +29 -25
- package/src/features/workflow/pages/workflow-gallery/workflow-card.tsx +187 -6
- package/src/features/workflow/pages/workflow-gallery/workflow-gallery-tabs.tsx +6 -0
- package/src/features/workflow/pages/workflow-gallery.tsx +2 -0
- package/src/lib/api.ts +2 -17
- package/vite.config.ts +0 -7
- package/dist/assets/channel-DPwJ8Ajm.js +0 -1
- package/dist/assets/classDiagram-2ON5EDUG-CYBYFsly.js +0 -1
- package/dist/assets/classDiagram-v2-WZHVMYZB-CYBYFsly.js +0 -1
- package/dist/assets/clone-tT9-ZTc-.js +0 -1
- package/dist/assets/index-BVY_PYJU.css +0 -1
- package/dist/assets/index-DVxGch9w.js +0 -790
- package/dist/assets/stateDiagram-v2-4FDKWEC3-dqZSxN9f.js +0 -1
- package/src/features/workflow/components/layouts/workflow-page-layout.tsx +0 -69
- package/src/features/workflow/data/templates/template-owner.ts +0 -7
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
ApiWorkflow,
|
|
21
21
|
ApiWorkflowVersion,
|
|
22
22
|
ApiWorkflowVersionSummary,
|
|
23
|
+
WorkflowCandidateSource,
|
|
23
24
|
WorkflowVersionMetadata,
|
|
24
25
|
StoredWorkflow,
|
|
25
26
|
WorkflowVersionRecord,
|
|
@@ -97,6 +98,45 @@ const extractAvatarEmoji = (metadata: unknown): string | undefined => {
|
|
|
97
98
|
return undefined;
|
|
98
99
|
};
|
|
99
100
|
|
|
101
|
+
const extractCandidateSource = (
|
|
102
|
+
metadata: unknown,
|
|
103
|
+
): WorkflowCandidateSource | undefined => {
|
|
104
|
+
if (!isRecord(metadata)) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const source =
|
|
109
|
+
typeof metadata.source === "string" ? metadata.source : undefined;
|
|
110
|
+
const candidateId =
|
|
111
|
+
typeof metadata.candidate_id === "string"
|
|
112
|
+
? metadata.candidate_id
|
|
113
|
+
: undefined;
|
|
114
|
+
const candidateHandle =
|
|
115
|
+
typeof metadata.candidate_handle === "string"
|
|
116
|
+
? metadata.candidate_handle
|
|
117
|
+
: undefined;
|
|
118
|
+
const candidateVersion =
|
|
119
|
+
typeof metadata.candidate_version === "string"
|
|
120
|
+
? metadata.candidate_version
|
|
121
|
+
: undefined;
|
|
122
|
+
const candidateSourceRef =
|
|
123
|
+
typeof metadata.candidate_source_ref === "string"
|
|
124
|
+
? metadata.candidate_source_ref
|
|
125
|
+
: undefined;
|
|
126
|
+
|
|
127
|
+
if (!source && !candidateId && !candidateHandle && !candidateVersion) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
source,
|
|
133
|
+
candidateId,
|
|
134
|
+
candidateHandle,
|
|
135
|
+
candidateVersion,
|
|
136
|
+
candidateSourceRef,
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
|
|
100
140
|
const toAuthor = (id: string | undefined): Workflow["owner"] => {
|
|
101
141
|
if (!id) {
|
|
102
142
|
return { ...DEFAULT_OWNER };
|
|
@@ -176,8 +216,10 @@ const parseWorkflowMetadata = (
|
|
|
176
216
|
): WorkflowVersionMetadata => {
|
|
177
217
|
const configurableSchemas = parseConfigurableSchemas(
|
|
178
218
|
isRecord(metadata) ? metadata.configurable_schema : undefined,
|
|
219
|
+
isRecord(metadata) ? metadata.configurable_schema_order : undefined,
|
|
179
220
|
);
|
|
180
221
|
const avatarEmoji = extractAvatarEmoji(metadata);
|
|
222
|
+
const candidateSource = extractCandidateSource(metadata);
|
|
181
223
|
const resolveTemplateFallback = (): WorkflowVersionMetadata | undefined => {
|
|
182
224
|
if (!metadata || typeof metadata !== "object") {
|
|
183
225
|
return undefined;
|
|
@@ -204,6 +246,7 @@ const parseWorkflowMetadata = (
|
|
|
204
246
|
templateId,
|
|
205
247
|
configurableSchemas,
|
|
206
248
|
avatarEmoji,
|
|
249
|
+
candidateSource,
|
|
207
250
|
};
|
|
208
251
|
};
|
|
209
252
|
|
|
@@ -214,6 +257,7 @@ const parseWorkflowMetadata = (
|
|
|
214
257
|
templateId: undefined,
|
|
215
258
|
configurableSchemas: undefined,
|
|
216
259
|
avatarEmoji: undefined,
|
|
260
|
+
candidateSource,
|
|
217
261
|
};
|
|
218
262
|
}
|
|
219
263
|
|
|
@@ -226,6 +270,7 @@ const parseWorkflowMetadata = (
|
|
|
226
270
|
summary: { ...DEFAULT_SUMMARY },
|
|
227
271
|
configurableSchemas,
|
|
228
272
|
avatarEmoji,
|
|
273
|
+
candidateSource,
|
|
229
274
|
}
|
|
230
275
|
);
|
|
231
276
|
}
|
|
@@ -280,11 +325,13 @@ const parseWorkflowMetadata = (
|
|
|
280
325
|
templateId,
|
|
281
326
|
configurableSchemas,
|
|
282
327
|
avatarEmoji,
|
|
328
|
+
candidateSource,
|
|
283
329
|
};
|
|
284
330
|
};
|
|
285
331
|
|
|
286
332
|
const parseConfigurableSchemas = (
|
|
287
333
|
value: unknown,
|
|
334
|
+
order: unknown,
|
|
288
335
|
): Record<string, RJSFSchema> | undefined => {
|
|
289
336
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
290
337
|
return undefined;
|
|
@@ -299,6 +346,23 @@ const parseConfigurableSchemas = (
|
|
|
299
346
|
return undefined;
|
|
300
347
|
}
|
|
301
348
|
|
|
349
|
+
// The schema object is persisted in a JSONB column that does not preserve key
|
|
350
|
+
// order, so reorder by the authored `configurable_schema_order` array when
|
|
351
|
+
// present. Keys missing from the order list keep their existing position at
|
|
352
|
+
// the end, keeping output stable for legacy versions without the array.
|
|
353
|
+
if (Array.isArray(order) && order.length > 0) {
|
|
354
|
+
const byKey = new Map(entries);
|
|
355
|
+
const orderedKeys = order.filter(
|
|
356
|
+
(key): key is string => typeof key === "string" && byKey.has(key),
|
|
357
|
+
);
|
|
358
|
+
const seen = new Set(orderedKeys);
|
|
359
|
+
const orderedEntries = [
|
|
360
|
+
...orderedKeys.map((key) => [key, byKey.get(key)] as const),
|
|
361
|
+
...entries.filter(([key]) => !seen.has(key)),
|
|
362
|
+
];
|
|
363
|
+
return Object.fromEntries(orderedEntries) as Record<string, RJSFSchema>;
|
|
364
|
+
}
|
|
365
|
+
|
|
302
366
|
return Object.fromEntries(entries) as Record<string, RJSFSchema>;
|
|
303
367
|
};
|
|
304
368
|
|
|
@@ -337,6 +401,7 @@ const toVersionRecord = (
|
|
|
337
401
|
graphToWorkflow: metadata.graphToWorkflow,
|
|
338
402
|
templateId: metadata.templateId,
|
|
339
403
|
avatarEmoji: metadata.avatarEmoji,
|
|
404
|
+
candidateSource: metadata.candidateSource,
|
|
340
405
|
};
|
|
341
406
|
};
|
|
342
407
|
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
fetchWorkflowVersions,
|
|
17
17
|
onboardCandidate,
|
|
18
18
|
request,
|
|
19
|
+
updateCandidateWorkflow,
|
|
19
20
|
upsertWorkflow,
|
|
20
21
|
} from "./workflow-storage-api";
|
|
21
22
|
import {
|
|
@@ -472,6 +473,22 @@ export const onboardCandidateAsWorkflow = async (
|
|
|
472
473
|
return stored;
|
|
473
474
|
};
|
|
474
475
|
|
|
476
|
+
export const updateCandidateWorkflowVersion = async (
|
|
477
|
+
workflowId: string,
|
|
478
|
+
candidateId: string,
|
|
479
|
+
): Promise<StoredWorkflow> => {
|
|
480
|
+
const apiWorkflow = await updateCandidateWorkflow(workflowId, candidateId);
|
|
481
|
+
invalidateWorkflowCache(apiWorkflow.id);
|
|
482
|
+
const stored = await ensureWorkflow(apiWorkflow.id);
|
|
483
|
+
if (!stored) {
|
|
484
|
+
throw new Error("Failed to load updated candidate workflow");
|
|
485
|
+
}
|
|
486
|
+
invalidateWorkflowListCache();
|
|
487
|
+
primeWorkflowCache(stored);
|
|
488
|
+
emitUpdate();
|
|
489
|
+
return stored;
|
|
490
|
+
};
|
|
491
|
+
|
|
475
492
|
export type {
|
|
476
493
|
StoredWorkflow,
|
|
477
494
|
WorkflowVersionRecord,
|
|
@@ -68,6 +68,20 @@ export interface ApiWorkflowVersionSummary {
|
|
|
68
68
|
updated_at: string;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
export interface CandidateUpdateNote {
|
|
72
|
+
version: string;
|
|
73
|
+
summary: string;
|
|
74
|
+
migration?: string | null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface WorkflowCandidateSource {
|
|
78
|
+
source?: string;
|
|
79
|
+
candidateId?: string;
|
|
80
|
+
candidateHandle?: string;
|
|
81
|
+
candidateVersion?: string;
|
|
82
|
+
candidateSourceRef?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
71
85
|
export interface ApiWorkflowPageData {
|
|
72
86
|
workflow: ApiWorkflow;
|
|
73
87
|
versions?: ApiWorkflowVersionSummary[];
|
|
@@ -219,6 +233,7 @@ export interface WorkflowVersionMetadata {
|
|
|
219
233
|
templateId?: string;
|
|
220
234
|
configurableSchemas?: Record<string, RJSFSchema>;
|
|
221
235
|
avatarEmoji?: string;
|
|
236
|
+
candidateSource?: WorkflowCandidateSource;
|
|
222
237
|
}
|
|
223
238
|
|
|
224
239
|
export interface RequestOptions extends RequestInit {
|
|
@@ -241,6 +256,7 @@ export interface WorkflowVersionRecord {
|
|
|
241
256
|
configurableSchemas?: Record<string, RJSFSchema>;
|
|
242
257
|
graphToWorkflow?: Record<string, string>;
|
|
243
258
|
templateId?: string;
|
|
259
|
+
candidateSource?: WorkflowCandidateSource;
|
|
244
260
|
}
|
|
245
261
|
|
|
246
262
|
export interface StoredWorkflow extends Workflow {
|
|
@@ -44,12 +44,28 @@ const baseWorkflowConfigSchema: RJSFSchema = {
|
|
|
44
44
|
},
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
const buildWorkflowConfigUiSchema = (
|
|
48
|
+
configurableSchemas?: Record<string, RJSFSchema>,
|
|
49
|
+
): UiSchema => {
|
|
50
|
+
// Opt any key declared with `"format": "cron"` into the visual cron widget.
|
|
51
|
+
const cronEntries: UiSchema = {};
|
|
52
|
+
for (const [key, schema] of Object.entries(configurableSchemas ?? {})) {
|
|
53
|
+
if (schema?.format === "cron") {
|
|
54
|
+
cronEntries[key] = { "ui:widget": "cron" };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
configurable: {
|
|
60
|
+
// Render configurable fields in the order declared by config.json,
|
|
61
|
+
// with any data-only keys appended afterwards via the "*" wildcard.
|
|
62
|
+
"ui:order": [...Object.keys(configurableSchemas ?? {}), "*"],
|
|
63
|
+
fields: {
|
|
64
|
+
"ui:field": "jsonObject",
|
|
65
|
+
},
|
|
66
|
+
...cronEntries,
|
|
51
67
|
},
|
|
52
|
-
}
|
|
68
|
+
};
|
|
53
69
|
};
|
|
54
70
|
|
|
55
71
|
interface WorkflowConfigSheetProps {
|
|
@@ -91,6 +107,11 @@ export function WorkflowConfigSheet({
|
|
|
91
107
|
};
|
|
92
108
|
}, [formData.configurable, configurableSchemas]);
|
|
93
109
|
|
|
110
|
+
const workflowConfigUiSchema = useMemo<UiSchema>(
|
|
111
|
+
() => buildWorkflowConfigUiSchema(configurableSchemas),
|
|
112
|
+
[configurableSchemas],
|
|
113
|
+
);
|
|
114
|
+
|
|
94
115
|
useEffect(() => {
|
|
95
116
|
if (open) {
|
|
96
117
|
setFormData(toFormData(initialConfig));
|
|
@@ -2,12 +2,14 @@ import { useEffect, useMemo, useState } from "react";
|
|
|
2
2
|
import { useUploadsAllowed } from "@/hooks/use-uploads-allowed";
|
|
3
3
|
import {
|
|
4
4
|
AlertTriangle,
|
|
5
|
+
CheckCircle2,
|
|
5
6
|
Copy,
|
|
6
7
|
ExternalLink,
|
|
7
8
|
LoaderCircle,
|
|
8
9
|
Play,
|
|
9
10
|
RefreshCw,
|
|
10
11
|
UserMinus,
|
|
12
|
+
XCircle,
|
|
11
13
|
} from "lucide-react";
|
|
12
14
|
import { useNavigate } from "react-router-dom";
|
|
13
15
|
import { Controls, ReactFlow, type Node, type NodeProps } from "@xyflow/react";
|
|
@@ -56,6 +58,7 @@ import {
|
|
|
56
58
|
} from "@features/workflow/lib/mermaid-renderer";
|
|
57
59
|
import { resolveWorkflowVersionMermaidSource } from "@features/workflow/lib/workflow-storage-helpers";
|
|
58
60
|
import { WorkflowConfigSheet } from "@features/workflow/pages/workflow/components/workflow-config-sheet";
|
|
61
|
+
import type { WorkflowExecutionStatus } from "@features/workflow/pages/workflow/helpers/types";
|
|
59
62
|
|
|
60
63
|
export interface WorkflowTabContentProps {
|
|
61
64
|
workflowId: string | null;
|
|
@@ -66,6 +69,7 @@ export interface WorkflowTabContentProps {
|
|
|
66
69
|
loadError: string | null;
|
|
67
70
|
isRunPending: boolean;
|
|
68
71
|
isRunning: boolean;
|
|
72
|
+
lastRunStatus?: WorkflowExecutionStatus | null;
|
|
69
73
|
onRunWorkflow: () => Promise<void>;
|
|
70
74
|
onSaveConfig: (nextConfig: WorkflowRunnableConfig | null) => Promise<void>;
|
|
71
75
|
hasCronTriggerNode: boolean;
|
|
@@ -189,6 +193,49 @@ const getErrorMessage = (error: unknown, fallback: string): string => {
|
|
|
189
193
|
return rawMessage;
|
|
190
194
|
};
|
|
191
195
|
|
|
196
|
+
interface RunResultBanner {
|
|
197
|
+
Icon: typeof CheckCircle2;
|
|
198
|
+
className: string;
|
|
199
|
+
message: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const resolveRunResultBanner = (
|
|
203
|
+
status: WorkflowExecutionStatus,
|
|
204
|
+
): RunResultBanner => {
|
|
205
|
+
switch (status) {
|
|
206
|
+
case "success":
|
|
207
|
+
return {
|
|
208
|
+
Icon: CheckCircle2,
|
|
209
|
+
className:
|
|
210
|
+
"border-emerald-500/40 bg-emerald-50 text-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-300",
|
|
211
|
+
message: "Workflow run succeeded. See the full trace on the Trace tab.",
|
|
212
|
+
};
|
|
213
|
+
case "failed":
|
|
214
|
+
return {
|
|
215
|
+
Icon: XCircle,
|
|
216
|
+
className: "border-destructive/40 bg-destructive/5 text-destructive",
|
|
217
|
+
message: "Workflow run failed. Check the Trace tab for details.",
|
|
218
|
+
};
|
|
219
|
+
case "partial":
|
|
220
|
+
return {
|
|
221
|
+
Icon: AlertTriangle,
|
|
222
|
+
className:
|
|
223
|
+
"border-amber-500/40 bg-amber-50 text-amber-700 dark:bg-amber-950/40 dark:text-amber-300",
|
|
224
|
+
message:
|
|
225
|
+
"Workflow run finished with partial results. Check the Trace tab for details.",
|
|
226
|
+
};
|
|
227
|
+
default:
|
|
228
|
+
// "running" reaching here means the stream ended without a final status.
|
|
229
|
+
return {
|
|
230
|
+
Icon: AlertTriangle,
|
|
231
|
+
className:
|
|
232
|
+
"border-amber-500/40 bg-amber-50 text-amber-700 dark:bg-amber-950/40 dark:text-amber-300",
|
|
233
|
+
message:
|
|
234
|
+
"Workflow run ended without a final status. Check the Trace tab for the latest status.",
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
192
239
|
export function WorkflowTabContent({
|
|
193
240
|
workflowId,
|
|
194
241
|
workflowName,
|
|
@@ -197,6 +244,7 @@ export function WorkflowTabContent({
|
|
|
197
244
|
loadError,
|
|
198
245
|
isRunPending,
|
|
199
246
|
isRunning,
|
|
247
|
+
lastRunStatus = null,
|
|
200
248
|
onRunWorkflow,
|
|
201
249
|
onSaveConfig,
|
|
202
250
|
hasCronTriggerNode,
|
|
@@ -390,6 +438,11 @@ export function WorkflowTabContent({
|
|
|
390
438
|
}
|
|
391
439
|
|
|
392
440
|
const canConfigure = Boolean(workflowId);
|
|
441
|
+
const isRunActive = isRunPending || isRunning;
|
|
442
|
+
const runResultBanner =
|
|
443
|
+
!isRunActive && lastRunStatus
|
|
444
|
+
? resolveRunResultBanner(lastRunStatus)
|
|
445
|
+
: null;
|
|
393
446
|
const canRun = Boolean(workflowId && latestVersion);
|
|
394
447
|
const runButtonDisabled = !canRun || isRunPending || isRunning;
|
|
395
448
|
const runButtonLabel = isRunPending || isRunning ? "Running..." : "Run";
|
|
@@ -667,7 +720,7 @@ export function WorkflowTabContent({
|
|
|
667
720
|
</div>
|
|
668
721
|
</div>
|
|
669
722
|
|
|
670
|
-
{
|
|
723
|
+
{isRunActive && (
|
|
671
724
|
<div
|
|
672
725
|
role="status"
|
|
673
726
|
className="rounded-md border border-primary/30 bg-primary/5 px-3 py-2 text-sm text-primary"
|
|
@@ -677,6 +730,16 @@ export function WorkflowTabContent({
|
|
|
677
730
|
</div>
|
|
678
731
|
)}
|
|
679
732
|
|
|
733
|
+
{runResultBanner && (
|
|
734
|
+
<div
|
|
735
|
+
role="status"
|
|
736
|
+
className={`flex items-center gap-2 rounded-md border px-3 py-2 text-sm ${runResultBanner.className}`}
|
|
737
|
+
>
|
|
738
|
+
<runResultBanner.Icon className="h-4 w-4 shrink-0" />
|
|
739
|
+
<span>{runResultBanner.message}</span>
|
|
740
|
+
</div>
|
|
741
|
+
)}
|
|
742
|
+
|
|
680
743
|
{isPublished && shareUrl && (
|
|
681
744
|
<div className="flex items-center justify-between rounded-md border border-border/60 bg-muted/20 px-3 py-2">
|
|
682
745
|
<div className="min-w-0">
|
|
@@ -75,6 +75,10 @@ export function buildWorkflowLayoutProps(
|
|
|
75
75
|
resources: WorkflowResources,
|
|
76
76
|
execution: WorkflowExecutionController,
|
|
77
77
|
): WorkflowLayoutProps {
|
|
78
|
+
const activeExecution = core.execution.executions.find(
|
|
79
|
+
(execution) => execution.id === core.execution.activeExecutionId,
|
|
80
|
+
);
|
|
81
|
+
|
|
78
82
|
const workflowProps: WorkflowTabContentProps = {
|
|
79
83
|
workflowId: core.metadata.currentWorkflowId,
|
|
80
84
|
workflowRouteRef: core.routeWorkflowRef,
|
|
@@ -84,6 +88,7 @@ export function buildWorkflowLayoutProps(
|
|
|
84
88
|
loadError: core.metadata.workflowLoadError,
|
|
85
89
|
isRunPending: execution.isRunPending,
|
|
86
90
|
isRunning: core.execution.isRunning,
|
|
91
|
+
lastRunStatus: activeExecution?.status ?? null,
|
|
87
92
|
onRunWorkflow: execution.handleRunPersistedWorkflow,
|
|
88
93
|
onSaveConfig: resources.saver.handleSaveWorkflowConfig,
|
|
89
94
|
hasCronTriggerNode: hasSchedulableCronTrigger(
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { Workflow } from "@features/workflow/data/workflow-data";
|
|
2
|
+
import type { CandidateBadgeDefinition } from "@features/workflow/data/templates/candidate-badges";
|
|
3
|
+
import type { CandidateUpdateNote } from "@features/workflow/lib/workflow-storage.types";
|
|
4
|
+
|
|
5
|
+
interface SemVer {
|
|
6
|
+
major: number;
|
|
7
|
+
minor: number;
|
|
8
|
+
patch: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CandidateUpdateAvailability {
|
|
12
|
+
candidateId: string;
|
|
13
|
+
candidateName: string;
|
|
14
|
+
currentVersion: string;
|
|
15
|
+
latestVersion: string;
|
|
16
|
+
latestSummary?: string;
|
|
17
|
+
firstMigration?: string;
|
|
18
|
+
crossedUpdates: CandidateUpdateNote[];
|
|
19
|
+
isMajor: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
23
|
+
|
|
24
|
+
export const parseSemVer = (value: string): SemVer | null => {
|
|
25
|
+
const match = SEMVER_PATTERN.exec(value);
|
|
26
|
+
if (!match) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
major: Number.parseInt(match[1] ?? "0", 10),
|
|
32
|
+
minor: Number.parseInt(match[2] ?? "0", 10),
|
|
33
|
+
patch: Number.parseInt(match[3] ?? "0", 10),
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const compareSemVer = (left: string, right: string): number | null => {
|
|
38
|
+
const leftVersion = parseSemVer(left);
|
|
39
|
+
const rightVersion = parseSemVer(right);
|
|
40
|
+
if (!leftVersion || !rightVersion) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const key of ["major", "minor", "patch"] as const) {
|
|
45
|
+
const delta = leftVersion[key] - rightVersion[key];
|
|
46
|
+
if (delta !== 0) {
|
|
47
|
+
return delta > 0 ? 1 : -1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return 0;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const getCandidateUpdateAvailability = (
|
|
55
|
+
workflow: Workflow,
|
|
56
|
+
candidate: CandidateBadgeDefinition | undefined,
|
|
57
|
+
): CandidateUpdateAvailability | undefined => {
|
|
58
|
+
if (!candidate?.version) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const candidateSource = workflow.versions?.at(-1)?.candidateSource;
|
|
63
|
+
if (candidateSource?.source !== "candidate-onboard") {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const currentVersion = candidateSource.candidateVersion;
|
|
68
|
+
if (!currentVersion) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const latestVersion = candidate.version;
|
|
73
|
+
if (compareSemVer(latestVersion, currentVersion) !== 1) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const currentParsed = parseSemVer(currentVersion);
|
|
78
|
+
const latestParsed = parseSemVer(latestVersion);
|
|
79
|
+
if (!currentParsed || !latestParsed) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const crossedUpdates = (candidate.updates ?? []).filter((note) => {
|
|
84
|
+
const newerThanCurrent = compareSemVer(note.version, currentVersion);
|
|
85
|
+
const notNewerThanLatest = compareSemVer(note.version, latestVersion);
|
|
86
|
+
return (
|
|
87
|
+
newerThanCurrent === 1 &&
|
|
88
|
+
notNewerThanLatest !== null &&
|
|
89
|
+
notNewerThanLatest <= 0
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const firstMigration = crossedUpdates.find(
|
|
94
|
+
(note) => typeof note.migration === "string" && note.migration.trim(),
|
|
95
|
+
)?.migration;
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
candidateId: candidate.candidateId,
|
|
99
|
+
candidateName: candidate.name,
|
|
100
|
+
currentVersion,
|
|
101
|
+
latestVersion,
|
|
102
|
+
latestSummary: crossedUpdates[0]?.summary,
|
|
103
|
+
firstMigration,
|
|
104
|
+
crossedUpdates,
|
|
105
|
+
isMajor: latestParsed.major > currentParsed.major,
|
|
106
|
+
};
|
|
107
|
+
};
|
|
@@ -9,6 +9,7 @@ import { getCandidateBadgeDefinition } from "@features/workflow/data/templates/c
|
|
|
9
9
|
import {
|
|
10
10
|
deleteWorkflow,
|
|
11
11
|
onboardCandidateAsWorkflow,
|
|
12
|
+
updateCandidateWorkflowVersion,
|
|
12
13
|
} from "@features/workflow/lib/workflow-storage";
|
|
13
14
|
import {
|
|
14
15
|
type ApiTeam,
|
|
@@ -104,10 +105,9 @@ export const useWorkflowGalleryActions = (
|
|
|
104
105
|
const handleOpenWorkflow = useCallback(
|
|
105
106
|
(workflowId: string, teamSlug?: string) => {
|
|
106
107
|
const slug = getSelectedWorkspaceSlug();
|
|
107
|
-
const path =
|
|
108
|
-
teamSlug
|
|
109
|
-
|
|
110
|
-
: getWorkspaceWorkflowPath(slug, workflowId);
|
|
108
|
+
const path = teamSlug
|
|
109
|
+
? getWorkspaceTeamWorkflowPath(slug, teamSlug, workflowId)
|
|
110
|
+
: getWorkspaceWorkflowPath(slug, workflowId);
|
|
111
111
|
navigate(path);
|
|
112
112
|
},
|
|
113
113
|
[navigate],
|
|
@@ -302,6 +302,30 @@ export const useWorkflowGalleryActions = (
|
|
|
302
302
|
[],
|
|
303
303
|
);
|
|
304
304
|
|
|
305
|
+
const handleUpdateCandidateWorkflow = useCallback(
|
|
306
|
+
async (workflowId: string, candidateId: string) => {
|
|
307
|
+
try {
|
|
308
|
+
const workflow = await updateCandidateWorkflowVersion(
|
|
309
|
+
workflowId,
|
|
310
|
+
candidateId,
|
|
311
|
+
);
|
|
312
|
+
toast({
|
|
313
|
+
title: "Candidate updated",
|
|
314
|
+
description: `"${workflow.name}" is now on the latest candidate version.`,
|
|
315
|
+
});
|
|
316
|
+
} catch (error) {
|
|
317
|
+
toast({
|
|
318
|
+
title: "Failed to update candidate",
|
|
319
|
+
description:
|
|
320
|
+
error instanceof Error ? error.message : "Unknown error occurred",
|
|
321
|
+
variant: "destructive",
|
|
322
|
+
});
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
[],
|
|
327
|
+
);
|
|
328
|
+
|
|
305
329
|
const openCreateTeamDialog = useCallback(() => setIsCreateTeamOpen(true), []);
|
|
306
330
|
const closeCreateTeamDialog = useCallback(
|
|
307
331
|
() => setIsCreateTeamOpen(false),
|
|
@@ -314,6 +338,7 @@ export const useWorkflowGalleryActions = (
|
|
|
314
338
|
handleImportStarterPack,
|
|
315
339
|
handleExportWorkflow,
|
|
316
340
|
handleDeleteWorkflow,
|
|
341
|
+
handleUpdateCandidateWorkflow,
|
|
317
342
|
onboardTarget,
|
|
318
343
|
confirmOnboardTeam,
|
|
319
344
|
cancelOnboardTeam,
|
|
@@ -49,6 +49,8 @@ const toCandidateSpec = (candidate: ApiCandidate): CandidateBadgeSpec => ({
|
|
|
49
49
|
avatar: candidate.avatar ?? undefined,
|
|
50
50
|
notes: candidate.notes,
|
|
51
51
|
mermaid: candidate.mermaid,
|
|
52
|
+
version: candidate.version,
|
|
53
|
+
updates: candidate.updates,
|
|
52
54
|
rawMetadata: candidate.metadata,
|
|
53
55
|
});
|
|
54
56
|
|
|
@@ -75,17 +77,22 @@ export const useWorkflowGalleryState = (): WorkflowGalleryStateSlice => {
|
|
|
75
77
|
useEffect(() => {
|
|
76
78
|
let isMounted = true;
|
|
77
79
|
|
|
78
|
-
const loadCandidates = async () => {
|
|
80
|
+
const loadCandidates = async (existingWorkflows?: StoredWorkflow[]) => {
|
|
79
81
|
try {
|
|
80
82
|
// Load both candidates and existing workflows to check for handle conflicts
|
|
81
|
-
const [candidates,
|
|
83
|
+
const [candidates, workflowsForConflicts] = await Promise.all([
|
|
82
84
|
fetchCandidates(),
|
|
83
|
-
|
|
85
|
+
existingWorkflows
|
|
86
|
+
? Promise.resolve(existingWorkflows)
|
|
87
|
+
: listWorkflows(),
|
|
84
88
|
]);
|
|
85
89
|
if (!isMounted) {
|
|
86
90
|
return;
|
|
87
91
|
}
|
|
88
|
-
setCandidateBadges(
|
|
92
|
+
setCandidateBadges(
|
|
93
|
+
candidates.map(toCandidateSpec),
|
|
94
|
+
workflowsForConflicts,
|
|
95
|
+
);
|
|
89
96
|
setCandidateWorkflows(getCandidateWorkflows());
|
|
90
97
|
} catch (error) {
|
|
91
98
|
if (isMounted) {
|
|
@@ -94,26 +101,8 @@ export const useWorkflowGalleryState = (): WorkflowGalleryStateSlice => {
|
|
|
94
101
|
}
|
|
95
102
|
};
|
|
96
103
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return () => {
|
|
100
|
-
isMounted = false;
|
|
101
|
-
};
|
|
102
|
-
}, []);
|
|
103
|
-
|
|
104
|
-
const refreshTeams = useCallback(async () => {
|
|
105
|
-
try {
|
|
106
|
-
const teamList = await fetchTeams();
|
|
107
|
-
setTeams(teamList);
|
|
108
|
-
} catch {
|
|
109
|
-
// non-fatal — teams list will stay stale
|
|
110
|
-
}
|
|
111
|
-
}, []);
|
|
112
|
-
|
|
113
|
-
useEffect(() => {
|
|
114
|
-
let isMounted = true;
|
|
115
|
-
|
|
116
|
-
const load = async (forceRefresh = false) => {
|
|
104
|
+
const load = async (forceRefresh = false, refreshCandidates = false) => {
|
|
105
|
+
let loadedWorkflows: StoredWorkflow[] | undefined;
|
|
117
106
|
if (isMounted) {
|
|
118
107
|
setIsLoadingWorkflows(true);
|
|
119
108
|
}
|
|
@@ -122,6 +111,7 @@ export const useWorkflowGalleryState = (): WorkflowGalleryStateSlice => {
|
|
|
122
111
|
listWorkflows({ forceRefresh }),
|
|
123
112
|
fetchTeams().catch(() => [] as ApiTeam[]),
|
|
124
113
|
]);
|
|
114
|
+
loadedWorkflows = items;
|
|
125
115
|
if (isMounted) {
|
|
126
116
|
setWorkflows(items);
|
|
127
117
|
setTeams(teamList);
|
|
@@ -143,14 +133,19 @@ export const useWorkflowGalleryState = (): WorkflowGalleryStateSlice => {
|
|
|
143
133
|
setIsLoadingWorkflows(false);
|
|
144
134
|
}
|
|
145
135
|
}
|
|
136
|
+
|
|
137
|
+
if (refreshCandidates && loadedWorkflows) {
|
|
138
|
+
await loadCandidates(loadedWorkflows);
|
|
139
|
+
}
|
|
146
140
|
};
|
|
147
141
|
|
|
142
|
+
void loadCandidates();
|
|
148
143
|
void load();
|
|
149
144
|
|
|
150
145
|
const targetWindow = typeof window !== "undefined" ? window : undefined;
|
|
151
146
|
if (targetWindow) {
|
|
152
147
|
const handler = () => {
|
|
153
|
-
void load(true);
|
|
148
|
+
void load(true, true);
|
|
154
149
|
};
|
|
155
150
|
targetWindow.addEventListener(WORKFLOW_STORAGE_EVENT, handler);
|
|
156
151
|
|
|
@@ -165,6 +160,15 @@ export const useWorkflowGalleryState = (): WorkflowGalleryStateSlice => {
|
|
|
165
160
|
};
|
|
166
161
|
}, []);
|
|
167
162
|
|
|
163
|
+
const refreshTeams = useCallback(async () => {
|
|
164
|
+
try {
|
|
165
|
+
const teamList = await fetchTeams();
|
|
166
|
+
setTeams(teamList);
|
|
167
|
+
} catch {
|
|
168
|
+
// non-fatal — teams list will stay stale
|
|
169
|
+
}
|
|
170
|
+
}, []);
|
|
171
|
+
|
|
168
172
|
const templates = useMemo(
|
|
169
173
|
() => [...GALLERY_TEMPLATE_WORKFLOWS, ...candidateWorkflows],
|
|
170
174
|
[candidateWorkflows],
|