From 1ec8d63968dbee7039621e35ce53c3cfdf5c8339 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 --- 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 | 27 +- .../src/lib/components/FlowBuilder.svelte | 192 ++-- .../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 | 384 +++++--- .../lib/components/ScheduleEditorInner.svelte | 3 +- .../src/lib/components/SchemaViewer.svelte | 18 +- .../src/lib/components/ScriptBuilder.svelte | 171 ++-- .../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 +- 75 files changed, 2438 insertions(+), 1400 deletions(-) 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/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 67a02029cf11c..35232c5f48871 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 ca9f9944dbe77..de183ae6eb048 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 c73972529a895..78108a61a4352 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 112a601901780..63a5f49f92a98 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 0000000000000..dbc5306027702 --- /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 596e1f1d209da..4c5ea36eba975 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 f78fbf5b1c354..0000000000000 --- 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 a09aaab4db269..ce58c61d67087 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 5ca250f3ccb9e..163ca4c0ed5d2 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} + + + +
+ + + {#if emptyString($primarySchedule.cron)} +

Define a schedule frequency first

+ {/if} + + {#if initialPrimarySchedule != false} +
+
-
- { - if (can_write) { - setScheduleEnabled(path, e.detail) - } else { - sendUserToast('not enough permission', true) - } - }} - options={{ - right: 'On' - }} - size="xs" - /> - + {#if initialPrimarySchedule != false && !newItem} + - -
+ {/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} -{: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 3980301d275ee..8622fffea355f 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 4239525fd9db0..cd9f43d3980f2 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 8c2379e9821b7..43868d170e27b 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 3b1382ccf69a4..c57a04ac4851c 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 e7fc2f9cdf82b..163e55866825c 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 a00278b780426..bab122a6c1d15 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 f41507fa16f8e..abc0136b07362 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 c2df3d8bd9440..4ce13fca2fe4e 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 3b8885740a11e..6a3012946b461 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 2ed73d06c5fa8..60022e4e6273b 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 0000000000000..cbf1e704b028c --- /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 fb93e0fc1c93d..4832dee7f3701 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 0000000000000..18d1bd5591402 --- /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 ef951a291024e..01fb16db41e01 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 d7a870ea05c3e..4993afa0f90a7 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 53caf04537d7f..a52370ea87758 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 14f98a4e47325..0000000000000 --- 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 0cf95cf676f64..c9bdcac771ec8 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} - {#if customUi?.settingsTabs?.workerGroup != false} - Worker Group - {/if} - {#if customUi?.settingsTabs?.concurrency != false} - Concurrency - {/if} - {#if customUi?.settingsTabs?.cache != false} - Cache +
+ +
+ + + {#if !noEditor} + {/if} - - -
-
- - {#if !noEditor} - - {/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 ($flowStore.value.priority && $flowStore.value.priority > 100) { - $flowStore.value.priority = 100 - } else if ($flowStore.value.priority && $flowStore.value.priority < 0) { - $flowStore.value.priority = 0 - } - }} - /> - - - - { - 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.' - }} - /> + +
+ + {#if customUi?.settingsTabs?.workerGroup != false} +
+ { + 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 displayWorkerTagPicker} + + {/if} +
- -
- 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. - -
- - + + {/if} - -
- - 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' - }} - /> - + + + {#if numberOfAdvancedOptionsOn > 0} +
+ {#each activeAdvancedOptionNames as optionName} + {optionName} + {/each} +
+ {/if} +
-
-
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} -