diff --git a/frontend/src/lib/components/Dev.svelte b/frontend/src/lib/components/Dev.svelte index ca8ecb8451..6d9b21aeb9 100644 --- a/frontend/src/lib/components/Dev.svelte +++ b/frontend/src/lib/components/Dev.svelte @@ -662,6 +662,7 @@ const previewArgsStore = $state({ val: {} }) const scriptEditorDrawer = writable(undefined) + const workspaceScriptSettingsDrawer = writable(undefined) const history = initHistory(flowStore.val) const stepsInputArgs = new StepsInputArgs() const selectionManager = new SelectionManager() @@ -687,6 +688,7 @@ selectionManager, previewArgs: previewArgsStore, scriptEditorDrawer, + workspaceScriptSettingsDrawer, flowEditorDrawer: writable(undefined), history, pathStore: pathStore, diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index b30db19af9..3ce0a9fa03 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -40,6 +40,7 @@ import { Button } from './common' import FlowEditor from './flows/FlowEditor.svelte' import ScriptEditorDrawer from './flows/content/ScriptEditorDrawer.svelte' + import WorkspaceScriptSettingsDrawer from './flows/content/WorkspaceScriptSettingsDrawer.svelte' import FlowEditorDrawer from './flows/content/FlowEditorDrawer.svelte' import { dfs as dfsApply } from './flows/dfs' import FlowImportExportMenu from './flows/header/FlowImportExportMenu.svelte' @@ -540,6 +541,9 @@ const previewArgsStore = $state({ val: untrack(() => initialArgs) }) const scriptEditorDrawer = writable(undefined) + const workspaceScriptSettingsDrawer = writable( + undefined + ) const flowEditorDrawer = writable(undefined) const history = initHistory(untrack(() => flowStore).val) const pathStore = writable(untrack(() => pathStoreInit) ?? initialPath) @@ -589,6 +593,7 @@ currentEditor: writable(undefined), previewArgs: previewArgsStore, scriptEditorDrawer, + workspaceScriptSettingsDrawer, flowEditorDrawer, history, flowStateStore: untrack(() => flowStateStore), @@ -1139,6 +1144,7 @@ +
diff --git a/frontend/src/lib/components/ScriptAdvancedSettings.svelte b/frontend/src/lib/components/ScriptAdvancedSettings.svelte new file mode 100644 index 0000000000..67fadbe36a --- /dev/null +++ b/frontend/src/lib/components/ScriptAdvancedSettings.svelte @@ -0,0 +1,286 @@ + + +
+
+ {#snippet header()} + + The script will be executed on a worker configured to listen to this worker group tag + (queue). For instance, you could setup an "highmem", or "gpu" tag. + + {/snippet} + +
+ +
+ {#snippet header()} + + Allowed concurrency within a given timeframe + + {/snippet} + { + if (script.concurrent_limit && script.concurrent_limit != undefined) { + script.concurrent_limit = undefined + script.concurrency_time_window_s = undefined + script.concurrency_key = undefined + } else { + script.concurrent_limit = 1 + } + }} + options={{ right: 'Concurrency limits' }} + /> + {#if Boolean(script.concurrent_limit)} +
+ + + +
+ {/if} +
+ +
+ {#snippet header()} + + Cache the results for each possible inputs + + {/snippet} +
+ !!script.cache_ttl, (v) => (script.cache_ttl = v ? 300 : undefined)} + options={{ right: 'Cache the results for each possible inputs' }} + /> + {#if script.cache_ttl} +
How long to keep the cache valid
+ + script.cache_ignore_s3_path, (v) => (script.cache_ignore_s3_path = v || undefined) + } + options={{ + right: 'Ignore S3 Object paths for caching purposes', + rightTooltip: + 'If two S3 objects passed as input have the same content, they will hit the same cache entry, regardless of their path.' + }} + /> + {/if} +
+
+ +
+ {#snippet header()} + + Add a custom timeout for this script + + {/snippet} +
+ { + if (script.timeout && script.timeout != undefined) { + script.timeout = undefined + } else { + script.timeout = 300 + } + }} + options={{ right: 'Add a custom timeout for this script' }} + /> + {#if Boolean(script.timeout)} + Timeout duration + + {/if} +
+
+ +
+ {#snippet header()} + + Debounce Jobs + + {/snippet} + +
+ +
+ {#snippet header()} + + Restart the script upon ending unless cancelled + + {/snippet} + { + script.restart_unless_cancelled = script.restart_unless_cancelled ? undefined : true + }} + options={{ right: 'Restart upon ending unless cancelled' }} + /> +
+ +
+ {#snippet header()} + + In this mode, the script is meant to be run on dedicated workers that run the script at + native speed. Can reach >1500rps per dedicated worker. Only available on enterprise + edition and for Python3, Deno, Bun and Bunnative. + + {/snippet} + { + script.dedicated_worker = script.dedicated_worker ? undefined : true + }} + options={{ right: 'Script is run on dedicated workers' }} + /> + {#if script.dedicated_worker} +
+ + A worker group needs to be configured to listen to this script. Select it in the dedicated + workers section of the worker group configuration. + +
+ {/if} +
+ +
+ {#snippet header()} + + The logs, arguments and results of the job will be completely deleted from Windmill after + the specified delay once it is complete. Set to 0 for immediate deletion. The deletion is + irreversible. This settings ONLY applies when the script is used within a flow or triggered + synchronously. + {#if !$enterpriseLicense} + This option is only available on Windmill Enterprise Edition. + {/if} + + {/snippet} +
+ { + script.delete_after_secs = script.delete_after_secs != null ? undefined : 0 + }} + options={{ right: 'Delete logs, arguments and results after completion' }} + /> + {#if script.delete_after_secs != null} + + {/if} +
+
+ + {#if !isCloudHosted()} +
+ {#snippet header()} + + Jobs from script labeled as high priority take precedence over the other jobs when in the + jobs queue. + {#if !$enterpriseLicense}This is a feature only available on enterprise edition.{/if} + + {/snippet} + 0} + on:change={() => { + script.priority = script.priority ? undefined : 100 + }} + options={{ right: 'Label as high priority' }} + > + {#snippet right()} + { + if (script.priority && script.priority > 100) { + script.priority = 100 + } else if (script.priority && script.priority < 0) { + script.priority = 0 + } + }} + /> + {/snippet} + +
+ {/if} +
diff --git a/frontend/src/lib/components/ScriptBuilder.svelte b/frontend/src/lib/components/ScriptBuilder.svelte index 831cabb498..4007f3bf0f 100644 --- a/frontend/src/lib/components/ScriptBuilder.svelte +++ b/frontend/src/lib/components/ScriptBuilder.svelte @@ -87,6 +87,7 @@ import DefaultScripts from './DefaultScripts.svelte' import { getContext, onMount, setContext, tick, untrack } from 'svelte' import EditorHeader from './EditorHeader.svelte' + import ScriptSettingsBadges from './ScriptSettingsBadges.svelte' import AutosaveIndicator from './AutosaveIndicator.svelte' import LabelsInput from './LabelsInput.svelte' @@ -1971,6 +1972,20 @@ {onOpenOthersDrafts} /> {/if} + {#if !condensedHeader} + {@const canOpenRuntime = + customUi?.topBar?.settings != false && + customUi?.settingsPanel?.disableRuntime !== true} + { + selectedTab = 'runtime' + metadataOpen = true + } + : undefined} + /> + {/if}
diff --git a/frontend/src/lib/components/ScriptSettingsBadges.svelte b/frontend/src/lib/components/ScriptSettingsBadges.svelte new file mode 100644 index 0000000000..8d73609891 --- /dev/null +++ b/frontend/src/lib/components/ScriptSettingsBadges.svelte @@ -0,0 +1,42 @@ + + +{#if badges.length > 0} +
+ {#each badges as badge (badge.key)} + + + onclick?.(badge.key) : undefined} + aria-label={`${badge.label}: ${badge.detail}`} + /> + {#snippet text()} + {badge.label} — {badge.detail} + {/snippet} + + {/each} +
+{/if} diff --git a/frontend/src/lib/components/flows/content/FlowModuleCache.svelte b/frontend/src/lib/components/flows/content/FlowModuleCache.svelte index f9744af36c..99fc4c7b3a 100644 --- a/frontend/src/lib/components/flows/content/FlowModuleCache.svelte +++ b/frontend/src/lib/components/flows/content/FlowModuleCache.svelte @@ -6,12 +6,29 @@ import type { FlowModule } from '$lib/gen' import { SecondsInput } from '../../common' + import WorkspaceScriptSettingInfo from './WorkspaceScriptSettingInfo.svelte' interface Props { flowModule: FlowModule + // For workspace-script steps: the cache_ttl currently set on the referenced + // script, and a shortcut to edit it. Undefined for inline/subflow steps. + workspaceScriptCacheTtl?: number | undefined + loadingWorkspaceScript?: boolean + workspaceScriptError?: string | undefined + canEditWorkspaceScript?: boolean + workspaceScriptNoEditReason?: string | undefined + onEditWorkspaceScript?: () => void } - let { flowModule = $bindable() }: Props = $props() + let { + flowModule = $bindable(), + workspaceScriptCacheTtl = undefined, + loadingWorkspaceScript = false, + workspaceScriptError = undefined, + canEditWorkspaceScript = false, + workspaceScriptNoEditReason = undefined, + onEditWorkspaceScript + }: Props = $props() let isCacheEnabled = $derived(Boolean(flowModule.cache_ttl)) @@ -25,10 +42,22 @@ {/snippet} - {#if flowModule.value.type != 'rawscript'} + {#if flowModule.value.type == 'script'} + + {:else if flowModule.value.type != 'rawscript'}

- The cache settings need to be set in the referenced script/flow settings directly. Cache for - hub scripts is not available yet. + The cache settings need to be set in the referenced flow settings directly.

{:else} ('FlowEditorContext') const selectedId = $derived(selectionManager.getSelectedId()) @@ -180,6 +185,59 @@ let assets = $derived((flowModule.value.type === 'rawscript' && flowModule.value.assets) || []) const flowGraphAssetsCtx = getContext('FlowGraphAssetContext') + // For workspace-script steps, load the referenced script's advanced settings so + // the delegating settings tabs (concurrency, cache, ...) can show current values + // and offer an "Edit script settings" shortcut instead of a bare warning. + const referencedScriptSettings = useWorkspaceScriptSettings( + () => (flowModule.value.type === 'script' ? flowModule.value.path : undefined), + () => (flowModule.value.type === 'script' ? flowModule.value.hash : undefined), + () => opWs + ) + // Hub scripts, hash-pinned steps, and embeddings that disable script editing + // can't have their settings edited from here. The drawer must also be mounted: + // local-dev editors (Dev.svelte / flows/dev) provide the context store but never + // render the drawer, so editing there would be a no-op — keep values read-only. + let canEditWorkspaceScriptSettings = $derived( + flowModule.value.type === 'script' && + !flowModule.value.path?.startsWith('hub/') && + flowModule.value.hash == undefined && + customUi?.scriptEdit != false && + $workspaceScriptSettingsDrawer != undefined + ) + let workspaceScriptNoEditReason = $derived( + flowModule.value.type !== 'script' || canEditWorkspaceScriptSettings + ? undefined + : flowModule.value.path?.startsWith('hub/') + ? 'Hub scripts cannot be edited from here.' + : flowModule.value.hash != undefined + ? 'Steps pinned to a specific version cannot be edited from here.' + : 'Editing script settings is not available in this editor.' + ) + // Non-positive concurrent_limit / cache_ttl are treated as unset by the runtime (legacy rows). + let referencedConcurrentLimit = $derived( + referencedScriptSettings.settings?.concurrent_limit != undefined && + referencedScriptSettings.settings.concurrent_limit > 0 + ? referencedScriptSettings.settings.concurrent_limit + : undefined + ) + let referencedCacheTtl = $derived( + referencedScriptSettings.settings?.cache_ttl != undefined && + referencedScriptSettings.settings.cache_ttl > 0 + ? referencedScriptSettings.settings.cache_ttl + : undefined + ) + function openWorkspaceScriptSettings() { + if (flowModule.value.type !== 'script') return + $workspaceScriptSettingsDrawer?.openDrawer( + flowModule.value.path, + flowModule.value.hash, + async () => { + await referencedScriptSettings.reload() + forceReload++ + } + ) + } + // UI Intent handling for AI tool control useUiIntent(`flow-${flowModule.id}`, { openTab: (tab) => { @@ -770,6 +828,9 @@ flowModule.value.hash = await getLatestHashForScript(flowModule.value.path, opWs) } forceReload++ + // Keep the surfaced concurrency/cache values and badges in sync after + // a settings/code save from the header (path/hash may be unchanged). + await referencedScriptSettings.reload() await reload(flowModule) } if (flowModule.value.type == 'flow') { @@ -991,6 +1052,16 @@ {:else if flowModule.value.type === 'script'} {#if !noEditor && (customUi?.hubCode != false || !flowModule?.value?.path?.startsWith('hub/'))}
+ {#if referencedScriptSettings.settings && getActiveScriptSettingsBadges(referencedScriptSettings.settings).length > 0} +
+ +
+ {/if} {#key forceReload} + {:else if flowModule.value.type == 'script'} + {:else} - The concurrency limit of a workspace script is only settable in the - script metadata itself. For hub scripts, this feature is non available - yet. + The concurrency limit of a referenced flow is only settable in the + flow settings directly. {/if} @@ -1322,7 +1412,15 @@
{:else if advancedSelected === 'cache'}
- +
{:else if advancedSelected === 'early-stop'} diff --git a/frontend/src/lib/components/flows/content/FlowModuleHeader.svelte b/frontend/src/lib/components/flows/content/FlowModuleHeader.svelte index c3d6742b54..b34a78e1db 100644 --- a/frontend/src/lib/components/flows/content/FlowModuleHeader.svelte +++ b/frontend/src/lib/components/flows/content/FlowModuleHeader.svelte @@ -13,7 +13,8 @@ Repeat, Square, Pin, - Save + Save, + Settings } from 'lucide-svelte' import Popover from '../../Popover.svelte' import type { FlowEditorContext } from '../types' @@ -28,7 +29,7 @@ } let { module, tag }: Props = $props() - const { scriptEditorDrawer, flowEditorDrawer, opWorkspace } = + const { scriptEditorDrawer, workspaceScriptSettingsDrawer, flowEditorDrawer, opWorkspace } = getContext('FlowEditorContext') const dispatch = createEventDispatcher() @@ -107,26 +108,54 @@ {/if} {#if module.value.type === 'script'} {#if !module.value.path.startsWith('hub/') && customUi?.scriptEdit != false} - + + + + + {/snippet} + + diff --git a/frontend/src/lib/components/flows/content/WorkspaceScriptSettingInfo.svelte b/frontend/src/lib/components/flows/content/WorkspaceScriptSettingInfo.svelte new file mode 100644 index 0000000000..505474c24c --- /dev/null +++ b/frontend/src/lib/components/flows/content/WorkspaceScriptSettingInfo.svelte @@ -0,0 +1,61 @@ + + +
+
+ + {label} is managed on the referenced workspace script. + + {#if canEdit} + + {/if} +
+
+ {#if loading} + + Loading current value… + + {:else if error} + Could not load the current value: {error} + {:else if active} + {valueText} + {:else} + Not set on the script. + {/if} +
+ {#if !canEdit && noEditReason} + {noEditReason} + {/if} +
diff --git a/frontend/src/lib/components/flows/content/WorkspaceScriptSettingsDrawer.svelte b/frontend/src/lib/components/flows/content/WorkspaceScriptSettingsDrawer.svelte new file mode 100644 index 0000000000..3a88e6214a --- /dev/null +++ b/frontend/src/lib/components/flows/content/WorkspaceScriptSettingsDrawer.svelte @@ -0,0 +1,150 @@ + + + + drawer?.closeDrawer()}> + {#if loading} +
+ + Loading +
+ {:else if loadError || !script} +
+ + {loadError ?? 'Script not found.'} + + {#if current} + + {/if} +
+ {:else} +
+
+ {script.path} + +
+

+ Saving creates a new version of the workspace script with these runtime settings. The code + is left unchanged. +

+ +
+ {/if} + {#snippet actions()} + + {/snippet} +
+
diff --git a/frontend/src/lib/components/flows/types.ts b/frontend/src/lib/components/flows/types.ts index 41165865ae..a527bd765d 100644 --- a/frontend/src/lib/components/flows/types.ts +++ b/frontend/src/lib/components/flows/types.ts @@ -2,6 +2,7 @@ import type { Job, OpenFlow } from '$lib/gen' import type { History } from '$lib/history.svelte' import type { Writable } from 'svelte/store' import type ScriptEditorDrawer from './content/ScriptEditorDrawer.svelte' +import type WorkspaceScriptSettingsDrawer from './content/WorkspaceScriptSettingsDrawer.svelte' import type FlowEditorDrawer from './content/FlowEditorDrawer.svelte' import type { FlowState } from './flowState' import type { FlowBuilderWhitelabelCustomUi } from '../custom_ui' @@ -76,6 +77,7 @@ export type FlowEditorContext = { currentEditor: Writable previewArgs: StateStore> scriptEditorDrawer: Writable + workspaceScriptSettingsDrawer: Writable flowEditorDrawer: Writable history: History pathStore: Writable diff --git a/frontend/src/lib/components/flows/useWorkspaceScriptSettings.svelte.ts b/frontend/src/lib/components/flows/useWorkspaceScriptSettings.svelte.ts new file mode 100644 index 0000000000..861f85e7bf --- /dev/null +++ b/frontend/src/lib/components/flows/useWorkspaceScriptSettings.svelte.ts @@ -0,0 +1,72 @@ +import { ScriptService } from '$lib/gen' +import type { ScriptAdvancedSettingsFields } from '$lib/components/scriptSettings' + +// Loads the advanced runtime settings (concurrency, cache, timeout, ...) of the +// workspace script referenced by a flow step, so the flow editor can surface the +// current values instead of only a "set it on the script" warning. Reactive to +// the path/hash/workspace getters; call reload() after saving new settings. +export function useWorkspaceScriptSettings( + pathGetter: () => string | undefined, + hashGetter: () => string | undefined, + workspaceGetter: () => string | undefined +) { + let settings = $state(undefined) + let loading = $state(false) + let error = $state(undefined) + // Guards against an older in-flight load resolving after a newer one and + // clobbering the displayed settings when path/hash change quickly. + let loadSeq = 0 + + async function load( + path: string | undefined, + hash: string | undefined, + workspace: string | undefined + ) { + const seq = ++loadSeq + if (!path || !workspace || path.startsWith('hub/')) { + settings = undefined + error = undefined + // Clear here too: this supersedes any in-flight load, whose guarded + // finally can no longer reset loading, else the card spins forever. + loading = false + return + } + loading = true + error = undefined + try { + const script = hash + ? await ScriptService.getScriptByHash({ workspace, hash }) + : await ScriptService.getScriptByPath({ workspace, path }) + if (seq !== loadSeq) return + settings = script as ScriptAdvancedSettingsFields + } catch (e) { + console.error('Could not load referenced script settings', e) + if (seq === loadSeq) { + settings = undefined + // Surface failure so cards distinguish "load failed" from "not set". + error = `${(e as { body?: string })?.body ?? e}` + } + } finally { + if (seq === loadSeq) loading = false + } + } + + $effect(() => { + load(pathGetter(), hashGetter(), workspaceGetter()) + }) + + return { + get settings() { + return settings + }, + get loading() { + return loading + }, + get error() { + return error + }, + reload() { + return load(pathGetter(), hashGetter(), workspaceGetter()) + } + } +} diff --git a/frontend/src/lib/components/scriptSettings.test.ts b/frontend/src/lib/components/scriptSettings.test.ts new file mode 100644 index 0000000000..f010e3863a --- /dev/null +++ b/frontend/src/lib/components/scriptSettings.test.ts @@ -0,0 +1,53 @@ +import { describe, it, expect } from 'vitest' +import { getActiveScriptSettingsBadges } from './scriptSettings' + +describe('getActiveScriptSettingsBadges', () => { + it('returns no badges for undefined or empty settings', () => { + expect(getActiveScriptSettingsBadges(undefined)).toEqual([]) + expect(getActiveScriptSettingsBadges({})).toEqual([]) + }) + + it('only surfaces settings that are actually active', () => { + const keys = getActiveScriptSettingsBadges({ + concurrent_limit: 3, + concurrency_time_window_s: 60, + cache_ttl: 600, + timeout: 120, + priority: 50, + tag: 'gpu' + }).map((b) => b.key) + expect(keys).toEqual(['concurrency', 'cache', 'timeout', 'priority', 'tag']) + }) + + it('treats a zero/absent priority and non-positive debounce as inactive', () => { + const keys = getActiveScriptSettingsBadges({ + priority: 0, + debounce_delay_s: 0 + }).map((b) => b.key) + expect(keys).toEqual([]) + }) + + it('treats non-positive concurrency limits, timeouts and cache ttl as inactive (legacy zero rows)', () => { + const keys = getActiveScriptSettingsBadges({ + concurrent_limit: 0, + timeout: 0, + cache_ttl: 0 + }).map((b) => b.key) + expect(keys).toEqual([]) + }) + + it('keeps delete_after_secs of 0 active (immediate deletion is a real setting)', () => { + const badge = getActiveScriptSettingsBadges({ delete_after_secs: 0 }) + expect(badge.map((b) => b.key)).toEqual(['delete_after_use']) + expect(badge[0].detail).toContain('immediately') + }) + + it('pluralizes the concurrency detail correctly', () => { + expect(getActiveScriptSettingsBadges({ concurrent_limit: 1 })[0].detail).toContain( + 'Max 1 execution' + ) + expect(getActiveScriptSettingsBadges({ concurrent_limit: 2 })[0].detail).toContain( + 'Max 2 executions' + ) + }) +}) diff --git a/frontend/src/lib/components/scriptSettings.ts b/frontend/src/lib/components/scriptSettings.ts new file mode 100644 index 0000000000..4a3b3ad5b8 --- /dev/null +++ b/frontend/src/lib/components/scriptSettings.ts @@ -0,0 +1,137 @@ +import { + Gauge, + Database, + Timer, + Hourglass, + Repeat, + Cpu, + Trash2, + ChevronsUp, + Tag +} from 'lucide-svelte' +import type { ScriptLang } from '$lib/gen' + +// Subset of Script/NewScript fields that make up the "advanced runtime settings" +// surfaced both in the standalone script editor and, via the mini settings drawer, +// from within the flow editor for workspace-script steps. +export type ScriptAdvancedSettingsFields = { + path?: string + language?: ScriptLang + schema?: unknown + tag?: string + concurrent_limit?: number + concurrency_time_window_s?: number + concurrency_key?: string + cache_ttl?: number + cache_ignore_s3_path?: boolean + timeout?: number + debounce_delay_s?: number + debounce_key?: string + debounce_args_to_accumulate?: string[] + max_total_debouncing_time?: number + max_total_debounces_amount?: number + restart_unless_cancelled?: boolean + dedicated_worker?: boolean + delete_after_secs?: number + priority?: number +} + +export type ScriptSettingsBadge = { + key: string + label: string + icon: any + detail: string +} + +// Compute the list of active advanced settings for a script, used to render +// at-a-glance badges in the editor top bar and in the flow drawers. +export function getActiveScriptSettingsBadges( + settings: ScriptAdvancedSettingsFields | undefined +): ScriptSettingsBadge[] { + if (!settings) return [] + const badges: ScriptSettingsBadge[] = [] + // Non-positive concurrent_limit / timeout are treated as unset by the runtime + // (legacy zero rows), so don't surface them as active settings. + if (settings.concurrent_limit != undefined && settings.concurrent_limit > 0) { + badges.push({ + key: 'concurrency', + label: 'Concurrency', + icon: Gauge, + detail: `Max ${settings.concurrent_limit} execution${ + settings.concurrent_limit === 1 ? '' : 's' + }${ + settings.concurrency_time_window_s != undefined + ? ` / ${settings.concurrency_time_window_s}s` + : '' + }` + }) + } + if (settings.cache_ttl != undefined && settings.cache_ttl > 0) { + badges.push({ + key: 'cache', + label: 'Cache', + icon: Database, + detail: `Cached for ${settings.cache_ttl}s` + }) + } + if (settings.timeout != undefined && settings.timeout > 0) { + badges.push({ + key: 'timeout', + label: 'Timeout', + icon: Timer, + detail: `${settings.timeout}s` + }) + } + if (settings.debounce_delay_s != undefined && settings.debounce_delay_s > 0) { + badges.push({ + key: 'debounce', + label: 'Debounce', + icon: Hourglass, + detail: `Debounced by ${settings.debounce_delay_s}s` + }) + } + if (settings.restart_unless_cancelled) { + badges.push({ + key: 'perpetual', + label: 'Perpetual', + icon: Repeat, + detail: 'Restarts unless cancelled' + }) + } + if (settings.dedicated_worker) { + badges.push({ + key: 'dedicated', + label: 'Dedicated', + icon: Cpu, + detail: 'Runs on dedicated workers' + }) + } + if (settings.delete_after_secs != undefined) { + badges.push({ + key: 'delete_after_use', + label: 'Delete after use', + icon: Trash2, + detail: + settings.delete_after_secs === 0 + ? 'Deleted immediately after completion' + : `Deleted ${settings.delete_after_secs}s after completion` + }) + } + if (settings.priority != undefined && settings.priority > 0) { + badges.push({ + key: 'priority', + label: 'High priority', + icon: ChevronsUp, + detail: `Priority ${settings.priority}` + }) + } + if (settings.tag) { + badges.push({ + key: 'tag', + label: settings.tag, + icon: Tag, + detail: `Worker tag: ${settings.tag}` + }) + } + return badges +} diff --git a/frontend/src/routes/flows/dev/+page.svelte b/frontend/src/routes/flows/dev/+page.svelte index 620cd60523..fdb3c512f1 100644 --- a/frontend/src/routes/flows/dev/+page.svelte +++ b/frontend/src/routes/flows/dev/+page.svelte @@ -77,6 +77,7 @@ const previewArgsStore = $state({ val: {} }) const scriptEditorDrawer = writable(undefined) + const workspaceScriptSettingsDrawer = writable(undefined) const history = initHistory(flowStore.val) const stepsInputArgs = new StepsInputArgs() @@ -94,6 +95,7 @@ selectionManager, previewArgs: previewArgsStore, scriptEditorDrawer, + workspaceScriptSettingsDrawer, flowEditorDrawer: writable(undefined), history, pathStore: writable(''), @@ -252,7 +254,7 @@ const selectedId = $derived(selectionManager.getSelectedId()) const selectedModule = $derived( selectedId && flowStore.val?.value - ? findModuleInFlow(flowStore.val.value, selectedId) ?? undefined + ? (findModuleInFlow(flowStore.val.value, selectedId) ?? undefined) : undefined ) @@ -293,7 +295,11 @@ {/if} -
+