From 91a3d065298cce7a882464fa0cd31d8f1ae9dda2 Mon Sep 17 00:00:00 2001 From: Faton Ramadani Date: Wed, 16 Oct 2024 17:46:59 +0200 Subject: [PATCH] feat(frontend): unify all triggers UX and simplify flow settings (#4259) * feat(frontend): added list of triggers in the flow graph * feat(frontend): added list of triggers in the flow graph * feat(frontend): clean up * feat(frontend): improve UX * feat(frontend): triggers * feat(frontend): triggers * feat(frontend): done * feat(frontend): fix trigger when position when a preprocessor is presetn * Glm/rework flow settings v2 (#4497) * fat(frontend): simplify flow settings menu * improve scroll * changing mute toggle * Add advanced settings badge * Add nord theme colors * Add bage for advanced options * fix minor issue * fix minor issue * Add triggers menu to flow settings * Add quick trigger access * remove triggers in flow settings * fix minor issue * Move triggers settings to flow right panel * polishing * fix unset store * remove save up to for triggers * fix padding * reset default tag color * remove custom select component * revert path change * revert section modif * Revert unused feature --------- Co-authored-by: Guilhem * Connect top bar cron to schedules settings * Turn copilot into node * fix copilot placement * remove useless import * fix center copilot * fix binding * remove copilot on top of preprocessor * render copilot node on condition * quickfix * remove copilot node * fix minor issues * fix route count update * fix schedule sync * harmonize colors * fix alignment and add edges * recenter node summary * fix schedules sync * Add id title * all * all * all * iteration * all * all * done * fix * more fixes --------- Co-authored-by: Guilhem Co-authored-by: Guilhem Co-authored-by: Ruben Fiszel Co-authored-by: Ruben Fiszel --- ...4def64233dc4f8fc3344d8d74be8e96ded659.json | 24 + ...232f173446d29c08fc29ddb8d55294f2c070e.json | 24 + ...8543435a95b019b20081f52f6ed6f4457e3c7.json | 23 + ...7f5033b9c9afc344d9c3e385ba20a3ad2197a.json | 2 +- ...4391516d8b169186548ec44528c84078b80d8.json | 23 + ...f62ec9db58611a99b0ba7f0375638b128ab89.json | 24 + ...b18e8c1d16ef25ffb9d04e5447d0d583d55c.json} | 7 +- ...f31572fc1bf12e6ae473fd58fafb025989965.json | 59 ++ ...ad78b69292ba091d69880945e42bebe890d66.json | 23 + ...d3fd25f6b85cbea870505751c6a82457ae1da.json | 59 ++ ...b71a6fcfa1e5603ea472892917c1accd1aafb.json | 23 + backend/windmill-api/openapi.yaml | 96 ++ backend/windmill-api/src/flows.rs | 21 + backend/windmill-api/src/lib.rs | 1 + backend/windmill-api/src/scripts.rs | 21 + backend/windmill-api/src/triggers.rs | 131 +++ backend/windmill-api/src/users.rs | 19 +- frontend/openapi-ts-error-1726231138297.log | 28 - .../src/lib/components/AssignableTags.svelte | 3 +- frontend/src/lib/components/CronInput.svelte | 9 +- frontend/src/lib/components/Dev.svelte | 25 +- .../src/lib/components/FlowBuilder.svelte | 194 +++-- .../src/lib/components/FlowGraphViewer.svelte | 12 +- .../src/lib/components/FlowViewerInner.svelte | 5 +- frontend/src/lib/components/Label.svelte | 3 +- frontend/src/lib/components/Path.svelte | 112 +-- .../src/lib/components/PrimarySchedule.svelte | 86 ++ .../lib/components/RunPageSchedules.svelte | 406 +++++---- .../lib/components/ScheduleEditorInner.svelte | 3 +- .../src/lib/components/SchemaViewer.svelte | 18 +- .../src/lib/components/ScriptBuilder.svelte | 173 ++-- .../src/lib/components/ScriptEditor.svelte | 20 +- .../src/lib/components/ScriptSchedules.svelte | 110 --- frontend/src/lib/components/Section.svelte | 3 + frontend/src/lib/components/Toast.svelte | 3 +- frontend/src/lib/components/Toggle.svelte | 24 +- .../src/lib/components/UserSettings.svelte | 8 +- .../src/lib/components/WorkerTagPicker.svelte | 2 +- .../apps/editor/AppExportButton.svelte | 5 +- .../components/common/button/Button.svelte | 7 + .../src/lib/components/common/tabs/Tab.svelte | 2 +- .../components/details/ClipboardPanel.svelte | 6 +- .../details/DetailPageDetailPanel.svelte | 93 +- .../details/DetailPageHeader.svelte | 23 + .../details/DetailPageLayout.svelte | 48 +- .../details/DetailPageTriggerPanel.svelte | 59 ++ .../details/EmailTriggerPanel.svelte | 45 +- .../details/ErrorHandlerToggleButtonV2.svelte | 67 ++ .../lib/components/flows/FlowEditor.svelte | 4 +- .../flows/content/FlowEditorPanel.svelte | 13 +- .../flows/content/FlowModuleWrapper.svelte | 45 +- .../flows/content/FlowSchedules.svelte | 106 --- .../flows/content/FlowSettings.svelte | 821 +++++++++--------- .../flows/header/FlowPreviewButtons.svelte | 3 +- .../flows/map/FlowCopilotButton.svelte | 3 +- .../flows/map/FlowModuleSchemaItem.svelte | 23 +- .../flows/map/FlowModuleSchemaMap.svelte | 6 +- .../lib/components/flows/map/MapItem.svelte | 5 - .../src/lib/components/flows/scheduleUtils.ts | 11 +- frontend/src/lib/components/flows/types.ts | 2 - .../lib/components/graph/FlowGraphV2.svelte | 23 +- .../src/lib/components/graph/graphBuilder.ts | 29 +- .../renderers/edges/HiddenBaseEdge.svelte | 6 + .../graph/renderers/nodes/InputNode.svelte | 6 +- .../graph/renderers/nodes/ModuleNode.svelte | 1 - .../graph/renderers/nodes/NodeWrapper.svelte | 4 +- .../graph/renderers/nodes/TriggersNode.svelte | 31 + .../renderers/triggers/TriggerButton.svelte | 15 + .../renderers/triggers/TriggerCount.svelte | 19 + .../renderers/triggers/TriggersBadge.svelte | 106 +++ .../renderers/triggers/TriggersWrapper.svelte | 32 + frontend/src/lib/components/toast.ts | 8 + frontend/src/lib/components/triggers.ts | 16 + .../components/triggers/RouteEditor.svelte | 4 + .../components/triggers/RoutesPanel.svelte | 80 +- .../components/triggers/TriggerTokens.svelte | 79 ++ .../components/triggers/TriggersEditor.svelte | 77 ++ .../WebhooksPanel.svelte | 76 +- .../(root)/(logged)/flows/add/+page.svelte | 4 + .../flows/edit/[...path]/+page.svelte | 12 + .../(logged)/flows/get/[...path]/+page.svelte | 129 +-- .../(root)/(logged)/scripts/add/+page.svelte | 7 +- .../scripts/edit/[...path]/+page.svelte | 8 + .../scripts/get/[...hash]/+page.svelte | 113 ++- frontend/src/routes/flows/dev/+page.svelte | 26 +- frontend/tailwind.config.cjs | 18 +- 86 files changed, 2737 insertions(+), 1416 deletions(-) create mode 100644 backend/.sqlx/query-1ca5bc2d35c0498b587fd0618434def64233dc4f8fc3344d8d74be8e96ded659.json create mode 100644 backend/.sqlx/query-31bc3dcea29be9cc0242771d25a232f173446d29c08fc29ddb8d55294f2c070e.json create mode 100644 backend/.sqlx/query-5fc6b4a4dbb7875bdec76f876c18543435a95b019b20081f52f6ed6f4457e3c7.json create mode 100644 backend/.sqlx/query-a7f5431e3b8960e9dc46fae69dd4391516d8b169186548ec44528c84078b80d8.json create mode 100644 backend/.sqlx/query-c060b8bbc5af7d2e7d0aaff64f0f62ec9db58611a99b0ba7f0375638b128ab89.json rename backend/.sqlx/{query-34ad8a2a5bd89b9b8e25847a7e5e94ef99e35a178ad6328c1bcde2a6d6f88cb5.json => query-c624f15f3e321b1eecf123da9bf0b18e8c1d16ef25ffb9d04e5447d0d583d55c.json} (58%) create mode 100644 backend/.sqlx/query-c7ee7ce64686cef41cebd99ad7ef31572fc1bf12e6ae473fd58fafb025989965.json create mode 100644 backend/.sqlx/query-d7a0f19f9e18d2ea49316012375ad78b69292ba091d69880945e42bebe890d66.json create mode 100644 backend/.sqlx/query-eff32aeac25a75d06f73e08c26dd3fd25f6b85cbea870505751c6a82457ae1da.json create mode 100644 backend/.sqlx/query-f06e0e4fa358b26792df22fff48b71a6fcfa1e5603ea472892917c1accd1aafb.json create mode 100644 backend/windmill-api/src/triggers.rs delete mode 100644 frontend/openapi-ts-error-1726231138297.log create mode 100644 frontend/src/lib/components/PrimarySchedule.svelte delete mode 100644 frontend/src/lib/components/ScriptSchedules.svelte create mode 100644 frontend/src/lib/components/details/DetailPageTriggerPanel.svelte create mode 100644 frontend/src/lib/components/details/ErrorHandlerToggleButtonV2.svelte delete mode 100644 frontend/src/lib/components/flows/content/FlowSchedules.svelte create mode 100644 frontend/src/lib/components/graph/renderers/edges/HiddenBaseEdge.svelte create mode 100644 frontend/src/lib/components/graph/renderers/nodes/TriggersNode.svelte create mode 100644 frontend/src/lib/components/graph/renderers/triggers/TriggerButton.svelte create mode 100644 frontend/src/lib/components/graph/renderers/triggers/TriggerCount.svelte create mode 100644 frontend/src/lib/components/graph/renderers/triggers/TriggersBadge.svelte create mode 100644 frontend/src/lib/components/graph/renderers/triggers/TriggersWrapper.svelte create mode 100644 frontend/src/lib/components/toast.ts create mode 100644 frontend/src/lib/components/triggers.ts create mode 100644 frontend/src/lib/components/triggers/TriggerTokens.svelte create mode 100644 frontend/src/lib/components/triggers/TriggersEditor.svelte rename frontend/src/lib/components/{details => triggers}/WebhooksPanel.svelte (82%) diff --git a/backend/.sqlx/query-1ca5bc2d35c0498b587fd0618434def64233dc4f8fc3344d8d74be8e96ded659.json b/backend/.sqlx/query-1ca5bc2d35c0498b587fd0618434def64233dc4f8fc3344d8d74be8e96ded659.json new file mode 100644 index 0000000000..3a2e44bdba --- /dev/null +++ b/backend/.sqlx/query-1ca5bc2d35c0498b587fd0618434def64233dc4f8fc3344d8d74be8e96ded659.json @@ -0,0 +1,24 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM schedule WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "Bool", + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "1ca5bc2d35c0498b587fd0618434def64233dc4f8fc3344d8d74be8e96ded659" +} diff --git a/backend/.sqlx/query-31bc3dcea29be9cc0242771d25a232f173446d29c08fc29ddb8d55294f2c070e.json b/backend/.sqlx/query-31bc3dcea29be9cc0242771d25a232f173446d29c08fc29ddb8d55294f2c070e.json new file mode 100644 index 0000000000..49fc84c349 --- /dev/null +++ b/backend/.sqlx/query-31bc3dcea29be9cc0242771d25a232f173446d29c08fc29ddb8d55294f2c070e.json @@ -0,0 +1,24 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM http_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "Bool", + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "31bc3dcea29be9cc0242771d25a232f173446d29c08fc29ddb8d55294f2c070e" +} diff --git a/backend/.sqlx/query-5fc6b4a4dbb7875bdec76f876c18543435a95b019b20081f52f6ed6f4457e3c7.json b/backend/.sqlx/query-5fc6b4a4dbb7875bdec76f876c18543435a95b019b20081f52f6ed6f4457e3c7.json new file mode 100644 index 0000000000..d4725c224e --- /dev/null +++ b/backend/.sqlx/query-5fc6b4a4dbb7875bdec76f876c18543435a95b019b20081f52f6ed6f4457e3c7.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM token WHERE label LIKE 'webhook-%' AND workspace_id = $1 AND scopes @> ARRAY['run:' || $2]::text[]", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "5fc6b4a4dbb7875bdec76f876c18543435a95b019b20081f52f6ed6f4457e3c7" +} diff --git a/backend/.sqlx/query-661f472ff3860983322162420457f5033b9c9afc344d9c3e385ba20a3ad2197a.json b/backend/.sqlx/query-661f472ff3860983322162420457f5033b9c9afc344d9c3e385ba20a3ad2197a.json index 1fa370e682..75b8108281 100644 --- a/backend/.sqlx/query-661f472ff3860983322162420457f5033b9c9afc344d9c3e385ba20a3ad2197a.json +++ b/backend/.sqlx/query-661f472ff3860983322162420457f5033b9c9afc344d9c3e385ba20a3ad2197a.json @@ -5,7 +5,7 @@ "columns": [ { "ordinal": 0, - "name": "bool", + "name": "?column?", "type_info": "Bool" } ], diff --git a/backend/.sqlx/query-a7f5431e3b8960e9dc46fae69dd4391516d8b169186548ec44528c84078b80d8.json b/backend/.sqlx/query-a7f5431e3b8960e9dc46fae69dd4391516d8b169186548ec44528c84078b80d8.json new file mode 100644 index 0000000000..7dc8e3efa7 --- /dev/null +++ b/backend/.sqlx/query-a7f5431e3b8960e9dc46fae69dd4391516d8b169186548ec44528c84078b80d8.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM token WHERE label LIKE 'email-%' AND workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "a7f5431e3b8960e9dc46fae69dd4391516d8b169186548ec44528c84078b80d8" +} diff --git a/backend/.sqlx/query-c060b8bbc5af7d2e7d0aaff64f0f62ec9db58611a99b0ba7f0375638b128ab89.json b/backend/.sqlx/query-c060b8bbc5af7d2e7d0aaff64f0f62ec9db58611a99b0ba7f0375638b128ab89.json new file mode 100644 index 0000000000..1a37220559 --- /dev/null +++ b/backend/.sqlx/query-c060b8bbc5af7d2e7d0aaff64f0f62ec9db58611a99b0ba7f0375638b128ab89.json @@ -0,0 +1,24 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT schedule FROM schedule WHERE path = $1 AND script_path = $1 AND is_flow = $2 AND workspace_id = $3", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "schedule", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text", + "Bool", + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "c060b8bbc5af7d2e7d0aaff64f0f62ec9db58611a99b0ba7f0375638b128ab89" +} diff --git a/backend/.sqlx/query-34ad8a2a5bd89b9b8e25847a7e5e94ef99e35a178ad6328c1bcde2a6d6f88cb5.json b/backend/.sqlx/query-c624f15f3e321b1eecf123da9bf0b18e8c1d16ef25ffb9d04e5447d0d583d55c.json similarity index 58% rename from backend/.sqlx/query-34ad8a2a5bd89b9b8e25847a7e5e94ef99e35a178ad6328c1bcde2a6d6f88cb5.json rename to backend/.sqlx/query-c624f15f3e321b1eecf123da9bf0b18e8c1d16ef25ffb9d04e5447d0d583d55c.json index 43d0f596ae..edb2fc7f49 100644 --- a/backend/.sqlx/query-34ad8a2a5bd89b9b8e25847a7e5e94ef99e35a178ad6328c1bcde2a6d6f88cb5.json +++ b/backend/.sqlx/query-c624f15f3e321b1eecf123da9bf0b18e8c1d16ef25ffb9d04e5447d0d583d55c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO token\n (token, email, label, expiration, super_admin, scopes)\n VALUES ($1, $2, $3, $4, $5, $6)", + "query": "INSERT INTO token\n (token, email, label, expiration, super_admin, scopes, workspace_id)\n VALUES ($1, $2, $3, $4, $5, $6, $7)", "describe": { "columns": [], "parameters": { @@ -10,10 +10,11 @@ "Varchar", "Timestamptz", "Bool", - "TextArray" + "TextArray", + "Varchar" ] }, "nullable": [] }, - "hash": "34ad8a2a5bd89b9b8e25847a7e5e94ef99e35a178ad6328c1bcde2a6d6f88cb5" + "hash": "c624f15f3e321b1eecf123da9bf0b18e8c1d16ef25ffb9d04e5447d0d583d55c" } diff --git a/backend/.sqlx/query-c7ee7ce64686cef41cebd99ad7ef31572fc1bf12e6ae473fd58fafb025989965.json b/backend/.sqlx/query-c7ee7ce64686cef41cebd99ad7ef31572fc1bf12e6ae473fd58fafb025989965.json new file mode 100644 index 0000000000..7dd454bbbe --- /dev/null +++ b/backend/.sqlx/query-c7ee7ce64686cef41cebd99ad7ef31572fc1bf12e6ae473fd58fafb025989965.json @@ -0,0 +1,59 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at, scopes, email FROM token WHERE workspace_id = $1 AND scopes @> ARRAY['run:script/' || $2]::text[]", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "label", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "token_prefix", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "expiration", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "last_used_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "scopes", + "type_info": "TextArray" + }, + { + "ordinal": 6, + "name": "email", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + true, + null, + true, + false, + false, + true, + true + ] + }, + "hash": "c7ee7ce64686cef41cebd99ad7ef31572fc1bf12e6ae473fd58fafb025989965" +} diff --git a/backend/.sqlx/query-d7a0f19f9e18d2ea49316012375ad78b69292ba091d69880945e42bebe890d66.json b/backend/.sqlx/query-d7a0f19f9e18d2ea49316012375ad78b69292ba091d69880945e42bebe890d66.json new file mode 100644 index 0000000000..f619796a27 --- /dev/null +++ b/backend/.sqlx/query-d7a0f19f9e18d2ea49316012375ad78b69292ba091d69880945e42bebe890d66.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM token WHERE label LIKE 'webhook-%' AND workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "d7a0f19f9e18d2ea49316012375ad78b69292ba091d69880945e42bebe890d66" +} diff --git a/backend/.sqlx/query-eff32aeac25a75d06f73e08c26dd3fd25f6b85cbea870505751c6a82457ae1da.json b/backend/.sqlx/query-eff32aeac25a75d06f73e08c26dd3fd25f6b85cbea870505751c6a82457ae1da.json new file mode 100644 index 0000000000..2190ea06eb --- /dev/null +++ b/backend/.sqlx/query-eff32aeac25a75d06f73e08c26dd3fd25f6b85cbea870505751c6a82457ae1da.json @@ -0,0 +1,59 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at, scopes, email FROM token WHERE workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "label", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "token_prefix", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "expiration", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "last_used_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "scopes", + "type_info": "TextArray" + }, + { + "ordinal": 6, + "name": "email", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + true, + null, + true, + false, + false, + true, + true + ] + }, + "hash": "eff32aeac25a75d06f73e08c26dd3fd25f6b85cbea870505751c6a82457ae1da" +} diff --git a/backend/.sqlx/query-f06e0e4fa358b26792df22fff48b71a6fcfa1e5603ea472892917c1accd1aafb.json b/backend/.sqlx/query-f06e0e4fa358b26792df22fff48b71a6fcfa1e5603ea472892917c1accd1aafb.json new file mode 100644 index 0000000000..71cdfe60bf --- /dev/null +++ b/backend/.sqlx/query-f06e0e4fa358b26792df22fff48b71a6fcfa1e5603ea472892917c1accd1aafb.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT COUNT(*) FROM token WHERE label LIKE 'email-%' AND workspace_id = $1 AND scopes @> ARRAY['run:script/' || $2]::text[]", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "f06e0e4fa358b26792df22fff48b71a6fcfa1e5603ea472892917c1accd1aafb" +} diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 67a02029cf..35232c5f48 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -4020,6 +4020,42 @@ paths: schema: $ref: "#/components/schemas/Script" + /w/{workspace}/scripts/get_triggers_count/{path}: + get: + summary: get triggers count of script + operationId: getTriggersCountOfScript + tags: + - script + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/ScriptPath" + responses: + "200": + description: triggers count + content: + application/json: + schema: + $ref: "#/components/schemas/TriggersCount" + + /w/{workspace}/scripts/list_tokens/{path}: + get: + summary: get tokens with script scope + operationId: listTokensOfScript + tags: + - script + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/ScriptPath" + responses: + "200": + description: tokens list + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/TruncatedToken" + /w/{workspace}/scripts/get/draft/{path}: get: summary: get script by path with draft @@ -4624,6 +4660,43 @@ paths: schema: $ref: "#/components/schemas/Flow" + /w/{workspace}/flows/get_triggers_count/{path}: + get: + summary: get triggers count of flow + operationId: getTriggersCountOfFlow + tags: + - flow + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/ScriptPath" + responses: + "200": + description: triggers count + content: + application/json: + schema: + $ref: "#/components/schemas/TriggersCount" + + /w/{workspace}/flows/list_tokens/{path}: + get: + summary: get tokens with flow scope + operationId: listTokensOfFlow + tags: + - flow + parameters: + - $ref: "#/components/parameters/WorkspaceId" + - $ref: "#/components/parameters/ScriptPath" + responses: + "200": + description: tokens list + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/TruncatedToken" + + /w/{workspace}/flows/toggle_workspace_error_handler/{path}: post: summary: Toggle ON and OFF the workspace error handler for a given flow @@ -10254,6 +10327,8 @@ components: type: array items: type: string + email: + type: string required: - token_prefix - created_at @@ -10271,6 +10346,8 @@ components: type: array items: type: string + workspace_id: + type: string NewTokenImpersonate: type: object @@ -10282,6 +10359,8 @@ components: format: date-time impersonate_email: type: string + workspace_id: + type: string required: - impersonate_email @@ -11118,6 +11197,23 @@ components: - requires_auth - http_method + TriggersCount: + type: object + properties: + primary_schedule: + type: object + properties: + schedule: + type: string + schedule_count: + type: number + http_routes_count: + type: number + webhook_count: + type: number + email_count: + type: number + Group: type: object properties: diff --git a/backend/windmill-api/src/flows.rs b/backend/windmill-api/src/flows.rs index ca9f9944db..de183ae6eb 100644 --- a/backend/windmill-api/src/flows.rs +++ b/backend/windmill-api/src/flows.rs @@ -9,6 +9,9 @@ use std::collections::HashMap; use crate::db::ApiAuthed; +use crate::triggers::{ + get_triggers_count_internal, list_tokens_internal, TriggersCount, TruncatedTokenWithEmail, +}; use crate::utils::WithStarredInfoQuery; use crate::{ db::DB, @@ -53,6 +56,8 @@ pub fn workspaced_service() -> Router { .route("/update/*path", post(update_flow)) .route("/archive/*path", post(archive_flow_by_path)) .route("/delete/*path", delete(delete_flow_by_path)) + .route("/get_triggers_count/*path", get(get_triggers_count)) + .route("/list_tokens/*path", get(list_tokens)) .route("/get/*path", get(get_flow_by_path)) .route("/get/draft/*path", get(get_flow_by_path_w_draft)) .route("/exists/*path", get(exists_flow_by_path)) @@ -874,6 +879,22 @@ async fn update_flow( Ok(nf.path.to_string()) } +async fn get_triggers_count( + Extension(db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, +) -> JsonResult { + let path = path.to_path(); + get_triggers_count_internal(&db, &w_id, &path, true).await +} + +async fn list_tokens( + Extension(db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, +) -> JsonResult> { + let path = path.to_path(); + list_tokens_internal(&db, &w_id, &path, true).await +} + async fn get_flow_by_path( authed: ApiAuthed, Extension(user_db): Extension, diff --git a/backend/windmill-api/src/lib.rs b/backend/windmill-api/src/lib.rs index c73972529a..78108a61a4 100644 --- a/backend/windmill-api/src/lib.rs +++ b/backend/windmill-api/src/lib.rs @@ -82,6 +82,7 @@ pub mod smtp_server_ee; mod static_assets; mod stripe_ee; mod tracing_init; +mod triggers; mod users; mod utils; mod variables; diff --git a/backend/windmill-api/src/scripts.rs b/backend/windmill-api/src/scripts.rs index 112a601901..63a5f49f92 100644 --- a/backend/windmill-api/src/scripts.rs +++ b/backend/windmill-api/src/scripts.rs @@ -9,6 +9,9 @@ use crate::{ db::{ApiAuthed, DB}, schedule::clear_schedule, + triggers::{ + get_triggers_count_internal, list_tokens_internal, TriggersCount, TruncatedTokenWithEmail, + }, users::{maybe_refresh_folders, require_owner_of_path, AuthCache}, utils::WithStarredInfoQuery, webhook_util::{WebhookMessage, WebhookShared}, @@ -132,6 +135,8 @@ pub fn workspaced_service() -> Router { .route("/archive/p/*path", post(archive_script_by_path)) .route("/get/draft/*path", get(get_script_by_path_w_draft)) .route("/get/p/*path", get(get_script_by_path)) + .route("/get_triggers_count/*path", get(get_triggers_count)) + .route("/list_tokens/*path", get(list_tokens)) .route("/raw/p/*path", get(raw_script_by_path)) .route("/raw_unpinned/p/*path", get(raw_script_by_path_unpinned)) .route("/exists/p/*path", get(exists_script_by_path)) @@ -874,6 +879,22 @@ async fn get_script_by_path( Ok(Json(script)) } +async fn list_tokens( + Extension(db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, +) -> JsonResult> { + let path = path.to_path(); + list_tokens_internal(&db, &w_id, &path, false).await +} + +async fn get_triggers_count( + Extension(db): Extension, + Path((w_id, path)): Path<(String, StripPath)>, +) -> JsonResult { + let path = path.to_path(); + get_triggers_count_internal(&db, &w_id, &path, false).await +} + async fn get_script_by_path_w_draft( authed: ApiAuthed, Extension(user_db): Extension, diff --git a/backend/windmill-api/src/triggers.rs b/backend/windmill-api/src/triggers.rs new file mode 100644 index 0000000000..dbc5306027 --- /dev/null +++ b/backend/windmill-api/src/triggers.rs @@ -0,0 +1,131 @@ +use axum::Json; +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; +use windmill_common::error::JsonResult; + +use crate::db::DB; + +#[derive(Serialize, Deserialize, Debug)] +pub struct TriggerPrimarySchedule { + schedule: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct TriggersCount { + primary_schedule: Option, + schedule_count: i64, + http_routes_count: i64, + webhook_count: i64, + email_count: i64, +} +pub(crate) async fn get_triggers_count_internal( + db: &DB, + w_id: &str, + path: &str, + is_flow: bool, +) -> JsonResult { + let primary_schedule = sqlx::query_scalar!( + "SELECT schedule FROM schedule WHERE path = $1 AND script_path = $1 AND is_flow = $2 AND workspace_id = $3", + path, + is_flow, + w_id + ) + .fetch_optional(db) + .await?; + + let schedule_count = sqlx::query_scalar!( + "SELECT COUNT(*) FROM schedule WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3", + path, + is_flow, + w_id + ) + .fetch_one(db) + .await? + .unwrap_or(0); + + let http_routes_count = sqlx::query_scalar!( + "SELECT COUNT(*) FROM http_trigger WHERE script_path = $1 AND is_flow = $2 AND workspace_id = $3", + path, + is_flow, + w_id + ) + .fetch_one(db) + .await? + .unwrap_or(0); + + let webhook_count = (if is_flow { + sqlx::query_scalar!( + "SELECT COUNT(*) FROM token WHERE label LIKE 'webhook-%' AND workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]", + w_id, + path, + ) + + } else { + sqlx::query_scalar!( + "SELECT COUNT(*) FROM token WHERE label LIKE 'webhook-%' AND workspace_id = $1 AND scopes @> ARRAY['run:' || $2]::text[]", + w_id, + path, + ) + }).fetch_one(db) + .await? + .unwrap_or(0); + + let email_count = (if is_flow { + sqlx::query_scalar!( + "SELECT COUNT(*) FROM token WHERE label LIKE 'email-%' AND workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]", + w_id, + path, + ) + + } else { + sqlx::query_scalar!( + "SELECT COUNT(*) FROM token WHERE label LIKE 'email-%' AND workspace_id = $1 AND scopes @> ARRAY['run:script/' || $2]::text[]", + w_id, + path, + ) + }).fetch_one(db) + .await? + .unwrap_or(0); + + Ok(Json(TriggersCount { + primary_schedule: primary_schedule.map(|s| TriggerPrimarySchedule { schedule: s }), + schedule_count, + http_routes_count, + webhook_count, + email_count, + })) +} + +#[derive(FromRow, Serialize)] +pub struct TruncatedTokenWithEmail { + pub label: Option, + pub token_prefix: Option, + pub expiration: Option>, + pub created_at: chrono::DateTime, + pub last_used_at: chrono::DateTime, + pub scopes: Option>, + pub email: Option, +} + +pub async fn list_tokens_internal( + db: &DB, + w_id: &str, + path: &str, + is_flow: bool, +) -> JsonResult> { + let tokens = if is_flow { + sqlx::query_as!( + TruncatedTokenWithEmail, + "SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at, scopes, email FROM token WHERE workspace_id = $1 AND scopes @> ARRAY['run:flow/' || $2]::text[]", + w_id, path).fetch_all(db) + .await? + } else { + sqlx::query_as!( + TruncatedTokenWithEmail, + "SELECT label, concat(substring(token for 10)) as token_prefix, expiration, created_at, last_used_at, scopes, email FROM token WHERE workspace_id = $1 AND scopes @> ARRAY['run:script/' || $2]::text[]", + w_id, path) + .fetch_all(db) + .await? + }; + Ok(Json(tokens)) +} diff --git a/backend/windmill-api/src/users.rs b/backend/windmill-api/src/users.rs index 596e1f1d20..4c5ea36eba 100644 --- a/backend/windmill-api/src/users.rs +++ b/backend/windmill-api/src/users.rs @@ -128,7 +128,13 @@ pub fn make_unauthed_service() -> Router { fn username_override_from_label(label: Option) -> Option { match label { - Some(label) if label.starts_with("webhook-") => Some(label), + Some(label) + if label.starts_with("webhook-") + || label.starts_with("http-") + || label.starts_with("email-") => + { + Some(label) + } Some(label) if label.starts_with("ephemeral-script-end-user-") => Some( label .trim_start_matches("ephemeral-script-end-user-") @@ -270,9 +276,10 @@ impl AuthCache { _ => { let user_o = sqlx::query_as::<_, (Option, Option, bool, Option>, Option)>( "UPDATE token SET last_used_at = now() WHERE token = $1 AND (expiration > NOW() \ - OR expiration IS NULL) RETURNING owner, email, super_admin, scopes, label", + OR expiration IS NULL) AND (workspace_id IS NULL OR workspace_id = $2) RETURNING owner, email, super_admin, scopes, label", ) .bind(token) + .bind(w_id.as_ref()) .fetch_optional(&self.db) .await .ok() @@ -837,6 +844,7 @@ pub struct NewToken { pub expiration: Option>, pub impersonate_email: Option, pub scopes: Option>, + pub workspace_id: Option, } #[derive(Deserialize)] @@ -2389,14 +2397,15 @@ async fn create_token( .unwrap_or(false); sqlx::query!( "INSERT INTO token - (token, email, label, expiration, super_admin, scopes) - VALUES ($1, $2, $3, $4, $5, $6)", + (token, email, label, expiration, super_admin, scopes, workspace_id) + VALUES ($1, $2, $3, $4, $5, $6, $7)", token, authed.email, new_token.label, new_token.expiration, is_super_admin, - new_token.scopes.as_ref().map(|x| x.as_slice()) + new_token.scopes.as_ref().map(|x| x.as_slice()), + new_token.workspace_id, ) .execute(&mut *tx) .await?; diff --git a/frontend/openapi-ts-error-1726231138297.log b/frontend/openapi-ts-error-1726231138297.log deleted file mode 100644 index f78fbf5b1c..0000000000 --- a/frontend/openapi-ts-error-1726231138297.log +++ /dev/null @@ -1,28 +0,0 @@ -Error parsing /git/windmill/backend/windmill-api/openapi.yaml: duplicated mapping key (8350:3) - - 8347 | application/json: - 8348 | schema: {} - 8349 | - 8350 | /w/{workspace}/job_helpers/loa ... -----------^ - 8351 | get: - 8352 | summary: Load a preview of ... -ParserError: Error parsing /git/windmill/backend/windmill-api/openapi.yaml: duplicated mapping key (8350:3) - - 8347 | application/json: - 8348 | schema: {} - 8349 | - 8350 | /w/{workspace}/job_helpers/loa ... -----------^ - 8351 | get: - 8352 | summary: Load a preview of ... - at Object.parse (/git/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/parsers/yaml.js:44:23) - at getResult (/git/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/util/plugins.js:116:22) - at runNextPlugin (/git/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/util/plugins.js:64:32) - at /git/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/util/plugins.js:55:9 - at new Promise () - at Object.run (/git/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/util/plugins.js:54:12) - at parseFile (/git/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/parse.js:130:38) - at parse (/git/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/parse.js:56:30) - at async $RefParser.parse (/git/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/index.js:115:28) - at async $RefParser.resolve (/git/windmill/frontend/node_modules/@apidevtools/json-schema-ref-parser/dist/lib/index.js:145:13) \ No newline at end of file diff --git a/frontend/src/lib/components/AssignableTags.svelte b/frontend/src/lib/components/AssignableTags.svelte index a09aaab4db..ce58c61d67 100644 --- a/frontend/src/lib/components/AssignableTags.svelte +++ b/frontend/src/lib/components/AssignableTags.svelte @@ -6,6 +6,7 @@ import AssignableTagsInner from './AssignableTagsInner.svelte' export let placement: 'bottom-end' | 'top-end' = 'bottom-end' + export let color: 'nord' | 'dark' = 'dark' - + + + + + + {#if Object.keys(schedule?.args ?? {}).length > 0} +
+ +
+ {:else} +
No arguments
+ {/if} + diff --git a/frontend/src/lib/components/RunPageSchedules.svelte b/frontend/src/lib/components/RunPageSchedules.svelte index 5ca250f3cc..163ca4c0ed 100644 --- a/frontend/src/lib/components/RunPageSchedules.svelte +++ b/frontend/src/lib/components/RunPageSchedules.svelte @@ -1,178 +1,294 @@ { - loadSchedule() - loadSchedules() + loadSchedules(true) }} bind:this={scheduleEditor} /> -
- -
+
+ {#if $primarySchedule == undefined} + + {:else if $primarySchedule} +
+ {#if can_write} +
+
+ + {:else} +
Deployed automatically with {isFlow ? 'flow' : 'script'}
+ {/if}
- - -
-
- {#if Object.keys(schedule?.args ?? {}).length > 0} -
- -
- {:else} -
No arguments
- {/if} -
-{:else if schedule == undefined} - -{/if} - -

Other schedules

- -{#if schedules} - {#if schedules.length == 0} -
No other schedules
- {:else} -
- {#each schedules as schedule (schedule.path)} -
{schedule.path}
{schedule.schedule}
-
{schedule.enabled ? 'on' : 'off'}
-
- {/each} + {/if} + + +
+ + + {#if emptyString($primarySchedule.cron)} +

Define a schedule frequency first

+ {/if} + + {#if initialPrimarySchedule != false} +
+ +
+ {/if} + {:else} +
+ + {#if initialPrimarySchedule != false && !newItem} + + {:else} +
Deployed automatically with {isFlow ? 'flow' : 'script'}
+ {/if} +
+ + + + {/if} -{:else} - -{/if} + + {#if !newItem} +
+ {#if $primarySchedule} + + {/if} + + + {/if} +
diff --git a/frontend/src/lib/components/ScheduleEditorInner.svelte b/frontend/src/lib/components/ScheduleEditorInner.svelte index 3980301d27..8622fffea3 100644 --- a/frontend/src/lib/components/ScheduleEditorInner.svelte +++ b/frontend/src/lib/components/ScheduleEditorInner.svelte @@ -520,13 +520,14 @@ { await ScheduleService.setScheduleEnabled({ path: initialPath, workspace: $workspaceStore ?? '', requestBody: { enabled: e.detail } }) + dispatch('update') sendUserToast(`${e.detail ? 'enabled' : 'disabled'} schedule ${initialPath}`) }} /> diff --git a/frontend/src/lib/components/SchemaViewer.svelte b/frontend/src/lib/components/SchemaViewer.svelte index 4239525fd9..cd9f43d398 100644 --- a/frontend/src/lib/components/SchemaViewer.svelte +++ b/frontend/src/lib/components/SchemaViewer.svelte @@ -1,6 +1,6 @@ - -
- - -
- - -
- -{#if emptyString($schedule.cron)} -

Define a schedule frequency first

-{/if} -
- - - Changes to the primary schedule are only applied upon deploy. Other schedules' changes are applied - immediately. - - -{#if initialPath != ''} - { - loadSchedules() - }} - bind:this={scheduleEditor} - /> -

Other schedules

-
- -
- - {#if schedules} - {#if schedules.length == 0} -
No other schedules
- {:else} -
- {#each schedules as schedule (schedule.path)} -
{schedule.path}
{schedule.schedule}
-
{schedule.enabled ? 'on' : 'off'}
- -
- {/each} -
- {/if} - {:else} - - {/if} -{/if} diff --git a/frontend/src/lib/components/Section.svelte b/frontend/src/lib/components/Section.svelte index 8c2379e982..43868d170e 100644 --- a/frontend/src/lib/components/Section.svelte +++ b/frontend/src/lib/components/Section.svelte @@ -48,6 +48,9 @@ {/if} + {#if collapsable && collapsed} + + {/if}
diff --git a/frontend/src/lib/components/Toast.svelte b/frontend/src/lib/components/Toast.svelte index 3b1382ccf6..c57a04ac48 100644 --- a/frontend/src/lib/components/Toast.svelte +++ b/frontend/src/lib/components/Toast.svelte @@ -4,6 +4,7 @@ import { onMount } from 'svelte' import Button from './common/button/Button.svelte' import type { ToastAction } from '$lib/toast' + import { processMessage } from './toast' export let message: string export let toastId: string @@ -37,7 +38,7 @@ {/if}
-

{message}

+

{@html processMessage(message)}

{#if errorMessage}

+{#if eeOnly && disabled} + + + EE only Enterprise Edition only feature + +{/if} diff --git a/frontend/src/lib/components/UserSettings.svelte b/frontend/src/lib/components/UserSettings.svelte index e7fc2f9cdf..163e558668 100644 --- a/frontend/src/lib/components/UserSettings.svelte +++ b/frontend/src/lib/components/UserSettings.svelte @@ -25,6 +25,7 @@ export let scopes: string[] | undefined = undefined export let newTokenLabel: string | undefined = undefined + export let newTokenWorkspace: string | undefined = undefined export let newToken: string | undefined = undefined let newPassword: string | undefined @@ -79,7 +80,12 @@ date.setDate(date.getDate() + newTokenExpiration) } newToken = await UserService.createToken({ - requestBody: { label: newTokenLabel, expiration: date?.toISOString(), scopes } as NewToken + requestBody: { + label: newTokenLabel, + expiration: date?.toISOString(), + scopes, + workspace_id: newTokenWorkspace + } as NewToken }) dispatch('tokenCreated', newToken) listTokens() diff --git a/frontend/src/lib/components/WorkerTagPicker.svelte b/frontend/src/lib/components/WorkerTagPicker.svelte index a00278b780..bab122a6c1 100644 --- a/frontend/src/lib/components/WorkerTagPicker.svelte +++ b/frontend/src/lib/components/WorkerTagPicker.svelte @@ -71,5 +71,5 @@ }} startIcon={{ icon: RotateCw }} /> - +
diff --git a/frontend/src/lib/components/apps/editor/AppExportButton.svelte b/frontend/src/lib/components/apps/editor/AppExportButton.svelte index f41507fa16..abc0136b07 100644 --- a/frontend/src/lib/components/apps/editor/AppExportButton.svelte +++ b/frontend/src/lib/components/apps/editor/AppExportButton.svelte @@ -49,9 +49,8 @@ size="sm" startIcon={{ icon: Clipboard }} btnClasses="absolute top-2 right-2 w-min z-20" - > - Copy content - + iconOnly + /> {#key rawType}
{ e.preventDefault() copyToClipboard(content) }} > -
{content}
- +
{content}
+
diff --git a/frontend/src/lib/components/details/DetailPageDetailPanel.svelte b/frontend/src/lib/components/details/DetailPageDetailPanel.svelte index c2df3d8bd9..4ce13fca2f 100644 --- a/frontend/src/lib/components/details/DetailPageDetailPanel.svelte +++ b/frontend/src/lib/components/details/DetailPageDetailPanel.svelte @@ -1,25 +1,17 @@ @@ -27,79 +19,38 @@ - Saved inputs + Saved Inputs {#if !isOperator} - Details & Triggers + Triggers {/if} {#if flow_json} - Raw + Export {/if} - {#if hasStepDetails} + {#if selected == 'flow_step'} Step {/if} + {#if !flow_json && !isOperator} + Schema + {/if} +
- - - - - - - - - - - Webhooks - - - - - - Schedules - - - - - - HTTP - - - - - - Email - - - - - - CLI - - - - -
-
- {#if triggerSelected === 'webhooks'} - - {:else if triggerSelected === 'routes'} - - {:else if triggerSelected === 'email'} - - {:else if triggerSelected === 'schedule'} - - {:else if triggerSelected === 'cli'} - - {/if} -
-
-
-
+ + - + + + + + + + + + + diff --git a/frontend/src/lib/components/details/DetailPageHeader.svelte b/frontend/src/lib/components/details/DetailPageHeader.svelte index 3b8885740a..6a3012946b 100644 --- a/frontend/src/lib/components/details/DetailPageHeader.svelte +++ b/frontend/src/lib/components/details/DetailPageHeader.svelte @@ -7,6 +7,9 @@ import ErrorHandlerToggleButton from './ErrorHandlerToggleButton.svelte' import { twMerge } from 'tailwind-merge' import { userStore } from '$lib/stores' + import { createEventDispatcher, getContext } from 'svelte' + import type { TriggerContext } from '../triggers' + import { Calendar } from 'lucide-svelte' type MainButton = { label: string @@ -22,6 +25,8 @@ color?: 'red' } + const { triggersCount, selectedTrigger } = getContext('TriggerContext') + export let mainButtons: MainButton[] = [] export let menuItems: MenuItemButton[] = [] export let title: string @@ -30,6 +35,8 @@ export let errorHandlerKind: 'flow' | 'script' export let scriptOrFlowPath: string export let errorHandlerMuted: boolean | undefined + + const dispatch = createEventDispatcher()
@@ -47,6 +54,22 @@ tag: {tag} {/if} + {#if $triggersCount?.primary_schedule} + + {/if} +
{#if menuItems.length > 0} diff --git a/frontend/src/lib/components/details/DetailPageLayout.svelte b/frontend/src/lib/components/details/DetailPageLayout.svelte index 2ed73d06c5..60022e4e62 100644 --- a/frontend/src/lib/components/details/DetailPageLayout.svelte +++ b/frontend/src/lib/components/details/DetailPageLayout.svelte @@ -3,16 +3,31 @@ import SplitPanesWrapper from '$lib/components/splitPanes/SplitPanesWrapper.svelte' import { Pane, Splitpanes } from 'svelte-splitpanes' import DetailPageDetailPanel from './DetailPageDetailPanel.svelte' + import type { ScheduleTrigger, TriggerContext } from '../triggers' + import { setContext } from 'svelte' + import { writable, type Writable } from 'svelte/store' + import type { TriggersCount } from '$lib/gen' + import DetailPageTriggerPanel from './DetailPageTriggerPanel.svelte' export let isOperator: boolean = false export let flow_json: any | undefined = undefined - export let hasStepDetails: boolean = false export let selected: string - export let triggerSelected: 'webhooks' | 'schedule' | 'cli' = 'webhooks' + export let triggersCount: Writable let mobileTab: 'form' | 'detail' = 'form' let clientWidth = window.innerWidth + + const primaryScheduleStore = writable(undefined) + const selectedTriggerStore = writable<'webhooks' | 'emails' | 'schedules' | 'cli' | 'routes'>( + 'webhooks' + ) + + setContext('TriggerContext', { + selectedTrigger: selectedTriggerStore, + primarySchedule: primaryScheduleStore, + triggersCount + })
@@ -26,20 +41,20 @@ - - + + + @@ -50,23 +65,26 @@ Run form - Details + Saved Inputs + {#if !isOperator} + Triggers + {/if} - - + + + + + - - + + - - - - + diff --git a/frontend/src/lib/components/details/DetailPageTriggerPanel.svelte b/frontend/src/lib/components/details/DetailPageTriggerPanel.svelte new file mode 100644 index 0000000000..cbf1e704b0 --- /dev/null +++ b/frontend/src/lib/components/details/DetailPageTriggerPanel.svelte @@ -0,0 +1,59 @@ + + + + + + + + + Webhooks + + + + + + Schedules + + + + + + HTTP + + + + + + Email + + + + + + CLI + + + + +
+
+ {#if triggerSelected === 'webhooks'} + + {:else if triggerSelected === 'routes'} + + {:else if triggerSelected === 'emails'} + + {:else if triggerSelected === 'schedules'} + + {:else if triggerSelected === 'cli'} + + {/if} +
+
diff --git a/frontend/src/lib/components/details/EmailTriggerPanel.svelte b/frontend/src/lib/components/details/EmailTriggerPanel.svelte index fb93e0fc1c..4832dee7f3 100644 --- a/frontend/src/lib/components/details/EmailTriggerPanel.svelte +++ b/frontend/src/lib/components/details/EmailTriggerPanel.svelte @@ -14,7 +14,8 @@ import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte' import { AlertTriangle } from 'lucide-svelte' import Skeleton from '../common/skeleton/Skeleton.svelte' - + import Label from '$lib/components/Label.svelte' + import TriggerTokens from '../triggers/TriggerTokens.svelte' let userSettings: UserSettings export let token: string @@ -33,6 +34,7 @@ })) as any) ?? null loading = false } + getEmailDomain() let requestType: 'hash' | 'path' = 'path' @@ -49,6 +51,12 @@ .toLowerCase() return `${pathOrHash}+${encodedPrefix}@${emailDomain}` } + + export let email: string = '' + + $: email = emailAddress() + + let triggerTokens: TriggerTokens | undefined = undefined @@ -57,18 +65,20 @@ bind:this={userSettings} on:tokenCreated={(e) => { token = e.detail + triggerTokens?.listTokens() }} + newTokenWorkspace={$workspaceStore} newTokenLabel={`${$userStore?.username ?? 'superadmin'}-${generateRandomString(4)}`} {scopes} /> -
+
{#if loading} {:else} {#if emailDomain} {#if SCRIPT_VIEW_SHOW_CREATE_TOKEN_BUTTON} -
+ {/if} {#if !isFlow} @@ -103,20 +113,19 @@
{/if} -
- {#key requestType} - {#key token} -
- -
- {/key} + + {#key requestType} + {#key token} + {/key} - - To trigger the job by email, send an email to the address above. The job will receive two - arguments: `raw_email` containing the raw email as string, and `parsed_email` containing - the parsed email as an object. - -
+ {/key} + + To trigger the job by email, send an email to the address above. The job will receive two + arguments: `raw_email` containing the raw email as string, and `parsed_email` containing the + parsed email as an object. + {:else}
@@ -133,5 +142,7 @@ Email triggers on Windmill Community Edition are limited to 100 emails per day. {/if} + + {/if}
diff --git a/frontend/src/lib/components/details/ErrorHandlerToggleButtonV2.svelte b/frontend/src/lib/components/details/ErrorHandlerToggleButtonV2.svelte new file mode 100644 index 0000000000..18d1bd5591 --- /dev/null +++ b/frontend/src/lib/components/details/ErrorHandlerToggleButtonV2.svelte @@ -0,0 +1,67 @@ + + + diff --git a/frontend/src/lib/components/flows/FlowEditor.svelte b/frontend/src/lib/components/flows/FlowEditor.svelte index ef951a2910..01fb16db41 100644 --- a/frontend/src/lib/components/flows/FlowEditor.svelte +++ b/frontend/src/lib/components/flows/FlowEditor.svelte @@ -18,6 +18,7 @@ export let disableSettings = false export let disabledFlowInputs = false export let smallErrorHandler = false + export let newFlow: boolean = false let size = 50 @@ -48,6 +49,7 @@ {disableAi} {disableSettings} {smallErrorHandler} + {newFlow} bind:modules={$flowStore.value.modules} on:reload /> @@ -62,7 +64,7 @@
{:else} - + {/if} diff --git a/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte b/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte index d7a870ea05..4993afa0f9 100644 --- a/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte +++ b/frontend/src/lib/components/flows/content/FlowEditorPanel.svelte @@ -7,6 +7,7 @@ import FlowInput from './FlowInput.svelte' import FlowFailureModule from './FlowFailureModule.svelte' import FlowConstants from './FlowConstants.svelte' + import TriggersEditor from '../../triggers/TriggersEditor.svelte' import type { FlowModule } from '$lib/gen' import { initFlowStepWarnings } from '../utils' import { dfs } from '../dfs' @@ -14,9 +15,10 @@ export let noEditor = false export let enableAi = false + export let newFlow = false export let disabledFlowInputs = false - const { selectedId, flowStore, flowStateStore, flowInputsStore } = + const { selectedId, flowStore, flowStateStore, flowInputsStore, pathStore, initialPath } = getContext('FlowEditorContext') function checkDup(modules: FlowModule[]): string | undefined { @@ -67,6 +69,15 @@ {:else if $selectedId === 'preprocessor'} +{:else if $selectedId === 'triggers'} + {:else} {@const dup = checkDup($flowStore.value.modules)} {#if dup} diff --git a/frontend/src/lib/components/flows/content/FlowModuleWrapper.svelte b/frontend/src/lib/components/flows/content/FlowModuleWrapper.svelte index 53caf04537..a52370ea87 100644 --- a/frontend/src/lib/components/flows/content/FlowModuleWrapper.svelte +++ b/frontend/src/lib/components/flows/content/FlowModuleWrapper.svelte @@ -20,14 +20,17 @@ import FlowWhileLoop from './FlowWhileLoop.svelte' import { initFlowStepWarnings } from '../utils' import { dfs } from '../dfs' + import type { TriggerContext } from '$lib/components/triggers' export let flowModule: FlowModule export let noEditor: boolean = false export let enableAi = false - const { selectedId, schedule, flowStateStore, flowInputsStore, flowStore } = + const { selectedId, flowStateStore, flowInputsStore, flowStore } = getContext('FlowEditorContext') + const { primarySchedule } = getContext('TriggerContext') + let scriptKind: 'script' | 'trigger' | 'approval' = 'script' let scriptTemplate: 'pgsql' | 'mysql' | 'script' | 'docker' | 'powershell' = 'script' @@ -37,6 +40,26 @@ // Pointer to previous module, for easy access to testing results export let previousModule: FlowModule | undefined = undefined + function initializePrimaryScheduleForTriggerScript(module: FlowModule) { + if (!$primarySchedule) { + $primarySchedule = { + summary: 'Scheduled poll of flow', + args: {}, + cron: '0 */15 * * *', + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + enabled: true + } + } + if (!$primarySchedule.cron) { + $primarySchedule.cron = '0 */15 * * *' + } + $primarySchedule.enabled = true + + module.stop_after_if = { + expr: 'result == undefined || Array.isArray(result) && result.length == 0', + skip_if_stopped: true + } + } async function createModuleFromScript( path: string, summary: string, @@ -50,15 +73,7 @@ } if (kind == 'trigger') { - if (!$schedule.cron) { - $schedule.cron = '0 */15 * * *' - } - $schedule.enabled = true - - module.stop_after_if = { - expr: 'result == undefined || Array.isArray(result) && result.length == 0', - skip_if_stopped: true - } + initializePrimaryScheduleForTriggerScript(module) } flowModule = module @@ -140,15 +155,7 @@ scriptTemplate = subkind if (kind == 'trigger') { - if (!$schedule.cron) { - $schedule.cron = '0 */15 * * *' - } - $schedule.enabled = true - - module.stop_after_if = { - expr: 'result == undefined || Array.isArray(result) && result.length == 0', - skip_if_stopped: true - } + initializePrimaryScheduleForTriggerScript(module) } if (kind == 'approval') { diff --git a/frontend/src/lib/components/flows/content/FlowSchedules.svelte b/frontend/src/lib/components/flows/content/FlowSchedules.svelte deleted file mode 100644 index 14f98a4e47..0000000000 --- a/frontend/src/lib/components/flows/content/FlowSchedules.svelte +++ /dev/null @@ -1,106 +0,0 @@ - - -
- - -
- - -
- -{#if emptyString($schedule.cron)} -

Define a schedule frequency first

-{/if} -
- - - Changes to the primary schedule are only applied upon deploy. Other schedules' changes are applied - immediately. - - -{#if initialPath != ''} - { - loadSchedules() - }} - bind:this={scheduleEditor} - /> -

Other schedules

-
- -
- - {#if schedules} - {#if schedules.length == 0} -
No other schedules
- {:else} -
- {#each schedules as schedule (schedule.path)} -
{schedule.path}
{schedule.schedule}
-
{schedule.enabled ? 'on' : 'off'}
- -
- {/each} -
- {/if} - {:else} - - {/if} -{/if} diff --git a/frontend/src/lib/components/flows/content/FlowSettings.svelte b/frontend/src/lib/components/flows/content/FlowSettings.svelte index 0cf95cf676..c9bdcac771 100644 --- a/frontend/src/lib/components/flows/content/FlowSettings.svelte +++ b/frontend/src/lib/components/flows/content/FlowSettings.svelte @@ -1,348 +1,195 @@ -
+
-
- - - Metadata - {#if !noEditor && customUi?.settingsTabs?.schedule != false} - Schedule - {/if} - {#if customUi?.settingsTabs?.sharedDiretory != false} - - Shared Directory - - {/if} - {#if customUi?.settingsTabs?.earlyStop != false} - - Early Stop - - {/if} - {#if customUi?.settingsTabs?.earlyReturn != false} - - Early Return - +
+ +
+ + + {#if !noEditor} + {/if} + + +
+ + +
+ {#if customUi?.settingsTabs?.workerGroup != false} - Worker Group - {/if} - {#if customUi?.settingsTabs?.concurrency != false} - Concurrency - {/if} - {#if customUi?.settingsTabs?.cache != false} - Cache - {/if} - - -
-
- +
+ { + displayWorkerTagPicker = !displayWorkerTagPicker + if (!displayWorkerTagPicker) { + $flowStore.tag = undefined + } + }} + options={{ + right: 'Worker Group Tag (Queue)', + rightTooltip: + "When a worker group tag is defined at the flow level, any steps inside the flow will run on any worker group that listen to that tag, regardless of the steps tag. If no worker group tags is defined, the flow controls will be executed with the default tag 'flow' and the steps will be executed with their respective tag" + }} + class="py-1" + /> - {#if !noEditor} - - {/if} + {#if displayWorkerTagPicker} + + {/if} +
- + + {/if} - - 0} - on:change={() => { - if ($flowStore.value.priority) { - $flowStore.value.priority = undefined - } else { - $flowStore.value.priority = 100 - } - }} - options={{ - right: `Label as high priority`, - rightTooltip: `All jobs scheduled by flows labeled as high priority take precedence over the other jobs in the jobs queue. ${ - !$enterpriseLicense - ? 'This is a feature only available on enterprise edition.' - : '' - }` - }} + + + {#if numberOfAdvancedOptionsOn > 0} +
+ {#each activeAdvancedOptionNames as optionName} + {optionName} - - { - if ($flowStore.value.priority && $flowStore.value.priority > 100) { - $flowStore.value.priority = 100 - } else if ($flowStore.value.priority && $flowStore.value.priority < 0) { - $flowStore.value.priority = 0 - } - }} - /> - - + {/each} +
+ {/if} +
- { - if ($flowStore.visible_to_runner_only) { - $flowStore.visible_to_runner_only = undefined - } else { - $flowStore.visible_to_runner_only = true - } - }} - options={{ - right: 'Make runs invisible to others', - rightTooltip: - 'When this option is enabled, manual executions of this script are invisible to users other than the user running it, including the owner(s). This setting can be overridden when this script is run manually from the advanced menu.' - }} - /> - -
- -
- - -
- On-demand: - -
-
-
-
-
- - - Flows can be triggered by any schedules, their webhooks or their UI but they have only - one primary schedule with which they share the same path. The primary schedule can be - set here. - -
- - - - -
- - Steps will share a folder at `./shared` in which they can store heavier data and - pass them to the next step.

Beware that the `./shared` folder is not - preserved across suspends and sleeps.

- Furthermore, steps' worker groups is not respected and only the flow's worker group will - be respected. -
- -
-
- -
- - { - if ($flowStore.value.cache_ttl && $flowStore.value.cache_ttl != undefined) { - $flowStore.value.cache_ttl = undefined - } else { - $flowStore.value.cache_ttl = 300 - } - }} - options={{ - right: 'Cache the results for each possible inputs' - }} - /> - - -
-
How long to keep the cache valid
+ + {#if customUi?.settingsTabs?.cache != false} +
+ { + if ($flowStore.value.cache_ttl && $flowStore.value.cache_ttl != undefined) { + $flowStore.value.cache_ttl = undefined + } else { + $flowStore.value.cache_ttl = 300 + } + }} + options={{ + right: 'Cache the results for each possible inputs' + }} + class="py-1" + /> + {#if $flowStore.value.cache_ttl} +
+
How long to keep the cache valid
{#if $flowStore.value.cache_ttl} @@ -351,136 +198,87 @@ {/if}
-
-
- - - - When a worker group tag is defined at the flow level, any steps inside the flow will - run on any worker group that listen to that tag, regardless of the steps' tag. If no - worker group tags is defined, the flow controls will be executed with the default tag - 'flow' and the steps will be executed with their respective tag - - Worker Group Tag (Queue) - - -
- Dedicated Worker {#if !$enterpriseLicense}
- - EE only -
{/if}
+ {/if} +
+ {/if} + + {#if customUi?.settingsTabs?.earlyStop != false} +
+ { - if ($flowStore.dedicated_worker) { - $flowStore.dedicated_worker = undefined + if (Boolean($flowStore.value.skip_expr) && $flowStore.value.skip_expr) { + $flowStore.value.skip_expr = undefined } else { - $flowStore.dedicated_worker = true + $flowStore.value.skip_expr = 'flow_input.foo == undefined' } }} options={{ - right: 'Flow is run on dedicated workers' + right: 'Early stop if condition met', + rightTooltip: + 'If defined, at the beginning of the step the predicate expression will be evaluated' + + 'to decide if the flow should stop early.' }} + class="py-1" /> - {#if $flowStore.dedicated_worker} -
- - One worker in a worker group needs to be configured with dedicated worker set to:
{$workspaceStore}:flow/{$pathStore}
-
-
- {/if} - - In this mode, every scripts of this flow is run on the workers dedicated to this - flow that keep the scripts "hot" so that there is not cold start cost incurred. - Steps can run at >1500 rps in this mode. - - - -
- - - If defined, at the beginning of the step the predicate expression will be - evaluated to decide if the flow should stop early. - - - { - if (Boolean($flowStore.value.skip_expr) && $flowStore.value.skip_expr) { - $flowStore.value.skip_expr = undefined - } else { - $flowStore.value.skip_expr = 'flow_input.foo == undefined' - } - }} - options={{ - right: 'Early stop if condition met' - }} - /> - + {#if $flowStore.value.skip_expr}
- {#if $flowStore.value.skip_expr} -
- -
- You can use the variable `flow_input` to access the inputs of the flow.
The variable `WM_SCHEDULED_FOR` contains the time the flow was scheduled for - which you can use to stop early non fresh jobs: -
new Date().getTime() - new Date(WM_SCHEDULED_FOR).getTime() {'>'} X
-
+
+ +
+ You can use the variable `flow_input` to access the inputs of the flow.
The variable `WM_SCHEDULED_FOR` contains the time the flow was scheduled for + which you can use to stop early non fresh jobs: +
new Date().getTime() - new Date(WM_SCHEDULED_FOR).getTime() {'>'} X
- {:else} -