orcheo-studio 0.24.3 → 0.24.5
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-BnLNnEym.js → arc-C-7b0zH4.js} +1 -1
- package/dist/assets/{architectureDiagram-VXUJARFQ-B8dIgVkK.js → architectureDiagram-VXUJARFQ-Ciy8lV6J.js} +1 -1
- package/dist/assets/{blockDiagram-VD42YOAC-BhDb_8Vg.js → blockDiagram-VD42YOAC-dC88adQ_.js} +1 -1
- package/dist/assets/{c4Diagram-YG6GDRKO-DxHEo9Fv.js → c4Diagram-YG6GDRKO-BiHVvXEu.js} +1 -1
- package/dist/assets/channel-pJdT5i-M.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-DMBtc1td.js → chunk-4BX2VUAB-C6V7Huup.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-DMMV3uf8.js → chunk-55IACEB6-DRmZnlbz.js} +1 -1
- package/dist/assets/{chunk-B4BG7PRW-BVmjJQGD.js → chunk-B4BG7PRW-DwV5xRJr.js} +1 -1
- package/dist/assets/{chunk-DI55MBZ5-3C02UFww.js → chunk-DI55MBZ5-BHsUZ0IJ.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-D-PvzEp6.js → chunk-FMBD7UC4-6Q-Lz0AM.js} +1 -1
- package/dist/assets/{chunk-QN33PNHL-BNp075o8.js → chunk-QN33PNHL-BFy_ml8O.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-ChdFCxgk.js → chunk-QZHKN3VN-C1fN91FN.js} +1 -1
- package/dist/assets/{chunk-TZMSLE5B-DKr2kcEO.js → chunk-TZMSLE5B-FLRSQ_Fi.js} +1 -1
- package/dist/assets/classDiagram-2ON5EDUG-CNT-EGNU.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-CNT-EGNU.js +1 -0
- package/dist/assets/clone-BCi0j082.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-T8_HzNYG.js → cose-bilkent-S5V4N54A-BSWP1X1D.js} +1 -1
- package/dist/assets/{dagre-6UL2VRFP-Chzqb9xS.js → dagre-6UL2VRFP-CYoZFlcz.js} +1 -1
- package/dist/assets/{diagram-PSM6KHXK-CLiXfMfi.js → diagram-PSM6KHXK-B8RGX0YL.js} +1 -1
- package/dist/assets/{diagram-QEK2KX5R-DYE_yT-r.js → diagram-QEK2KX5R-ChWXJ8p_.js} +1 -1
- package/dist/assets/{diagram-S2PKOQOG-B4LSPAmZ.js → diagram-S2PKOQOG-Dwic0Fjn.js} +1 -1
- package/dist/assets/{erDiagram-Q2GNP2WA-D9fO0sd2.js → erDiagram-Q2GNP2WA-0NSDB0DX.js} +1 -1
- package/dist/assets/{flowDiagram-NV44I4VS-BU7f4j4T.js → flowDiagram-NV44I4VS-DEoGWs9L.js} +1 -1
- package/dist/assets/{ganttDiagram-JELNMOA3-BSvXqVC4.js → ganttDiagram-JELNMOA3-DqzV4S6l.js} +1 -1
- package/dist/assets/{gitGraphDiagram-V2S2FVAM-B6qztLiE.js → gitGraphDiagram-V2S2FVAM-B-EIQM_H.js} +1 -1
- package/dist/assets/{graph-CxstBGnL.js → graph-Ha1DTJNG.js} +1 -1
- package/dist/assets/{index-Cnz1aoh0.js → index-DTxvVcXp.js} +108 -108
- package/dist/assets/{infoDiagram-HS3SLOUP-BN6_fsOH.js → infoDiagram-HS3SLOUP-C4_vpEkn.js} +1 -1
- package/dist/assets/{isUndefined-DxBJKB1b.js → isUndefined-DkuoqItu.js} +1 -1
- package/dist/assets/{journeyDiagram-XKPGCS4Q-CnjRjeOv.js → journeyDiagram-XKPGCS4Q-BxY4e6OZ.js} +1 -1
- package/dist/assets/{kanban-definition-3W4ZIXB7-qaU6OHR6.js → kanban-definition-3W4ZIXB7-BTX3_Q7O.js} +1 -1
- package/dist/assets/{layout-BULznkFT.js → layout-Dt1W0fl0.js} +1 -1
- package/dist/assets/{linear-ogckhhec.js → linear-CjywSMUX.js} +1 -1
- package/dist/assets/{min-DljR10o-.js → min-Dblq6bty.js} +1 -1
- package/dist/assets/{mindmap-definition-VGOIOE7T-DaEYqWVU.js → mindmap-definition-VGOIOE7T-C5P7bAFm.js} +1 -1
- package/dist/assets/{pieDiagram-ADFJNKIX-C_xhGVfM.js → pieDiagram-ADFJNKIX-BSMylwok.js} +1 -1
- package/dist/assets/{quadrantDiagram-AYHSOK5B-D_TV5asG.js → quadrantDiagram-AYHSOK5B-C74B7bqa.js} +1 -1
- package/dist/assets/{requirementDiagram-UZGBJVZJ-BuLlPC62.js → requirementDiagram-UZGBJVZJ-CvPzL8Le.js} +1 -1
- package/dist/assets/{sankeyDiagram-TZEHDZUN-1P0__QD_.js → sankeyDiagram-TZEHDZUN-BFHm6g6t.js} +1 -1
- package/dist/assets/{sequenceDiagram-WL72ISMW-TfKpluC6.js → sequenceDiagram-WL72ISMW-e74mJ0xk.js} +1 -1
- package/dist/assets/{stateDiagram-FKZM4ZOC-nQ-BoGjY.js → stateDiagram-FKZM4ZOC-VLQxNVA7.js} +1 -1
- package/dist/assets/stateDiagram-v2-4FDKWEC3-Bbpq2t6B.js +1 -0
- package/dist/assets/{timeline-definition-IT6M3QCI-Bz79EHNk.js → timeline-definition-IT6M3QCI-oveNP_QD.js} +1 -1
- package/dist/assets/{treemap-GDKQZRPO-BiJYOG6x.js → treemap-GDKQZRPO-Cowimd29.js} +1 -1
- package/dist/assets/{xychartDiagram-PRI3JC2R-_EJmDCRo.js → xychartDiagram-PRI3JC2R-CP9Fm_aO.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/orcheo.png +0 -0
- package/package.json +1 -1
- package/public/orcheo.png +0 -0
- package/src/App.tsx +48 -44
- package/src/features/account/pages/workspace-members.tsx +58 -62
- package/src/features/auth/pages/login.tsx +3 -1
- package/src/features/chatkit/lib/public-chat-access.ts +13 -0
- package/src/features/chatkit/pages/public-chat.tsx +1 -12
- package/src/features/shared/components/chat-interface.types.ts +1 -5
- package/src/features/shared/components/top-navigation/version-status.tsx +27 -31
- package/src/features/workflow/components/dialogs/add-credential-dialog.tsx +6 -2
- package/src/features/workflow/components/dialogs/credentials-vault.tsx +9 -3
- package/src/features/workflow/components/dialogs/upload-workflow-dialog.tsx +5 -0
- package/src/features/workflow/components/panels/rjsf-cron-widget.tsx +35 -10
- package/src/features/workflow/components/trace/agent-prism/SpanCard/SpanCardConnector.tsx +1 -5
- package/src/features/workflow/components/trace/agent-prism/shared.ts +1 -10
- package/src/features/workflow/lib/workflow-execution-builders.ts +4 -4
- package/src/features/workflow/lib/workflow-execution.types.ts +2 -9
- package/src/features/workflow/lib/workflow-frontmatter.ts +112 -0
- package/src/features/workflow/lib/workflow-storage-api.ts +5 -9
- package/src/features/workflow/lib/workflow-storage-helpers.ts +2 -4
- package/src/features/workflow/lib/workflow-storage.ts +11 -4
- package/src/features/workflow/lib/workflow-storage.types.ts +1 -6
- package/src/features/workflow/lib/workflow-upload-errors.ts +6 -2
- package/src/features/workflow/pages/workflow/components/workflow-tab-content.tsx +3 -1
- package/src/features/workflow/pages/workflow/hooks/controller/use-workflow-execution-controller.ts +4 -1
- package/src/features/workflow/pages/workflow/hooks/use-execution-updates.ts +4 -2
- package/src/features/workflow/pages/workflow-gallery/create-team-dialog.tsx +2 -6
- package/src/lib/api.ts +7 -2
- package/src/lib/config.ts +25 -3
- package/src/lib/workspace-routing.ts +1 -3
- package/dist/assets/channel-0htV4G8n.js +0 -1
- package/dist/assets/classDiagram-2ON5EDUG-Blgk-ihj.js +0 -1
- package/dist/assets/classDiagram-v2-WZHVMYZB-Blgk-ihj.js +0 -1
- package/dist/assets/clone-BgMTf_8w.js +0 -1
- package/dist/assets/stateDiagram-v2-4FDKWEC3-hx1MeqAD.js +0 -1
|
@@ -282,69 +282,65 @@ export default function WorkspaceMembers() {
|
|
|
282
282
|
</div>
|
|
283
283
|
)}
|
|
284
284
|
|
|
285
|
-
{canManage &&
|
|
286
|
-
|
|
287
|
-
<div className="
|
|
288
|
-
<
|
|
289
|
-
<h3 className="text-sm font-medium">Pending Invitations</h3>
|
|
290
|
-
</div>
|
|
291
|
-
<Table>
|
|
292
|
-
<TableHeader>
|
|
293
|
-
<TableRow>
|
|
294
|
-
<TableHead>Email</TableHead>
|
|
295
|
-
<TableHead>Role</TableHead>
|
|
296
|
-
<TableHead>Expires</TableHead>
|
|
297
|
-
<TableHead className="text-right">Actions</TableHead>
|
|
298
|
-
</TableRow>
|
|
299
|
-
</TableHeader>
|
|
300
|
-
<TableBody>
|
|
301
|
-
{invitations
|
|
302
|
-
.filter((i) => i.status === "pending")
|
|
303
|
-
.map((invitation) => {
|
|
304
|
-
const isRevoking =
|
|
305
|
-
revokingInvitationId === invitation.id;
|
|
306
|
-
const isExpired =
|
|
307
|
-
new Date(invitation.expires_at) < new Date();
|
|
308
|
-
return (
|
|
309
|
-
<TableRow
|
|
310
|
-
key={invitation.id}
|
|
311
|
-
className={isExpired ? "opacity-60" : undefined}
|
|
312
|
-
>
|
|
313
|
-
<TableCell className="text-sm">
|
|
314
|
-
{invitation.email}
|
|
315
|
-
</TableCell>
|
|
316
|
-
<TableCell className="capitalize">
|
|
317
|
-
{invitation.role}
|
|
318
|
-
</TableCell>
|
|
319
|
-
<TableCell className="text-sm text-muted-foreground">
|
|
320
|
-
{isExpired ? (
|
|
321
|
-
<span className="text-destructive">Expired</span>
|
|
322
|
-
) : (
|
|
323
|
-
new Date(
|
|
324
|
-
invitation.expires_at,
|
|
325
|
-
).toLocaleDateString()
|
|
326
|
-
)}
|
|
327
|
-
</TableCell>
|
|
328
|
-
<TableCell className="text-right">
|
|
329
|
-
<Button
|
|
330
|
-
variant="ghost"
|
|
331
|
-
size="sm"
|
|
332
|
-
className="text-destructive hover:text-destructive"
|
|
333
|
-
onClick={() =>
|
|
334
|
-
void handleRevokeInvitation(invitation.id)
|
|
335
|
-
}
|
|
336
|
-
disabled={isRevoking}
|
|
337
|
-
>
|
|
338
|
-
{isRevoking ? "Revoking..." : "Revoke"}
|
|
339
|
-
</Button>
|
|
340
|
-
</TableCell>
|
|
341
|
-
</TableRow>
|
|
342
|
-
);
|
|
343
|
-
})}
|
|
344
|
-
</TableBody>
|
|
345
|
-
</Table>
|
|
285
|
+
{canManage && invitations.some((i) => i.status === "pending") && (
|
|
286
|
+
<div className="rounded-lg border">
|
|
287
|
+
<div className="border-b px-4 py-3">
|
|
288
|
+
<h3 className="text-sm font-medium">Pending Invitations</h3>
|
|
346
289
|
</div>
|
|
347
|
-
|
|
290
|
+
<Table>
|
|
291
|
+
<TableHeader>
|
|
292
|
+
<TableRow>
|
|
293
|
+
<TableHead>Email</TableHead>
|
|
294
|
+
<TableHead>Role</TableHead>
|
|
295
|
+
<TableHead>Expires</TableHead>
|
|
296
|
+
<TableHead className="text-right">Actions</TableHead>
|
|
297
|
+
</TableRow>
|
|
298
|
+
</TableHeader>
|
|
299
|
+
<TableBody>
|
|
300
|
+
{invitations
|
|
301
|
+
.filter((i) => i.status === "pending")
|
|
302
|
+
.map((invitation) => {
|
|
303
|
+
const isRevoking = revokingInvitationId === invitation.id;
|
|
304
|
+
const isExpired =
|
|
305
|
+
new Date(invitation.expires_at) < new Date();
|
|
306
|
+
return (
|
|
307
|
+
<TableRow
|
|
308
|
+
key={invitation.id}
|
|
309
|
+
className={isExpired ? "opacity-60" : undefined}
|
|
310
|
+
>
|
|
311
|
+
<TableCell className="text-sm">
|
|
312
|
+
{invitation.email}
|
|
313
|
+
</TableCell>
|
|
314
|
+
<TableCell className="capitalize">
|
|
315
|
+
{invitation.role}
|
|
316
|
+
</TableCell>
|
|
317
|
+
<TableCell className="text-sm text-muted-foreground">
|
|
318
|
+
{isExpired ? (
|
|
319
|
+
<span className="text-destructive">Expired</span>
|
|
320
|
+
) : (
|
|
321
|
+
new Date(invitation.expires_at).toLocaleDateString()
|
|
322
|
+
)}
|
|
323
|
+
</TableCell>
|
|
324
|
+
<TableCell className="text-right">
|
|
325
|
+
<Button
|
|
326
|
+
variant="ghost"
|
|
327
|
+
size="sm"
|
|
328
|
+
className="text-destructive hover:text-destructive"
|
|
329
|
+
onClick={() =>
|
|
330
|
+
void handleRevokeInvitation(invitation.id)
|
|
331
|
+
}
|
|
332
|
+
disabled={isRevoking}
|
|
333
|
+
>
|
|
334
|
+
{isRevoking ? "Revoking..." : "Revoke"}
|
|
335
|
+
</Button>
|
|
336
|
+
</TableCell>
|
|
337
|
+
</TableRow>
|
|
338
|
+
);
|
|
339
|
+
})}
|
|
340
|
+
</TableBody>
|
|
341
|
+
</Table>
|
|
342
|
+
</div>
|
|
343
|
+
)}
|
|
348
344
|
|
|
349
345
|
{isForbidden ? null : isLoading ? (
|
|
350
346
|
<p className="text-sm text-muted-foreground">Loading members...</p>
|
|
@@ -73,7 +73,9 @@ export default function Login() {
|
|
|
73
73
|
setStage("sent");
|
|
74
74
|
} catch (err) {
|
|
75
75
|
setError(
|
|
76
|
-
err instanceof Error
|
|
76
|
+
err instanceof Error
|
|
77
|
+
? err.message
|
|
78
|
+
: "Unable to send the sign-in email.",
|
|
77
79
|
);
|
|
78
80
|
} finally {
|
|
79
81
|
setBusy(false);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PublicChatHttpError } from "@features/chatkit/lib/chatkit-client";
|
|
2
|
+
|
|
3
|
+
export const getPublicChatAccessErrorMessage = (
|
|
4
|
+
error: Pick<PublicChatHttpError, "status" | "code">,
|
|
5
|
+
): string => {
|
|
6
|
+
if (error.code === "chatkit.auth.workspace_mismatch") {
|
|
7
|
+
return "Your account is signed in, but it is not a member of this workflow workspace. Switch workspaces or ask the owner to add you.";
|
|
8
|
+
}
|
|
9
|
+
if (error.status === 403) {
|
|
10
|
+
return "You do not have permission to use this workflow. Ask the owner to confirm your workspace access.";
|
|
11
|
+
}
|
|
12
|
+
return "You do not have access to this workflow yet. Ask the owner to confirm it is still published.";
|
|
13
|
+
};
|
|
@@ -19,6 +19,7 @@ import type { PublicWorkflowMetadata } from "@features/workflow/lib/workflow-sto
|
|
|
19
19
|
import { PublicChatWidget } from "@features/chatkit/components/public-chat-widget";
|
|
20
20
|
import { PublicChatErrorBoundary } from "@features/chatkit/components/public-chat-error-boundary";
|
|
21
21
|
import type { PublicChatHttpError } from "@features/chatkit/lib/chatkit-client";
|
|
22
|
+
import { getPublicChatAccessErrorMessage } from "@features/chatkit/lib/public-chat-access";
|
|
22
23
|
|
|
23
24
|
type ColorScheme = "light" | "dark";
|
|
24
25
|
|
|
@@ -27,18 +28,6 @@ type WorkflowState =
|
|
|
27
28
|
| { status: "error"; message: string }
|
|
28
29
|
| { status: "ready"; workflow: PublicWorkflowMetadata };
|
|
29
30
|
|
|
30
|
-
export const getPublicChatAccessErrorMessage = (
|
|
31
|
-
error: Pick<PublicChatHttpError, "status" | "code">,
|
|
32
|
-
): string => {
|
|
33
|
-
if (error.code === "chatkit.auth.workspace_mismatch") {
|
|
34
|
-
return "Your account is signed in, but it is not a member of this workflow workspace. Switch workspaces or ask the owner to add you.";
|
|
35
|
-
}
|
|
36
|
-
if (error.status === 403) {
|
|
37
|
-
return "You do not have permission to use this workflow. Ask the owner to confirm your workspace access.";
|
|
38
|
-
}
|
|
39
|
-
return "You do not have access to this workflow yet. Ask the owner to confirm it is still published.";
|
|
40
|
-
};
|
|
41
|
-
|
|
42
31
|
const sanitizeWorkflowNameForEmail = (value: string): string =>
|
|
43
32
|
value
|
|
44
33
|
.replace(/[\r\n]+/g, " ")
|
|
@@ -27,11 +27,7 @@ export interface ChatInterfaceProps {
|
|
|
27
27
|
isMinimizable?: boolean;
|
|
28
28
|
isClosable?: boolean;
|
|
29
29
|
position?:
|
|
30
|
-
| "bottom-right"
|
|
31
|
-
| "bottom-left"
|
|
32
|
-
| "top-right"
|
|
33
|
-
| "top-left"
|
|
34
|
-
| "center";
|
|
30
|
+
"bottom-right" | "bottom-left" | "top-right" | "top-left" | "center";
|
|
35
31
|
triggerButton?: React.ReactNode;
|
|
36
32
|
user: ChatParticipant;
|
|
37
33
|
ai: ChatParticipant;
|
|
@@ -44,17 +44,6 @@ const parseCache = (raw: string | null): SystemInfoCachePayload | null => {
|
|
|
44
44
|
}
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
-
const shouldRefresh = (cache: SystemInfoCachePayload | null): boolean => {
|
|
48
|
-
if (!cache) {
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
const lastCheckedAt = Date.parse(cache.checkedAt);
|
|
52
|
-
if (Number.isNaN(lastCheckedAt)) {
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
return Date.now() - lastCheckedAt >= UPDATE_CHECK_TTL_MS;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
47
|
const parseSemver = (value: string | null | undefined): ParsedSemver | null => {
|
|
59
48
|
if (!value) {
|
|
60
49
|
return null;
|
|
@@ -152,7 +141,10 @@ const isDismissed = (): boolean => {
|
|
|
152
141
|
};
|
|
153
142
|
|
|
154
143
|
export default function VersionStatus() {
|
|
155
|
-
const [
|
|
144
|
+
const [cachedInfo, setCachedInfo] = useState<SystemInfoResponse | null>(null);
|
|
145
|
+
const [liveBackendVersion, setLiveBackendVersion] = useState<string | null>(
|
|
146
|
+
null,
|
|
147
|
+
);
|
|
156
148
|
const [dismissedReminder, setDismissedReminder] = useState<boolean>(() =>
|
|
157
149
|
isDismissed(),
|
|
158
150
|
);
|
|
@@ -163,11 +155,12 @@ export default function VersionStatus() {
|
|
|
163
155
|
window.localStorage.getItem(UPDATE_CHECK_CACHE_KEY),
|
|
164
156
|
);
|
|
165
157
|
if (cache) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
158
|
+
setCachedInfo(cache.payload);
|
|
159
|
+
setLiveBackendVersion(cache.payload.backend.current_version);
|
|
160
|
+
const cacheAge = Date.now() - Date.parse(cache.checkedAt);
|
|
161
|
+
if (cacheAge < UPDATE_CHECK_TTL_MS) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
171
164
|
}
|
|
172
165
|
|
|
173
166
|
let active = true;
|
|
@@ -176,7 +169,8 @@ export default function VersionStatus() {
|
|
|
176
169
|
if (!active) {
|
|
177
170
|
return;
|
|
178
171
|
}
|
|
179
|
-
|
|
172
|
+
setLiveBackendVersion(payload.backend.current_version);
|
|
173
|
+
setCachedInfo(payload);
|
|
180
174
|
const cachePayload: SystemInfoCachePayload = {
|
|
181
175
|
checkedAt: new Date().toISOString(),
|
|
182
176
|
payload,
|
|
@@ -196,39 +190,41 @@ export default function VersionStatus() {
|
|
|
196
190
|
}, []);
|
|
197
191
|
|
|
198
192
|
const studioUpdateAvailable = useMemo(() => {
|
|
199
|
-
if (!
|
|
193
|
+
if (!cachedInfo) {
|
|
200
194
|
return false;
|
|
201
195
|
}
|
|
202
|
-
if (
|
|
196
|
+
if (cachedInfo.studio.update_available) {
|
|
203
197
|
return true;
|
|
204
198
|
}
|
|
205
|
-
return compareSemver(studioVersion,
|
|
206
|
-
}, [studioVersion,
|
|
199
|
+
return compareSemver(studioVersion, cachedInfo.studio.latest_version) < 0;
|
|
200
|
+
}, [studioVersion, cachedInfo]);
|
|
207
201
|
|
|
208
202
|
const showReminder =
|
|
209
|
-
((
|
|
203
|
+
((cachedInfo?.backend.update_available ?? false) ||
|
|
210
204
|
studioUpdateAvailable) &&
|
|
211
205
|
!dismissedReminder;
|
|
212
206
|
|
|
213
207
|
const versionSummary = useMemo(() => {
|
|
214
|
-
const backend =
|
|
208
|
+
const backend = liveBackendVersion ?? "unknown";
|
|
215
209
|
return `Orcheo ${studioVersion} · Backend ${backend}`;
|
|
216
|
-
}, [studioVersion,
|
|
210
|
+
}, [studioVersion, liveBackendVersion]);
|
|
217
211
|
|
|
218
212
|
const updateLines = useMemo(() => {
|
|
219
|
-
if (!
|
|
213
|
+
if (!cachedInfo) return [];
|
|
220
214
|
const lines: string[] = [];
|
|
221
|
-
if (
|
|
215
|
+
if (cachedInfo.backend.update_available) {
|
|
222
216
|
lines.push(
|
|
223
|
-
`Backend: ${
|
|
217
|
+
`Backend: ${cachedInfo.backend.current_version} → ${cachedInfo.backend.latest_version}`,
|
|
224
218
|
);
|
|
225
219
|
}
|
|
226
220
|
if (studioUpdateAvailable) {
|
|
227
|
-
const studioCurrent =
|
|
228
|
-
lines.push(
|
|
221
|
+
const studioCurrent = cachedInfo.studio.current_version ?? studioVersion;
|
|
222
|
+
lines.push(
|
|
223
|
+
`Orcheo: ${studioCurrent} → ${cachedInfo.studio.latest_version}`,
|
|
224
|
+
);
|
|
229
225
|
}
|
|
230
226
|
return lines;
|
|
231
|
-
}, [studioUpdateAvailable, studioVersion,
|
|
227
|
+
}, [studioUpdateAvailable, studioVersion, cachedInfo]);
|
|
232
228
|
|
|
233
229
|
const dismissUpdateReminder = () => {
|
|
234
230
|
if (typeof window === "undefined") {
|
|
@@ -27,6 +27,8 @@ type CredentialAccess = CredentialVaultAccessLevel;
|
|
|
27
27
|
|
|
28
28
|
interface AddCredentialDialogProps {
|
|
29
29
|
onAddCredential?: (credential: CredentialInput) => Promise<void> | void;
|
|
30
|
+
triggerLabel?: string;
|
|
31
|
+
showTriggerIcon?: boolean;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
const DEFAULT_CREDENTIAL: CredentialInput = {
|
|
@@ -38,6 +40,8 @@ const DEFAULT_CREDENTIAL: CredentialInput = {
|
|
|
38
40
|
|
|
39
41
|
export function AddCredentialDialog({
|
|
40
42
|
onAddCredential,
|
|
43
|
+
triggerLabel = "Add Credential",
|
|
44
|
+
showTriggerIcon = true,
|
|
41
45
|
}: AddCredentialDialogProps) {
|
|
42
46
|
const [isOpen, setIsOpen] = useState(false);
|
|
43
47
|
const [pendingCredential, setPendingCredential] =
|
|
@@ -117,8 +121,8 @@ export function AddCredentialDialog({
|
|
|
117
121
|
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
|
118
122
|
<DialogTrigger asChild>
|
|
119
123
|
<Button>
|
|
120
|
-
<Plus className="h-4 w-4 mr-2" />
|
|
121
|
-
|
|
124
|
+
{showTriggerIcon ? <Plus className="h-4 w-4 mr-2" /> : null}
|
|
125
|
+
{triggerLabel}
|
|
122
126
|
</Button>
|
|
123
127
|
</DialogTrigger>
|
|
124
128
|
<DialogContent className="sm:max-w-[500px]">
|
|
@@ -43,16 +43,22 @@ export default function CredentialsVault({
|
|
|
43
43
|
|
|
44
44
|
return (
|
|
45
45
|
<div className={containerClassName}>
|
|
46
|
-
<div className="
|
|
47
|
-
<div className="
|
|
46
|
+
<div className="space-y-2 pr-10">
|
|
47
|
+
<div className="flex items-start justify-between gap-3">
|
|
48
48
|
<h2 className="text-xl font-bold">Credential Vault</h2>
|
|
49
|
+
<AddCredentialDialog
|
|
50
|
+
onAddCredential={onAddCredential}
|
|
51
|
+
showTriggerIcon={false}
|
|
52
|
+
triggerLabel="+ Add"
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
<div className="space-y-1">
|
|
49
56
|
<p className="text-sm text-muted-foreground">
|
|
50
57
|
Credentials stored here are securely injected into your workflows at
|
|
51
58
|
runtime, so nodes can authenticate to external services without
|
|
52
59
|
hardcoding secrets.
|
|
53
60
|
</p>
|
|
54
61
|
</div>
|
|
55
|
-
<AddCredentialDialog onAddCredential={onAddCredential} />
|
|
56
62
|
</div>
|
|
57
63
|
|
|
58
64
|
<div className="flex min-w-0 items-center">
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
uploadWorkflowFromFiles,
|
|
18
18
|
WorkflowUploadFailedError,
|
|
19
19
|
} from "@features/workflow/lib/workflow-storage";
|
|
20
|
+
import { parseWorkflowFrontmatter } from "@features/workflow/lib/workflow-frontmatter";
|
|
20
21
|
import { getWorkflowRouteRef } from "@features/workflow/lib/workflow-storage-helpers";
|
|
21
22
|
import { getSelectedWorkspaceSlug } from "@/lib/workspace-session";
|
|
22
23
|
import { getWorkspaceWorkflowPath } from "@/lib/workspace-routing";
|
|
@@ -126,6 +127,10 @@ export function UploadWorkflowDialog({
|
|
|
126
127
|
const text = e.target?.result;
|
|
127
128
|
if (typeof text === "string") {
|
|
128
129
|
setScriptContent(text);
|
|
130
|
+
const frontmatter = parseWorkflowFrontmatter(text);
|
|
131
|
+
if (!workflowName && frontmatter.name) {
|
|
132
|
+
setWorkflowName(frontmatter.name);
|
|
133
|
+
}
|
|
129
134
|
}
|
|
130
135
|
};
|
|
131
136
|
reader.readAsText(file);
|
|
@@ -72,11 +72,20 @@ const clampInt = (value: string, max: number, fallback: number) => {
|
|
|
72
72
|
};
|
|
73
73
|
|
|
74
74
|
/** Parse a single cron field's first numeric token, falling back to `fallback`. */
|
|
75
|
-
const parseField = (
|
|
76
|
-
field
|
|
75
|
+
const parseField = (
|
|
76
|
+
field: string | undefined,
|
|
77
|
+
max: number,
|
|
78
|
+
fallback: number,
|
|
79
|
+
) =>
|
|
80
|
+
field && field !== "*"
|
|
81
|
+
? clampInt(field.split(/[,/-]/)[0], max, fallback)
|
|
82
|
+
: fallback;
|
|
77
83
|
|
|
78
84
|
/** Extract the step N from a `*\/N` field (clamped to `max`), or undefined if it isn't a step. */
|
|
79
|
-
const parseStep = (
|
|
85
|
+
const parseStep = (
|
|
86
|
+
field: string | undefined,
|
|
87
|
+
max: number,
|
|
88
|
+
): number | undefined => {
|
|
80
89
|
const match = /^\*\/(\d+)$/.exec(field ?? "");
|
|
81
90
|
return match ? clampInt(match[1], max, 1) || 1 : undefined;
|
|
82
91
|
};
|
|
@@ -98,7 +107,9 @@ const parseDaysOfWeek = (field: string | undefined): number[] => {
|
|
|
98
107
|
return Array.from(new Set(days));
|
|
99
108
|
};
|
|
100
109
|
|
|
101
|
-
const parseCron = (
|
|
110
|
+
const parseCron = (
|
|
111
|
+
value: string | undefined,
|
|
112
|
+
): { freq: Frequency; parts: CronParts } => {
|
|
102
113
|
const [minute, hour, dom, , dow] = (value ?? "").trim().split(/\s+/);
|
|
103
114
|
const minuteStep = parseStep(minute, 59);
|
|
104
115
|
const hourStep = parseStep(hour, 23);
|
|
@@ -117,13 +128,15 @@ const parseCron = (value: string | undefined): { freq: Frequency; parts: CronPar
|
|
|
117
128
|
// `*/30 * * * *` (step) or `* * * * *` (every minute) → minute interval.
|
|
118
129
|
else if (minuteStep !== undefined || minute === "*") freq = "minute";
|
|
119
130
|
// `M */2 * * *` (step) or `M * * * *` (every hour) → hour interval.
|
|
120
|
-
else if (hourStep !== undefined || hour === "*" || hour === undefined)
|
|
131
|
+
else if (hourStep !== undefined || hour === "*" || hour === undefined)
|
|
132
|
+
freq = "hour";
|
|
121
133
|
|
|
122
134
|
return { freq, parts };
|
|
123
135
|
};
|
|
124
136
|
|
|
125
137
|
const buildCron = (freq: Frequency, parts: CronParts): string => {
|
|
126
|
-
const { interval, hourInterval, minute, hour, dayOfMonth, daysOfWeek } =
|
|
138
|
+
const { interval, hourInterval, minute, hour, dayOfMonth, daysOfWeek } =
|
|
139
|
+
parts;
|
|
127
140
|
switch (freq) {
|
|
128
141
|
case "minute":
|
|
129
142
|
// `*/1` is just "every minute"; emit the canonical wildcard form.
|
|
@@ -136,7 +149,9 @@ const buildCron = (freq: Frequency, parts: CronParts): string => {
|
|
|
136
149
|
case "day":
|
|
137
150
|
return `${minute} ${hour} * * *`;
|
|
138
151
|
case "week": {
|
|
139
|
-
const dow = daysOfWeek.length
|
|
152
|
+
const dow = daysOfWeek.length
|
|
153
|
+
? [...daysOfWeek].sort((a, b) => a - b).join(",")
|
|
154
|
+
: "*";
|
|
140
155
|
return `${minute} ${hour} * * ${dow}`;
|
|
141
156
|
}
|
|
142
157
|
case "month":
|
|
@@ -165,7 +180,11 @@ const canonicalCron = (value: string | undefined): string | null => {
|
|
|
165
180
|
if (/[a-z]/i.test(fields.join(""))) return null;
|
|
166
181
|
const days = parseDaysOfWeek(dow);
|
|
167
182
|
const dowField =
|
|
168
|
-
dow === "*"
|
|
183
|
+
dow === "*"
|
|
184
|
+
? "*"
|
|
185
|
+
: days.length
|
|
186
|
+
? [...days].sort((a, b) => a - b).join(",")
|
|
187
|
+
: dow;
|
|
169
188
|
return [minute, hour, dom, month, dowField].join(" ");
|
|
170
189
|
};
|
|
171
190
|
|
|
@@ -292,7 +311,9 @@ function CronWidget(props: WidgetProps) {
|
|
|
292
311
|
|
|
293
312
|
{(freq === "minute" || freq === "hour") && (
|
|
294
313
|
<Select
|
|
295
|
-
value={String(
|
|
314
|
+
value={String(
|
|
315
|
+
freq === "minute" ? parts.interval : parts.hourInterval,
|
|
316
|
+
)}
|
|
296
317
|
onValueChange={(v) =>
|
|
297
318
|
freq === "minute"
|
|
298
319
|
? update("minute", { ...parts, interval: Number(v) })
|
|
@@ -343,7 +364,11 @@ function CronWidget(props: WidgetProps) {
|
|
|
343
364
|
"flex h-9 min-w-[6rem] items-center justify-between gap-2 whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
|
344
365
|
)}
|
|
345
366
|
>
|
|
346
|
-
<span
|
|
367
|
+
<span
|
|
368
|
+
className={cn(
|
|
369
|
+
parts.daysOfWeek.length === 0 && "text-muted-foreground",
|
|
370
|
+
)}
|
|
371
|
+
>
|
|
347
372
|
{selectedDaysLabel}
|
|
348
373
|
</span>
|
|
349
374
|
<ChevronDown className="h-4 w-4 opacity-50" />
|
|
@@ -31,16 +31,7 @@ export type ColorVariant =
|
|
|
31
31
|
| "gray";
|
|
32
32
|
|
|
33
33
|
export type ComponentSize =
|
|
34
|
-
| "
|
|
35
|
-
| "5"
|
|
36
|
-
| "6"
|
|
37
|
-
| "7"
|
|
38
|
-
| "8"
|
|
39
|
-
| "9"
|
|
40
|
-
| "10"
|
|
41
|
-
| "11"
|
|
42
|
-
| "12"
|
|
43
|
-
| "16";
|
|
34
|
+
"4" | "5" | "6" | "7" | "8" | "9" | "10" | "11" | "12" | "16";
|
|
44
35
|
|
|
45
36
|
// CONSTANTS
|
|
46
37
|
|
|
@@ -50,10 +50,10 @@ const extractVersionRecord = (
|
|
|
50
50
|
lookup: WorkflowLookup,
|
|
51
51
|
) => {
|
|
52
52
|
const inputs = history.inputs ?? {};
|
|
53
|
-
const metadata = (inputs.metadata ??
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
const metadata = (inputs.metadata ??
|
|
54
|
+
inputs.workflow ??
|
|
55
|
+
inputs.canvas ??
|
|
56
|
+
{}) as Record<string, unknown>;
|
|
57
57
|
const versionIdRaw = metadata?.workflow_version_id;
|
|
58
58
|
if (typeof versionIdRaw === "string" && versionIdRaw) {
|
|
59
59
|
return lookup.versions.get(versionIdRaw);
|
|
@@ -31,17 +31,10 @@ export type WorkflowLookup = {
|
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
export type WorkflowExecutionStatus =
|
|
34
|
-
| "
|
|
35
|
-
| "success"
|
|
36
|
-
| "failed"
|
|
37
|
-
| "partial";
|
|
34
|
+
"running" | "success" | "failed" | "partial";
|
|
38
35
|
|
|
39
36
|
export type WorkflowExecutionNodeStatus =
|
|
40
|
-
| "
|
|
41
|
-
| "running"
|
|
42
|
-
| "success"
|
|
43
|
-
| "error"
|
|
44
|
-
| "warning";
|
|
37
|
+
"idle" | "running" | "success" | "error" | "warning";
|
|
45
38
|
|
|
46
39
|
export interface WorkflowExecutionNode {
|
|
47
40
|
id: string;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const FRONTMATTER_START_RE = /^# \/\/\/ (?<type>[a-zA-Z0-9_-]+)[ \t]*$/;
|
|
2
|
+
const FRONTMATTER_END_RE = /^# \/\/\/[ \t]*$/;
|
|
3
|
+
|
|
4
|
+
const STRING_FIELDS = new Set([
|
|
5
|
+
"name",
|
|
6
|
+
"id",
|
|
7
|
+
"handle",
|
|
8
|
+
"description",
|
|
9
|
+
"entrypoint",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
export interface WorkflowFrontmatter {
|
|
13
|
+
name?: string;
|
|
14
|
+
id?: string;
|
|
15
|
+
handle?: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
entrypoint?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const parseTomlString = (value: string): string | undefined => {
|
|
21
|
+
const trimmed = value.trim();
|
|
22
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(trimmed) as unknown;
|
|
25
|
+
return typeof parsed === "string" && parsed.trim()
|
|
26
|
+
? parsed.trim()
|
|
27
|
+
: undefined;
|
|
28
|
+
} catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
33
|
+
const parsed = trimmed.slice(1, -1);
|
|
34
|
+
return parsed.trim() ? parsed.trim() : undefined;
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const collectOrcheoFrontmatter = (source: string): string[] | undefined => {
|
|
40
|
+
const blocks: string[][] = [];
|
|
41
|
+
let currentLines: string[] = [];
|
|
42
|
+
let currentType: string | undefined;
|
|
43
|
+
let inBlock = false;
|
|
44
|
+
|
|
45
|
+
for (const line of source.split(/\r?\n/)) {
|
|
46
|
+
if (inBlock) {
|
|
47
|
+
if (FRONTMATTER_END_RE.test(line)) {
|
|
48
|
+
if (currentType === "orcheo") {
|
|
49
|
+
blocks.push(currentLines);
|
|
50
|
+
}
|
|
51
|
+
currentLines = [];
|
|
52
|
+
currentType = undefined;
|
|
53
|
+
inBlock = false;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
currentLines.push(line);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const match = line.match(FRONTMATTER_START_RE);
|
|
61
|
+
if (match?.groups?.type) {
|
|
62
|
+
currentType = match.groups.type;
|
|
63
|
+
currentLines = [];
|
|
64
|
+
inBlock = true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (blocks.length !== 1) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
return blocks[0];
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const toTomlLines = (contentLines: string[]): string[] =>
|
|
75
|
+
contentLines.flatMap((line) => {
|
|
76
|
+
if (!line.startsWith("#")) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
let stripped = line.slice(1);
|
|
80
|
+
if (stripped.startsWith(" ") || stripped.startsWith("\t")) {
|
|
81
|
+
stripped = stripped.slice(1);
|
|
82
|
+
}
|
|
83
|
+
return [stripped];
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export const parseWorkflowFrontmatter = (
|
|
87
|
+
source: string,
|
|
88
|
+
): WorkflowFrontmatter => {
|
|
89
|
+
const contentLines = collectOrcheoFrontmatter(source);
|
|
90
|
+
if (!contentLines) {
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const frontmatter: WorkflowFrontmatter = {};
|
|
95
|
+
for (const line of toTomlLines(contentLines)) {
|
|
96
|
+
const trimmed = line.trim();
|
|
97
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const match = trimmed.match(/^([A-Za-z0-9_-]+)\s*=\s*(.+)$/);
|
|
101
|
+
const key = match?.[1];
|
|
102
|
+
if (!key || !STRING_FIELDS.has(key)) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const value = parseTomlString(match[2] ?? "");
|
|
106
|
+
if (!value) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
frontmatter[key as keyof WorkflowFrontmatter] = value;
|
|
110
|
+
}
|
|
111
|
+
return frontmatter;
|
|
112
|
+
};
|