orcheo-studio 0.24.0 → 0.24.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/.bumpversion.cfg +1 -1
- package/dist/assets/{arc-DAv23sxp.js → arc-B5ofENSG.js} +1 -1
- package/dist/assets/{architectureDiagram-VXUJARFQ-D_gUzQPQ.js → architectureDiagram-VXUJARFQ-DxYHdW7c.js} +1 -1
- package/dist/assets/{blockDiagram-VD42YOAC-2_hx47qT.js → blockDiagram-VD42YOAC-DJFNLztP.js} +1 -1
- package/dist/assets/{c4Diagram-YG6GDRKO-dehdDKxT.js → c4Diagram-YG6GDRKO-i42OpjrQ.js} +1 -1
- package/dist/assets/channel-D4idvlBv.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-CtLOuP9F.js → chunk-4BX2VUAB-BP38Jeu1.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-CwinXV4-.js → chunk-55IACEB6-DgOteLeo.js} +1 -1
- package/dist/assets/{chunk-B4BG7PRW-BI7iPFRv.js → chunk-B4BG7PRW-DSFMHhnv.js} +1 -1
- package/dist/assets/{chunk-DI55MBZ5-CNYNrYjW.js → chunk-DI55MBZ5-houO_rDU.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-BFVTHaU-.js → chunk-FMBD7UC4-2uWxVMuK.js} +1 -1
- package/dist/assets/{chunk-QN33PNHL-xWy7sSv1.js → chunk-QN33PNHL-CNE7iDzV.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-Ds_R1QaP.js → chunk-QZHKN3VN-CuJasZjZ.js} +1 -1
- package/dist/assets/{chunk-TZMSLE5B-D8g2OOzb.js → chunk-TZMSLE5B-DdOfh1YS.js} +1 -1
- package/dist/assets/classDiagram-2ON5EDUG-BbsIqyB3.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-BbsIqyB3.js +1 -0
- package/dist/assets/clone-BtsJ7Mgy.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-DnkMvL-f.js → cose-bilkent-S5V4N54A-n5L5u7aX.js} +1 -1
- package/dist/assets/{dagre-6UL2VRFP-dpKYJ1gN.js → dagre-6UL2VRFP-DA0iPKXN.js} +1 -1
- package/dist/assets/{diagram-PSM6KHXK-BnFoR8YQ.js → diagram-PSM6KHXK-3FRZnvw9.js} +1 -1
- package/dist/assets/{diagram-QEK2KX5R-Brg1AGVu.js → diagram-QEK2KX5R-ePGkAGnY.js} +1 -1
- package/dist/assets/{diagram-S2PKOQOG-Dhmom7Q4.js → diagram-S2PKOQOG-FYWv9iIE.js} +1 -1
- package/dist/assets/{erDiagram-Q2GNP2WA-BTez8fDL.js → erDiagram-Q2GNP2WA-2la-V9DA.js} +1 -1
- package/dist/assets/{flowDiagram-NV44I4VS-CVXmtc0D.js → flowDiagram-NV44I4VS-Cv-HCQV2.js} +1 -1
- package/dist/assets/{ganttDiagram-JELNMOA3-CHbw3AtF.js → ganttDiagram-JELNMOA3-DZhWBmY1.js} +1 -1
- package/dist/assets/{gitGraphDiagram-V2S2FVAM-CaPZ1SUE.js → gitGraphDiagram-V2S2FVAM-CBfpN1uw.js} +1 -1
- package/dist/assets/{graph-DjLsaDIL.js → graph-D6aI-bPq.js} +1 -1
- package/dist/assets/index-D8PZ9Puu.js +795 -0
- package/dist/assets/index-Df4jFRhd.css +1 -0
- package/dist/assets/{infoDiagram-HS3SLOUP-D_9DW9VU.js → infoDiagram-HS3SLOUP-Lvd-vDDU.js} +1 -1
- package/dist/assets/{isUndefined-BkijP54r.js → isUndefined-DLcwy6eJ.js} +1 -1
- package/dist/assets/{journeyDiagram-XKPGCS4Q-xxRWc5wn.js → journeyDiagram-XKPGCS4Q-rVvUlw1X.js} +1 -1
- package/dist/assets/{kanban-definition-3W4ZIXB7-BCI2NbpB.js → kanban-definition-3W4ZIXB7-Dg-tIUSI.js} +1 -1
- package/dist/assets/{layout-M840MAqW.js → layout-CYgJ9n1K.js} +1 -1
- package/dist/assets/{linear--_gMRK53.js → linear-CfAw1dgz.js} +1 -1
- package/dist/assets/{min-D06s3WKk.js → min-BBMETs6R.js} +1 -1
- package/dist/assets/{mindmap-definition-VGOIOE7T-B1-ALKFj.js → mindmap-definition-VGOIOE7T-DY__eUNT.js} +1 -1
- package/dist/assets/{pieDiagram-ADFJNKIX-76kjoNcf.js → pieDiagram-ADFJNKIX-Nr7ukKrm.js} +1 -1
- package/dist/assets/{quadrantDiagram-AYHSOK5B-O_d3GTQl.js → quadrantDiagram-AYHSOK5B-Bb54Oaa_.js} +1 -1
- package/dist/assets/{requirementDiagram-UZGBJVZJ-CasSIIPr.js → requirementDiagram-UZGBJVZJ-BtLViRZC.js} +1 -1
- package/dist/assets/{sankeyDiagram-TZEHDZUN-CL_kG7jD.js → sankeyDiagram-TZEHDZUN-COqbhL3I.js} +1 -1
- package/dist/assets/{sequenceDiagram-WL72ISMW-RYnztN0k.js → sequenceDiagram-WL72ISMW-DHKy94Yw.js} +1 -1
- package/dist/assets/{stateDiagram-FKZM4ZOC-NHFLaLiz.js → stateDiagram-FKZM4ZOC-8dJVXo1X.js} +1 -1
- package/dist/assets/stateDiagram-v2-4FDKWEC3-_96AsFOw.js +1 -0
- package/dist/assets/{timeline-definition-IT6M3QCI-BH1cZoua.js → timeline-definition-IT6M3QCI-CyS6rBy_.js} +1 -1
- package/dist/assets/{treemap-GDKQZRPO-QAQDPzuO.js → treemap-GDKQZRPO-CUM9baT2.js} +1 -1
- package/dist/assets/{xychartDiagram-PRI3JC2R-C3M4ZzCP.js → xychartDiagram-PRI3JC2R-BztiRmM1.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/features/account/pages/service-tokens.tsx +13 -60
- package/src/features/auth/pages/login.tsx +8 -1
- package/src/features/workflow/components/dialogs/update-workflow-dialog.tsx +1 -1
- package/src/features/workflow/components/dialogs/upload-workflow-dialog.tsx +14 -2
- 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/workflow-types.ts +5 -0
- package/src/features/workflow/lib/workflow-storage-api.ts +103 -2
- package/src/features/workflow/lib/workflow-storage-helpers.ts +40 -0
- package/src/features/workflow/lib/workflow-storage.ts +58 -31
- package/src/features/workflow/lib/workflow-storage.types.ts +11 -0
- package/src/features/workflow/lib/workflow-upload-errors.ts +81 -0
- package/src/features/workflow/pages/workflow/components/workflow-config-sheet.tsx +26 -5
- package/src/features/workflow/pages/workflow/components/workflow-layout.tsx +4 -1
- package/src/features/workflow/pages/workflow/components/workflow-tab-content.tsx +169 -5
- package/src/features/workflow/pages/workflow/hooks/controller/build-layout-props.ts +8 -0
- package/src/features/workflow/pages/workflow/hooks/controller/use-workflow-lifecycle.ts +6 -0
- package/src/features/workflow/pages/workflow/hooks/controller/use-workflow-resources.ts +2 -0
- package/src/features/workflow/pages/workflow/hooks/execution-runtime-updates.ts +1 -1
- package/src/features/workflow/pages/workflow/hooks/use-workflow-loader.ts +22 -0
- package/src/features/workflow/pages/workflow/hooks/use-workflow-metadata-state.ts +11 -0
- package/src/features/workflow/pages/workflow/hooks/use-workflow-saver.ts +8 -0
- package/src/features/workflow/pages/workflow/hooks/use-workflow-storage-listener.ts +14 -0
- package/src/features/workflow/pages/workflow-gallery/workflow-card.tsx +26 -4
- package/src/lib/api.ts +2 -17
- package/dist/assets/channel-C3cH7p5A.js +0 -1
- package/dist/assets/classDiagram-2ON5EDUG-o6x5AEsV.js +0 -1
- package/dist/assets/classDiagram-v2-WZHVMYZB-o6x5AEsV.js +0 -1
- package/dist/assets/clone-CnJR7M_G.js +0 -1
- package/dist/assets/index-DSthAh3a.css +0 -1
- package/dist/assets/index-DlptXU2u.js +0 -795
- package/dist/assets/stateDiagram-v2-4FDKWEC3-3y5AUrwb.js +0 -1
|
@@ -25,6 +25,10 @@ import {
|
|
|
25
25
|
persistRunnableConfig,
|
|
26
26
|
primeWorkflowCache,
|
|
27
27
|
} from "./workflow-storage-versioning";
|
|
28
|
+
import {
|
|
29
|
+
clearWorkflowUploadError,
|
|
30
|
+
saveWorkflowUploadError,
|
|
31
|
+
} from "./workflow-upload-errors";
|
|
28
32
|
import type {
|
|
29
33
|
ApiWorkflow,
|
|
30
34
|
SaveWorkflowInput,
|
|
@@ -53,6 +57,16 @@ let workflowListCache: WorkflowListCacheEntry | undefined;
|
|
|
53
57
|
let workflowListInflight: Promise<StoredWorkflow[]> | undefined;
|
|
54
58
|
let workflowListRequestId = 0;
|
|
55
59
|
|
|
60
|
+
export class WorkflowUploadFailedError extends Error {
|
|
61
|
+
workflow: StoredWorkflow;
|
|
62
|
+
|
|
63
|
+
constructor(message: string, workflow: StoredWorkflow) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.name = "WorkflowUploadFailedError";
|
|
66
|
+
this.workflow = workflow;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
56
70
|
const resolveActor = (actor?: string): string => {
|
|
57
71
|
const explicitActor = actor?.trim();
|
|
58
72
|
if (explicitActor) {
|
|
@@ -135,24 +149,6 @@ const buildTemplateWorkflowMetadata = ({
|
|
|
135
149
|
summary: { added: 0, removed: 0, modified: 0 },
|
|
136
150
|
});
|
|
137
151
|
|
|
138
|
-
const archiveWorkflowAfterFailedIngest = async (
|
|
139
|
-
workflowId: string,
|
|
140
|
-
actor: string,
|
|
141
|
-
): Promise<void> => {
|
|
142
|
-
try {
|
|
143
|
-
await request(
|
|
144
|
-
`${API_BASE}/${workflowId}?actor=${encodeURIComponent(actor)}`,
|
|
145
|
-
{
|
|
146
|
-
method: "DELETE",
|
|
147
|
-
},
|
|
148
|
-
);
|
|
149
|
-
invalidateWorkflowListCache();
|
|
150
|
-
emitUpdate();
|
|
151
|
-
} catch {
|
|
152
|
-
// Preserve the original ingest error so callers see why the upload failed.
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
|
-
|
|
156
152
|
const assertTemplatePluginRequirements = async (
|
|
157
153
|
requiredPlugins: string[] | undefined,
|
|
158
154
|
): Promise<void> => {
|
|
@@ -249,6 +245,8 @@ export const saveWorkflow = async (
|
|
|
249
245
|
if (options?.runnableConfig !== undefined) {
|
|
250
246
|
await persistRunnableConfig(workflowId, actor, options.runnableConfig);
|
|
251
247
|
}
|
|
248
|
+
clearWorkflowUploadError(workflowId);
|
|
249
|
+
invalidateWorkflowCache(workflowId);
|
|
252
250
|
|
|
253
251
|
const stored = await ensureWorkflow(workflowId);
|
|
254
252
|
if (!stored) {
|
|
@@ -397,9 +395,27 @@ export const uploadWorkflowFromFiles = async (
|
|
|
397
395
|
}),
|
|
398
396
|
});
|
|
399
397
|
} catch (error) {
|
|
400
|
-
|
|
401
|
-
|
|
398
|
+
const message =
|
|
399
|
+
error instanceof Error ? error.message : "Upload failed. Please try again.";
|
|
400
|
+
const uploadError = saveWorkflowUploadError(created.id, message);
|
|
401
|
+
const stored = await ensureWorkflow(created.id);
|
|
402
|
+
invalidateWorkflowListCache();
|
|
403
|
+
if (stored) {
|
|
404
|
+
const failedWorkflow = {
|
|
405
|
+
...stored,
|
|
406
|
+
uploadError,
|
|
407
|
+
};
|
|
408
|
+
primeWorkflowCache(failedWorkflow);
|
|
409
|
+
emitUpdate();
|
|
410
|
+
throw new WorkflowUploadFailedError(message, failedWorkflow);
|
|
411
|
+
}
|
|
412
|
+
emitUpdate();
|
|
413
|
+
throw new WorkflowUploadFailedError(message, {
|
|
414
|
+
...toStoredWorkflow(created, []),
|
|
415
|
+
uploadError,
|
|
416
|
+
});
|
|
402
417
|
}
|
|
418
|
+
clearWorkflowUploadError(created.id);
|
|
403
419
|
|
|
404
420
|
const stored = await ensureWorkflow(created.id);
|
|
405
421
|
if (!stored) {
|
|
@@ -420,17 +436,28 @@ export const updateWorkflowFromFiles = async (
|
|
|
420
436
|
): Promise<StoredWorkflow> => {
|
|
421
437
|
const actor = resolveActor(options?.actor);
|
|
422
438
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
439
|
+
try {
|
|
440
|
+
await request(`${API_BASE}/${workflowId}/versions/ingest`, {
|
|
441
|
+
method: "POST",
|
|
442
|
+
body: JSON.stringify({
|
|
443
|
+
script,
|
|
444
|
+
entrypoint: null,
|
|
445
|
+
runnable_config: config ?? null,
|
|
446
|
+
metadata: { source: "studio-update" },
|
|
447
|
+
notes: null,
|
|
448
|
+
created_by: actor,
|
|
449
|
+
}),
|
|
450
|
+
});
|
|
451
|
+
} catch (error) {
|
|
452
|
+
const message =
|
|
453
|
+
error instanceof Error ? error.message : "Update failed. Please try again.";
|
|
454
|
+
saveWorkflowUploadError(workflowId, message);
|
|
455
|
+
invalidateWorkflowListCache();
|
|
456
|
+
invalidateWorkflowCache(workflowId);
|
|
457
|
+
emitUpdate();
|
|
458
|
+
throw error;
|
|
459
|
+
}
|
|
460
|
+
clearWorkflowUploadError(workflowId);
|
|
434
461
|
|
|
435
462
|
const stored = await ensureWorkflow(workflowId);
|
|
436
463
|
if (!stored) {
|
|
@@ -17,6 +17,7 @@ export interface ApiWorkflow {
|
|
|
17
17
|
id: string;
|
|
18
18
|
handle?: string | null;
|
|
19
19
|
team_id?: string | null;
|
|
20
|
+
team_slug?: string | null;
|
|
20
21
|
name: string;
|
|
21
22
|
slug: string;
|
|
22
23
|
description: string | null;
|
|
@@ -31,10 +32,16 @@ export interface ApiWorkflow {
|
|
|
31
32
|
updated_at: string;
|
|
32
33
|
share_url?: string | null;
|
|
33
34
|
chatkit?: WorkflowChatKitConfig | null;
|
|
35
|
+
upload_error?: ApiWorkflowUploadError | null;
|
|
34
36
|
latest_version?: ApiWorkflowVersion | null;
|
|
35
37
|
is_scheduled?: boolean;
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
export interface ApiWorkflowUploadError {
|
|
41
|
+
message: string;
|
|
42
|
+
occurred_at: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
38
45
|
export interface ChatKitStartScreenPrompt {
|
|
39
46
|
label: string;
|
|
40
47
|
prompt: string;
|
|
@@ -262,6 +269,10 @@ export interface WorkflowVersionRecord {
|
|
|
262
269
|
export interface StoredWorkflow extends Workflow {
|
|
263
270
|
versions: WorkflowVersionRecord[];
|
|
264
271
|
teamId?: string | null;
|
|
272
|
+
uploadError?: {
|
|
273
|
+
message: string;
|
|
274
|
+
occurredAt: string;
|
|
275
|
+
};
|
|
265
276
|
draftAccess?: "personal" | "authenticated" | "workspace";
|
|
266
277
|
isArchived?: boolean;
|
|
267
278
|
isPublic?: boolean;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { StoredWorkflow } from "./workflow-storage.types";
|
|
2
|
+
|
|
3
|
+
export interface WorkflowUploadError {
|
|
4
|
+
message: string;
|
|
5
|
+
occurredAt: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const STORAGE_PREFIX = "orcheo.workflowUploadError.";
|
|
9
|
+
|
|
10
|
+
const storageKey = (workflowId: string): string => `${STORAGE_PREFIX}${workflowId}`;
|
|
11
|
+
|
|
12
|
+
const canUseSessionStorage = (): boolean =>
|
|
13
|
+
typeof window !== "undefined" && Boolean(window.sessionStorage);
|
|
14
|
+
|
|
15
|
+
export const saveWorkflowUploadError = (
|
|
16
|
+
workflowId: string,
|
|
17
|
+
message: string,
|
|
18
|
+
): WorkflowUploadError => {
|
|
19
|
+
const error = {
|
|
20
|
+
message,
|
|
21
|
+
occurredAt: new Date().toISOString(),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
if (!canUseSessionStorage()) {
|
|
25
|
+
return error;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
window.sessionStorage.setItem(storageKey(workflowId), JSON.stringify(error));
|
|
30
|
+
} catch {
|
|
31
|
+
// Upload failure rendering should not depend on storage availability.
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return error;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const readWorkflowUploadError = (
|
|
38
|
+
workflowId: string,
|
|
39
|
+
): WorkflowUploadError | undefined => {
|
|
40
|
+
if (!canUseSessionStorage()) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const raw = window.sessionStorage.getItem(storageKey(workflowId));
|
|
46
|
+
if (!raw) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
const parsed = JSON.parse(raw) as Partial<WorkflowUploadError>;
|
|
50
|
+
if (
|
|
51
|
+
typeof parsed.message === "string" &&
|
|
52
|
+
parsed.message.trim().length > 0 &&
|
|
53
|
+
typeof parsed.occurredAt === "string"
|
|
54
|
+
) {
|
|
55
|
+
return {
|
|
56
|
+
message: parsed.message,
|
|
57
|
+
occurredAt: parsed.occurredAt,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return undefined;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const clearWorkflowUploadError = (workflowId: string): void => {
|
|
68
|
+
if (!canUseSessionStorage()) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
window.sessionStorage.removeItem(storageKey(workflowId));
|
|
74
|
+
} catch {
|
|
75
|
+
// Ignore storage errors; the next successful version is authoritative.
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const getWorkflowUploadError = (
|
|
80
|
+
workflow: Pick<StoredWorkflow, "versions" | "uploadError">,
|
|
81
|
+
): WorkflowUploadError | undefined => workflow.uploadError;
|
|
@@ -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));
|
|
@@ -88,7 +88,10 @@ export function WorkflowLayout({
|
|
|
88
88
|
forceMount
|
|
89
89
|
className="m-0 flex min-h-0 flex-1 flex-col overflow-hidden p-0 data-[state=inactive]:hidden"
|
|
90
90
|
>
|
|
91
|
-
<WorkflowTabContent
|
|
91
|
+
<WorkflowTabContent
|
|
92
|
+
{...workflowProps}
|
|
93
|
+
isActive={tabsProps.activeTab === "workflow"}
|
|
94
|
+
/>
|
|
92
95
|
</TabsContent>
|
|
93
96
|
|
|
94
97
|
<TabsContent
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { useEffect, useMemo, useState } from "react";
|
|
1
|
+
import { useEffect, useMemo, useRef, 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,16 +58,27 @@ 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";
|
|
62
|
+
|
|
63
|
+
const RUN_RESULT_BANNER_TIMEOUT_MS = 6000;
|
|
59
64
|
|
|
60
65
|
export interface WorkflowTabContentProps {
|
|
61
66
|
workflowId: string | null;
|
|
62
67
|
workflowRouteRef?: string | null;
|
|
63
68
|
workflowName: string;
|
|
69
|
+
workflowHandle?: string | null;
|
|
70
|
+
workflowTeamSlug?: string | null;
|
|
64
71
|
versions: WorkflowVersionRecord[];
|
|
72
|
+
uploadError?: {
|
|
73
|
+
message: string;
|
|
74
|
+
occurredAt: string;
|
|
75
|
+
} | null;
|
|
65
76
|
isLoading: boolean;
|
|
66
77
|
loadError: string | null;
|
|
67
78
|
isRunPending: boolean;
|
|
68
79
|
isRunning: boolean;
|
|
80
|
+
isActive?: boolean;
|
|
81
|
+
lastRunStatus?: WorkflowExecutionStatus | null;
|
|
69
82
|
onRunWorkflow: () => Promise<void>;
|
|
70
83
|
onSaveConfig: (nextConfig: WorkflowRunnableConfig | null) => Promise<void>;
|
|
71
84
|
hasCronTriggerNode: boolean;
|
|
@@ -189,14 +202,85 @@ const getErrorMessage = (error: unknown, fallback: string): string => {
|
|
|
189
202
|
return rawMessage;
|
|
190
203
|
};
|
|
191
204
|
|
|
205
|
+
const formatWorkflowHandle = (
|
|
206
|
+
handle?: string | null,
|
|
207
|
+
teamSlug?: string | null,
|
|
208
|
+
): string | null => {
|
|
209
|
+
const trimmedHandle = handle?.trim().replace(/^@+/, "");
|
|
210
|
+
if (!trimmedHandle) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (trimmedHandle.includes("/")) {
|
|
215
|
+
return `@${trimmedHandle}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const trimmedTeamSlug = teamSlug
|
|
219
|
+
?.trim()
|
|
220
|
+
.replace(/^@+/, "")
|
|
221
|
+
.replace(/\/+$/, "");
|
|
222
|
+
|
|
223
|
+
return trimmedTeamSlug
|
|
224
|
+
? `@${trimmedTeamSlug}/${trimmedHandle}`
|
|
225
|
+
: `@${trimmedHandle}`;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
interface RunResultBanner {
|
|
229
|
+
Icon: typeof CheckCircle2;
|
|
230
|
+
className: string;
|
|
231
|
+
message: string;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const resolveRunResultBanner = (
|
|
235
|
+
status: WorkflowExecutionStatus,
|
|
236
|
+
): RunResultBanner => {
|
|
237
|
+
switch (status) {
|
|
238
|
+
case "success":
|
|
239
|
+
return {
|
|
240
|
+
Icon: CheckCircle2,
|
|
241
|
+
className:
|
|
242
|
+
"border-emerald-500/40 bg-emerald-50 text-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-300",
|
|
243
|
+
message: "Workflow run succeeded. See the full trace on the Trace tab.",
|
|
244
|
+
};
|
|
245
|
+
case "failed":
|
|
246
|
+
return {
|
|
247
|
+
Icon: XCircle,
|
|
248
|
+
className: "border-destructive/40 bg-destructive/5 text-destructive",
|
|
249
|
+
message: "Workflow run failed. Check the Trace tab for details.",
|
|
250
|
+
};
|
|
251
|
+
case "partial":
|
|
252
|
+
return {
|
|
253
|
+
Icon: AlertTriangle,
|
|
254
|
+
className:
|
|
255
|
+
"border-amber-500/40 bg-amber-50 text-amber-700 dark:bg-amber-950/40 dark:text-amber-300",
|
|
256
|
+
message:
|
|
257
|
+
"Workflow run finished with partial results. Check the Trace tab for details.",
|
|
258
|
+
};
|
|
259
|
+
default:
|
|
260
|
+
// "running" reaching here means the stream ended without a final status.
|
|
261
|
+
return {
|
|
262
|
+
Icon: AlertTriangle,
|
|
263
|
+
className:
|
|
264
|
+
"border-amber-500/40 bg-amber-50 text-amber-700 dark:bg-amber-950/40 dark:text-amber-300",
|
|
265
|
+
message:
|
|
266
|
+
"Workflow run ended without a final status. Check the Trace tab for the latest status.",
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
192
271
|
export function WorkflowTabContent({
|
|
193
272
|
workflowId,
|
|
194
273
|
workflowName,
|
|
274
|
+
workflowHandle,
|
|
275
|
+
workflowTeamSlug,
|
|
195
276
|
versions,
|
|
277
|
+
uploadError,
|
|
196
278
|
isLoading,
|
|
197
279
|
loadError,
|
|
198
280
|
isRunPending,
|
|
199
281
|
isRunning,
|
|
282
|
+
isActive = true,
|
|
283
|
+
lastRunStatus = null,
|
|
200
284
|
onRunWorkflow,
|
|
201
285
|
onSaveConfig,
|
|
202
286
|
hasCronTriggerNode,
|
|
@@ -208,6 +292,8 @@ export function WorkflowTabContent({
|
|
|
208
292
|
const navigate = useNavigate();
|
|
209
293
|
const uploadsAllowed = useUploadsAllowed();
|
|
210
294
|
const latestVersion = versions.at(-1);
|
|
295
|
+
const workflowSubtitleLabel =
|
|
296
|
+
formatWorkflowHandle(workflowHandle, workflowTeamSlug) ?? workflowName;
|
|
211
297
|
const [isConfigOpen, setIsConfigOpen] = useState(false);
|
|
212
298
|
const [isPublished, setIsPublished] = useState(initialIsPublished);
|
|
213
299
|
const [requireLogin, setRequireLogin] = useState(initialRequireLogin);
|
|
@@ -223,6 +309,10 @@ export function WorkflowTabContent({
|
|
|
223
309
|
const [isUpdateDialogOpen, setIsUpdateDialogOpen] = useState(false);
|
|
224
310
|
const [isMissingCredentialsDialogOpen, setIsMissingCredentialsDialogOpen] =
|
|
225
311
|
useState(false);
|
|
312
|
+
const [visibleRunResultStatus, setVisibleRunResultStatus] =
|
|
313
|
+
useState<WorkflowExecutionStatus | null>(null);
|
|
314
|
+
const isRunActive = isRunPending || isRunning;
|
|
315
|
+
const wasRunActiveRef = useRef(isRunActive);
|
|
226
316
|
const hasMissingCredentials = missingCredentials.length > 0;
|
|
227
317
|
const canDeleteWorkflow = Boolean(workflowId);
|
|
228
318
|
|
|
@@ -348,6 +438,26 @@ export function WorkflowTabContent({
|
|
|
348
438
|
};
|
|
349
439
|
}, [mermaidCacheKey, mermaidRenderId, mermaidSource]);
|
|
350
440
|
|
|
441
|
+
useEffect(() => {
|
|
442
|
+
const didFinishRun =
|
|
443
|
+
wasRunActiveRef.current && !isRunActive && Boolean(lastRunStatus);
|
|
444
|
+
wasRunActiveRef.current = isRunActive;
|
|
445
|
+
|
|
446
|
+
if (!isActive || isRunActive || !didFinishRun || !lastRunStatus) {
|
|
447
|
+
setVisibleRunResultStatus(null);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
setVisibleRunResultStatus(lastRunStatus);
|
|
452
|
+
const timeoutId = window.setTimeout(() => {
|
|
453
|
+
setVisibleRunResultStatus(null);
|
|
454
|
+
}, RUN_RESULT_BANNER_TIMEOUT_MS);
|
|
455
|
+
|
|
456
|
+
return () => {
|
|
457
|
+
window.clearTimeout(timeoutId);
|
|
458
|
+
};
|
|
459
|
+
}, [isActive, isRunActive, lastRunStatus]);
|
|
460
|
+
|
|
351
461
|
const diagramNodes = useMemo(() => {
|
|
352
462
|
if (!diagramSvg) {
|
|
353
463
|
return [] as Node[];
|
|
@@ -390,6 +500,12 @@ export function WorkflowTabContent({
|
|
|
390
500
|
}
|
|
391
501
|
|
|
392
502
|
const canConfigure = Boolean(workflowId);
|
|
503
|
+
const hasUploadError = Boolean(uploadError);
|
|
504
|
+
const uploadBlocksWorkflow = hasUploadError && !latestVersion;
|
|
505
|
+
const runResultBanner =
|
|
506
|
+
!isRunActive && visibleRunResultStatus
|
|
507
|
+
? resolveRunResultBanner(visibleRunResultStatus)
|
|
508
|
+
: null;
|
|
393
509
|
const canRun = Boolean(workflowId && latestVersion);
|
|
394
510
|
const runButtonDisabled = !canRun || isRunPending || isRunning;
|
|
395
511
|
const runButtonLabel = isRunPending || isRunning ? "Running..." : "Run";
|
|
@@ -427,6 +543,16 @@ export function WorkflowTabContent({
|
|
|
427
543
|
return;
|
|
428
544
|
}
|
|
429
545
|
|
|
546
|
+
if (uploadBlocksWorkflow) {
|
|
547
|
+
setIsPublished(false);
|
|
548
|
+
toast({
|
|
549
|
+
title: "Fix upload error first",
|
|
550
|
+
description: "Fix the error and upload again before publishing.",
|
|
551
|
+
variant: "destructive",
|
|
552
|
+
});
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
430
556
|
if (nextValue) {
|
|
431
557
|
setIsPublishDialogOpen(true);
|
|
432
558
|
return;
|
|
@@ -500,6 +626,16 @@ export function WorkflowTabContent({
|
|
|
500
626
|
return;
|
|
501
627
|
}
|
|
502
628
|
|
|
629
|
+
if (uploadBlocksWorkflow) {
|
|
630
|
+
setIsScheduled(false);
|
|
631
|
+
toast({
|
|
632
|
+
title: "Fix upload error first",
|
|
633
|
+
description: "Fix the error and upload again before scheduling.",
|
|
634
|
+
variant: "destructive",
|
|
635
|
+
});
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
503
639
|
setIsSchedulePending(true);
|
|
504
640
|
try {
|
|
505
641
|
if (nextValue) {
|
|
@@ -577,7 +713,7 @@ export function WorkflowTabContent({
|
|
|
577
713
|
{workflowName || "Workflow"}
|
|
578
714
|
</h2>
|
|
579
715
|
<p className="text-sm text-muted-foreground">
|
|
580
|
-
{
|
|
716
|
+
{workflowSubtitleLabel}
|
|
581
717
|
{latestVersion ? ` · ${latestVersion.version}` : ""}
|
|
582
718
|
</p>
|
|
583
719
|
</div>
|
|
@@ -595,7 +731,7 @@ export function WorkflowTabContent({
|
|
|
595
731
|
onCheckedChange={(checked) =>
|
|
596
732
|
void handlePublishToggle(checked)
|
|
597
733
|
}
|
|
598
|
-
disabled={isPublishPending}
|
|
734
|
+
disabled={isPublishPending || uploadBlocksWorkflow}
|
|
599
735
|
/>
|
|
600
736
|
</div>
|
|
601
737
|
</TooltipTrigger>
|
|
@@ -615,7 +751,9 @@ export function WorkflowTabContent({
|
|
|
615
751
|
onCheckedChange={(checked) =>
|
|
616
752
|
void handleScheduleToggle(checked)
|
|
617
753
|
}
|
|
618
|
-
disabled={
|
|
754
|
+
disabled={
|
|
755
|
+
isSchedulePending || !canToggleSchedule || uploadBlocksWorkflow
|
|
756
|
+
}
|
|
619
757
|
/>
|
|
620
758
|
</div>
|
|
621
759
|
{workflowId && uploadsAllowed === true ? (
|
|
@@ -667,7 +805,7 @@ export function WorkflowTabContent({
|
|
|
667
805
|
</div>
|
|
668
806
|
</div>
|
|
669
807
|
|
|
670
|
-
{
|
|
808
|
+
{isRunActive && (
|
|
671
809
|
<div
|
|
672
810
|
role="status"
|
|
673
811
|
className="rounded-md border border-primary/30 bg-primary/5 px-3 py-2 text-sm text-primary"
|
|
@@ -677,6 +815,32 @@ export function WorkflowTabContent({
|
|
|
677
815
|
</div>
|
|
678
816
|
)}
|
|
679
817
|
|
|
818
|
+
{runResultBanner && (
|
|
819
|
+
<div
|
|
820
|
+
role="status"
|
|
821
|
+
className={`flex items-center gap-2 rounded-md border px-3 py-2 text-sm ${runResultBanner.className}`}
|
|
822
|
+
>
|
|
823
|
+
<runResultBanner.Icon className="h-4 w-4 shrink-0" />
|
|
824
|
+
<span>{runResultBanner.message}</span>
|
|
825
|
+
</div>
|
|
826
|
+
)}
|
|
827
|
+
|
|
828
|
+
{uploadError && (
|
|
829
|
+
<Alert variant="destructive">
|
|
830
|
+
<XCircle className="h-4 w-4" />
|
|
831
|
+
<AlertTitle>Workflow upload failed</AlertTitle>
|
|
832
|
+
<AlertDescription>
|
|
833
|
+
<div className="space-y-2">
|
|
834
|
+
<p>{uploadError.message}</p>
|
|
835
|
+
<p>
|
|
836
|
+
Fix the error in the workflow script or config, then upload
|
|
837
|
+
again.
|
|
838
|
+
</p>
|
|
839
|
+
</div>
|
|
840
|
+
</AlertDescription>
|
|
841
|
+
</Alert>
|
|
842
|
+
)}
|
|
843
|
+
|
|
680
844
|
{isPublished && shareUrl && (
|
|
681
845
|
<div className="flex items-center justify-between rounded-md border border-border/60 bg-muted/20 px-3 py-2">
|
|
682
846
|
<div className="min-w-0">
|
|
@@ -75,15 +75,23 @@ 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,
|
|
81
85
|
workflowName: core.metadata.workflowName,
|
|
86
|
+
workflowHandle: core.metadata.workflowHandle,
|
|
87
|
+
workflowTeamSlug: core.metadata.workflowTeamSlug,
|
|
82
88
|
versions: core.metadata.workflowVersions ?? [],
|
|
89
|
+
uploadError: core.metadata.workflowUploadError,
|
|
83
90
|
isLoading: core.metadata.isWorkflowLoading,
|
|
84
91
|
loadError: core.metadata.workflowLoadError,
|
|
85
92
|
isRunPending: execution.isRunPending,
|
|
86
93
|
isRunning: core.execution.isRunning,
|
|
94
|
+
lastRunStatus: activeExecution?.status ?? null,
|
|
87
95
|
onRunWorkflow: execution.handleRunPersistedWorkflow,
|
|
88
96
|
onSaveConfig: resources.saver.handleSaveWorkflowConfig,
|
|
89
97
|
hasCronTriggerNode: hasSchedulableCronTrigger(
|
|
@@ -13,8 +13,11 @@ export function useWorkflowLifecycle(
|
|
|
13
13
|
setCurrentWorkflowId: core.metadata.setCurrentWorkflowId,
|
|
14
14
|
setWorkflowName: core.metadata.setWorkflowName,
|
|
15
15
|
setWorkflowDescription: core.metadata.setWorkflowDescription,
|
|
16
|
+
setWorkflowHandle: core.metadata.setWorkflowHandle,
|
|
17
|
+
setWorkflowTeamSlug: core.metadata.setWorkflowTeamSlug,
|
|
16
18
|
setWorkflowTags: core.metadata.setWorkflowTags,
|
|
17
19
|
setWorkflowVersions: core.metadata.setWorkflowVersions,
|
|
20
|
+
setWorkflowUploadError: core.metadata.setWorkflowUploadError,
|
|
18
21
|
setChatkitStartScreenPrompts: core.metadata.setChatkitStartScreenPrompts,
|
|
19
22
|
setChatkitSupportedModels: core.metadata.setChatkitSupportedModels,
|
|
20
23
|
setIsWorkflowPublic: core.metadata.setIsWorkflowPublic,
|
|
@@ -30,7 +33,10 @@ export function useWorkflowLifecycle(
|
|
|
30
33
|
currentWorkflowId: core.metadata.currentWorkflowId,
|
|
31
34
|
setWorkflowName: core.metadata.setWorkflowName,
|
|
32
35
|
setWorkflowDescription: core.metadata.setWorkflowDescription,
|
|
36
|
+
setWorkflowHandle: core.metadata.setWorkflowHandle,
|
|
37
|
+
setWorkflowTeamSlug: core.metadata.setWorkflowTeamSlug,
|
|
33
38
|
setWorkflowVersions: core.metadata.setWorkflowVersions,
|
|
39
|
+
setWorkflowUploadError: core.metadata.setWorkflowUploadError,
|
|
34
40
|
setWorkflowTags: core.metadata.setWorkflowTags,
|
|
35
41
|
setChatkitStartScreenPrompts: core.metadata.setChatkitStartScreenPrompts,
|
|
36
42
|
setChatkitSupportedModels: core.metadata.setChatkitSupportedModels,
|
|
@@ -40,6 +40,8 @@ export function useWorkflowResources(
|
|
|
40
40
|
const saver = useWorkflowSaver({
|
|
41
41
|
setWorkflowName: core.metadata.setWorkflowName,
|
|
42
42
|
setWorkflowDescription: core.metadata.setWorkflowDescription,
|
|
43
|
+
setWorkflowHandle: core.metadata.setWorkflowHandle,
|
|
44
|
+
setWorkflowTeamSlug: core.metadata.setWorkflowTeamSlug,
|
|
43
45
|
setWorkflowVersions: core.metadata.setWorkflowVersions,
|
|
44
46
|
setWorkflowTags: core.metadata.setWorkflowTags,
|
|
45
47
|
workflowName: core.metadata.workflowName,
|