feat: surface workspace-script advanced settings in flow editor (#10289)

* feat(flow-editor): surface workspace-script advanced settings in flows

Workspace-script steps in a flow could not view or edit script-level
runtime settings (concurrency, cache, timeout, debounce, dedicated
worker, priority, delete-after-use). The concurrency and cache tabs
only showed a "set it on the script" warning with no value and no way
to act on it.

- Add ScriptAdvancedSettings, a reusable subset of the script editor's
  runtime settings, and two entry points that reuse it:
  - WorkspaceScriptSettingsDrawer: a mini settings drawer reachable from
    the flow step (header "Settings" button and the delegating tabs),
    saving a new script version with the code left unchanged.
  - an inner "Settings" drawer inside ScriptEditorDrawer, saved together
    with the code.
- Replace the concurrency/cache delegation warnings with a box that
  fetches the referenced script's current value and offers an
  "Edit script settings" shortcut (useWorkspaceScriptSettings loader).
- Add ScriptSettingsBadges showing active advanced settings, in the
  standalone script editor top bar, the edit-code drawer, and above the
  workspace-script step preview.

Fixes WIN-2233

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(flow-editor): keep subflow concurrency note distinct from workspace-script

The concurrency delegation box is workspace-script specific; subflow
steps now keep a plain limitation note instead of the script settings
shortcut.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(flow-editor): preserve all script fields when saving settings-only version

Building the createScript body by hand dropped codebase/labels/envs and
other fields on the new version. Spread the loaded script instead and
override only lineage, matching ScriptEditorDrawer's save.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(flow-editor): address review — settings-save safety and stale display

- WorkspaceScriptSettingsDrawer: keep settings-only saves from hijacking
  execution identity or discarding the author's draft (preserve_on_behalf_of
  + skip_draft_deletion), and normalize cleared concurrency/debounce keys to
  undefined so blanks don't become shared global keys.
- ScriptEditorDrawer: normalize cleared keys in its save too (the inner
  settings drawer edits them).
- FlowModuleComponent: reload the surfaced concurrency/cache values + badges
  after a header settings/code save; gate settings editing on customUi.scriptEdit.
- useWorkspaceScriptSettings: sequence-guard load() against stale overwrites.
- Add unit tests for getActiveScriptSettingsBadges.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(flow-editor): round-3 review — concurrency-safe save, load guards, UI gates

- WorkspaceScriptSettingsDrawer: drop auto_parent so a settings-only save uses
  the loaded parent as an optimistic-concurrency guard (fails loudly instead of
  silently reverting a concurrent deploy); sequence-guard openDrawer so a slow
  load for a previous script can't clobber a reopened one.
- useWorkspaceScriptSettings: clear loading in the superseded/early-return path
  so a hub/empty step can't spin forever.
- ScriptBuilder: gate the clickable settings badges on customUi.topBar.settings
  and settingsPanel.disableRuntime.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(flow-editor): round-4 review — template, load-error, legacy-zero handling

- WorkspaceScriptSettingsDrawer: stop forcing is_template=false so saving a
  setting on a template keeps its template status; show a recoverable error
  (with Retry) when the settings load fails instead of spinning forever.
- scriptSettings/FlowModuleComponent: treat non-positive concurrent_limit and
  timeout as unset (legacy zero rows), so no "Max 0 executions"/"Timeout 0s".
- Add badge tests for the non-positive cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(flow-editor): round-5 nits — neutral card wording, load-error surfacing, cache zero

- WorkspaceScriptSettingInfo: neutral "managed on the referenced workspace
  script" header (no longer claims "configured" when unset) and a distinct
  error line so a failed load isn't misread as "not set".
- useWorkspaceScriptSettings: expose an error state; thread it into the
  concurrency and cache cards.
- Treat cache_ttl <= 0 as unset, matching concurrency/timeout; add test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(flow-editor): icon-only script action buttons + gate settings in local-dev

- Gate the workspace-script settings actions (header button, clickable badges,
  Concurrency/Cache shortcuts) on the settings drawer actually being mounted, so
  the local-dev flow editors (Dev.svelte / flows/dev) that provide the context
  store but never render the drawer keep the values read-only instead of showing
  no-op controls.
- Make the script action buttons icon-only with clear hover popovers to save
  space in the crowded step/script-editor top bars: Edit, Settings and Fork in
  the step header, Settings in the edit-code drawer, and the settings badges
  (icon chip + label/value popover).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(flow-editor): round-6 nits — a11y names + accurate read-only reason

- Add aria-label to the icon-only Edit/Settings/Fork buttons and the setting
  badges so keyboard/screen-reader users get an accessible name (the hover
  popover alone didn't expose it).
- WorkspaceScriptSettingInfo takes a noEditReason so the read-only explanation
  matches the actual gate (hub / hash-pinned / unavailable-in-this-editor)
  instead of always blaming hub/pinned — fixes the wrong reason shown in the
  local-dev flow editors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(flow-editor): drop narrating comment on the no-edit-reason derived

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(flow-editor): bind settings save completion to the drawer target

The drawer is a singleton, so a save that outlived a reopen ran the new
target's callback and closed its drawer, discarding edits in progress.
Capture the target sequence and callback at save time: the captured
callback still fires (it refreshes the script it belongs to) while the
close, error toast and saving flag only apply if the target is unchanged.
Reopening also resets the saving flag, which the seq-guarded save no
longer clears for a superseded target.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-07-23 23:21:24 +02:00
committed by GitHub
parent a29e13fd18
commit bf16e7d49a
16 changed files with 1101 additions and 43 deletions
+2
View File
@@ -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,
@@ -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<ScriptEditorDrawer | undefined>(undefined)
const workspaceScriptSettingsDrawer = writable<WorkspaceScriptSettingsDrawer | undefined>(
undefined
)
const flowEditorDrawer = writable<FlowEditorDrawer | undefined>(undefined)
const history = initHistory(untrack(() => flowStore).val)
const pathStore = writable<string>(untrack(() => pathStoreInit) ?? initialPath)
@@ -589,6 +593,7 @@
currentEditor: writable(undefined),
previewArgs: previewArgsStore,
scriptEditorDrawer,
workspaceScriptSettingsDrawer,
flowEditorDrawer,
history,
flowStateStore: untrack(() => flowStateStore),
@@ -1139,6 +1144,7 @@
<FlowYamlEditor bind:drawer={yamlEditorDrawer} />
<FlowImportExportMenu bind:drawer={jsonViewerDrawer} />
<ScriptEditorDrawer bind:this={$scriptEditorDrawer} />
<WorkspaceScriptSettingsDrawer bind:this={$workspaceScriptSettingsDrawer} />
<FlowEditorDrawer bind:this={$flowEditorDrawer} />
<div bind:this={flowBuilderRoot} class="flex flex-col h-full">
@@ -0,0 +1,286 @@
<script lang="ts">
import { createBubbler } from 'svelte/legacy'
import Section from '$lib/components/Section.svelte'
import Label from '$lib/components/Label.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import Alert from '$lib/components/common/alert/Alert.svelte'
import { SecondsInput } from '$lib/components/common'
import WorkerTagPicker from '$lib/components/WorkerTagPicker.svelte'
import DebounceLimit from '$lib/components/flows/DebounceLimit.svelte'
import { enterpriseLicense } from '$lib/stores'
import { isCloudHosted } from '$lib/cloud'
import type { Schema } from '$lib/common'
import type { ScriptAdvancedSettingsFields } from './scriptSettings'
const bubble = createBubbler()
interface Props {
script: ScriptAdvancedSettingsFields
// Workspace to read worker tags from (defaults to $workspaceStore inside the picker).
workspaceId?: string | undefined
}
let { script = $bindable(), workspaceId = undefined }: Props = $props()
</script>
<div class="flex flex-col gap-8">
<Section label="Worker group tag (queue)">
{#snippet header()}
<Tooltip documentationLink="https://www.windmill.dev/docs/core_concepts/worker_groups">
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.
</Tooltip>
{/snippet}
<WorkerTagPicker bind:tag={script.tag} {workspaceId} />
</Section>
<Section label="Concurrency limits" eeOnly>
{#snippet header()}
<Tooltip documentationLink="https://www.windmill.dev/docs/core_concepts/concurrency_limits">
Allowed concurrency within a given timeframe
</Tooltip>
{/snippet}
<Toggle
size="sm"
checked={Boolean(script.concurrent_limit)}
on:change={() => {
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)}
<div class="flex flex-col gap-4 mt-2">
<Label label="Max number of executions within the time window">
<div class="flex flex-row gap-2 max-w-sm whitespace-nowrap">
<input
disabled={!$enterpriseLicense}
bind:value={script.concurrent_limit}
type="number"
/>
</div>
</Label>
<Label label="Time window in seconds">
<SecondsInput
disabled={!$enterpriseLicense}
bind:seconds={script.concurrency_time_window_s}
/>
</Label>
<Label label="Custom concurrency key (optional)">
{#snippet header()}
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/concurrency_limits#custom-concurrency-key"
>
Concurrency keys are global, you can have them be workspace specific using the
variable `$workspace`. You can also use an argument's value using `$args[name_of_arg]`</Tooltip
>
{/snippet}
<input
disabled={!$enterpriseLicense}
type="text"
bind:value={script.concurrency_key}
placeholder={`$workspace/script/${script.path ?? ''}-$args[foo]`}
/>
</Label>
</div>
{/if}
</Section>
<Section label="Cache">
{#snippet header()}
<Tooltip documentationLink="https://www.windmill.dev/docs/core_concepts/caching">
Cache the results for each possible inputs
</Tooltip>
{/snippet}
<div class="flex gap-2 shrink flex-col">
<Toggle
size="sm"
bind:checked={() => !!script.cache_ttl, (v) => (script.cache_ttl = v ? 300 : undefined)}
options={{ right: 'Cache the results for each possible inputs' }}
/>
{#if script.cache_ttl}
<div class="text-2xs text-secondary">How long to keep the cache valid</div>
<SecondsInput bind:seconds={script.cache_ttl} />
<Toggle
size="2xs"
bind:checked={
() => 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}
</div>
</Section>
<Section label="Timeout">
{#snippet header()}
<Tooltip documentationLink="https://www.windmill.dev/docs/script_editor/settings#timeout">
Add a custom timeout for this script
</Tooltip>
{/snippet}
<div class="flex gap-2 shrink flex-col">
<Toggle
size="sm"
checked={Boolean(script.timeout)}
on:change={() => {
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)}
<span class="text-xs font-semibold text-emphasis leading-none mt-2">Timeout duration</span>
<SecondsInput bind:seconds={script.timeout} />
{/if}
</div>
</Section>
<Section label="Debouncing">
{#snippet header()}
<Tooltip documentationLink="https://www.windmill.dev/docs/core_concepts/job_debouncing">
Debounce Jobs
</Tooltip>
{/snippet}
<DebounceLimit
size="sm"
bind:debounce_delay_s={script.debounce_delay_s}
bind:debounce_key={script.debounce_key}
bind:debounce_args_to_accumulate={script.debounce_args_to_accumulate}
bind:max_total_debouncing_time={script.max_total_debouncing_time}
bind:max_total_debounces_amount={script.max_total_debounces_amount}
schema={script.schema as Schema}
placeholder={`$workspace/script/${script.path ?? ''}-$args[foo]`}
/>
</Section>
<Section label="Perpetual script">
{#snippet header()}
<Tooltip documentationLink="https://www.windmill.dev/docs/script_editor/perpetual_scripts">
Restart the script upon ending unless cancelled
</Tooltip>
{/snippet}
<Toggle
size="sm"
checked={Boolean(script.restart_unless_cancelled)}
on:change={() => {
script.restart_unless_cancelled = script.restart_unless_cancelled ? undefined : true
}}
options={{ right: 'Restart upon ending unless cancelled' }}
/>
</Section>
<Section label="Dedicated workers" eeOnly>
{#snippet header()}
<Tooltip documentationLink="https://www.windmill.dev/docs/core_concepts/dedicated_workers">
In this mode, the script is meant to be run on dedicated workers that run the script at
native speed. Can reach &gt;1500rps per dedicated worker. Only available on enterprise
edition and for Python3, Deno, Bun and Bunnative.
</Tooltip>
{/snippet}
<Toggle
disabled={!$enterpriseLicense ||
isCloudHosted() ||
(script.language != 'bun' &&
script.language != 'bunnative' &&
script.language != 'python3' &&
script.language != 'deno')}
size="sm"
checked={Boolean(script.dedicated_worker)}
on:change={() => {
script.dedicated_worker = script.dedicated_worker ? undefined : true
}}
options={{ right: 'Script is run on dedicated workers' }}
/>
{#if script.dedicated_worker}
<div class="py-2">
<Alert type="info" title="Require dedicated workers">
A worker group needs to be configured to listen to this script. Select it in the dedicated
workers section of the worker group configuration.
</Alert>
</div>
{/if}
</Section>
<Section label="Delete after completion">
{#snippet header()}
<Tooltip
documentationLink="https://www.windmill.dev/docs/script_editor/settings#delete-after-use"
>
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}
</Tooltip>
{/snippet}
<div class="flex gap-2 shrink flex-col">
<Toggle
disabled={!$enterpriseLicense}
size="sm"
checked={script.delete_after_secs != null}
on:change={() => {
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}
<SecondsInput bind:seconds={script.delete_after_secs} disabled={!$enterpriseLicense} />
{/if}
</div>
</Section>
{#if !isCloudHosted()}
<Section label="High priority script" eeOnly>
{#snippet header()}
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/jobs#high-priority-jobs"
>
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}
</Tooltip>
{/snippet}
<Toggle
disabled={!$enterpriseLicense || isCloudHosted()}
size="sm"
checked={script.priority !== undefined && script.priority > 0}
on:change={() => {
script.priority = script.priority ? undefined : 100
}}
options={{ right: 'Label as high priority' }}
>
{#snippet right()}
<input
type="number"
class="!w-16 ml-4"
disabled={script.priority === undefined}
bind:value={script.priority}
onfocus={bubble('focus')}
onchange={() => {
if (script.priority && script.priority > 100) {
script.priority = 100
} else if (script.priority && script.priority < 0) {
script.priority = 0
}
}}
/>
{/snippet}
</Toggle>
</Section>
{/if}
</div>
@@ -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}
<ScriptSettingsBadges
settings={script}
onclick={canOpenRuntime
? () => {
selectedTab = 'runtime'
metadataOpen = true
}
: undefined}
/>
{/if}
</div>
<!-- Separator -->
@@ -0,0 +1,42 @@
<script lang="ts">
import Badge from '$lib/components/common/badge/Badge.svelte'
import Popover from '$lib/components/Popover.svelte'
import {
getActiveScriptSettingsBadges,
type ScriptAdvancedSettingsFields
} from './scriptSettings'
interface Props {
settings: ScriptAdvancedSettingsFields | undefined
// When provided, badges become clickable and call this with the badge key
// (e.g. to open the settings drawer focused on that section).
onclick?: (key: string) => void
small?: boolean
}
let { settings, onclick, small = true }: Props = $props()
let badges = $derived(getActiveScriptSettingsBadges(settings))
</script>
{#if badges.length > 0}
<div class="flex flex-row flex-wrap gap-1 items-center">
{#each badges as badge (badge.key)}
<!-- Icon-only chips to save space in crowded top bars; the label and current
value are shown in the hover popover. -->
<Popover notClickable placement="bottom">
<Badge
color="blue"
{small}
icon={{ icon: badge.icon, position: 'left' }}
clickable={Boolean(onclick)}
onclick={onclick ? () => onclick?.(badge.key) : undefined}
aria-label={`${badge.label}: ${badge.detail}`}
/>
{#snippet text()}
<span class="font-semibold">{badge.label}</span>{badge.detail}
{/snippet}
</Popover>
{/each}
</div>
{/if}
@@ -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))
</script>
@@ -25,10 +42,22 @@
</Tooltip>
{/snippet}
{#if flowModule.value.type != 'rawscript'}
{#if flowModule.value.type == 'script'}
<WorkspaceScriptSettingInfo
label="Cache"
active={workspaceScriptCacheTtl != undefined}
valueText={workspaceScriptCacheTtl != undefined
? `Cached for ${workspaceScriptCacheTtl}s`
: undefined}
loading={loadingWorkspaceScript}
error={workspaceScriptError}
canEdit={canEditWorkspaceScript}
noEditReason={workspaceScriptNoEditReason}
onEdit={onEditWorkspaceScript}
/>
{:else if flowModule.value.type != 'rawscript'}
<p class="text-xs text-secondary">
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.
</p>
{:else}
<Toggle
@@ -55,6 +55,10 @@
import { type Job } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { checkIfParentLoop } from '../utils.svelte'
import { useWorkspaceScriptSettings } from '../useWorkspaceScriptSettings.svelte'
import ScriptSettingsBadges from '$lib/components/ScriptSettingsBadges.svelte'
import { getActiveScriptSettingsBadges } from '$lib/components/scriptSettings'
import WorkspaceScriptSettingInfo from './WorkspaceScriptSettingInfo.svelte'
import ModulePreviewResultViewer from '$lib/components/ModulePreviewResultViewer.svelte'
import LogViewer from '$lib/components/LogViewer.svelte'
import DisplayResult from '$lib/components/DisplayResult.svelte'
@@ -95,7 +99,8 @@
saveDraft,
customUi,
executionCount,
opWorkspace
opWorkspace,
workspaceScriptSettingsDrawer
} = getContext<FlowEditorContext>('FlowEditorContext')
const selectedId = $derived(selectionManager.getSelectedId())
@@ -180,6 +185,59 @@
let assets = $derived((flowModule.value.type === 'rawscript' && flowModule.value.assets) || [])
const flowGraphAssetsCtx = getContext<FlowGraphAssetContext | undefined>('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/'))}
<div class="border-t">
{#if referencedScriptSettings.settings && getActiveScriptSettingsBadges(referencedScriptSettings.settings).length > 0}
<div class="flex flex-row items-center gap-2 px-2 pt-2 flex-wrap">
<ScriptSettingsBadges
settings={referencedScriptSettings.settings}
onclick={canEditWorkspaceScriptSettings
? openWorkspaceScriptSettings
: undefined}
/>
</div>
{/if}
{#key forceReload}
<FlowModuleScript
bind:tag={workspaceScriptTag}
@@ -1251,11 +1322,30 @@
placeholder={`$workspace/script/${$pathStore}-$args[foo]`}
/>
</Label>
{:else if flowModule.value.type == 'script'}
<WorkspaceScriptSettingInfo
label="Concurrency limit"
active={referencedConcurrentLimit != undefined}
valueText={referencedConcurrentLimit != undefined
? `Max ${referencedConcurrentLimit} execution${
referencedConcurrentLimit === 1 ? '' : 's'
}${
referencedScriptSettings.settings?.concurrency_time_window_s !=
undefined
? ` within ${referencedScriptSettings.settings.concurrency_time_window_s}s`
: ''
}`
: undefined}
loading={referencedScriptSettings.loading}
error={referencedScriptSettings.error}
canEdit={canEditWorkspaceScriptSettings}
noEditReason={workspaceScriptNoEditReason}
onEdit={openWorkspaceScriptSettings}
/>
{:else}
<Alert type="warning" title="Limitation" size="xs">
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.
</Alert>
{/if}
</Section>
@@ -1322,7 +1412,15 @@
</div>
{:else if advancedSelected === 'cache'}
<div>
<FlowModuleCache bind:flowModule />
<FlowModuleCache
bind:flowModule
workspaceScriptCacheTtl={referencedCacheTtl}
loadingWorkspaceScript={referencedScriptSettings.loading}
workspaceScriptError={referencedScriptSettings.error}
canEditWorkspaceScript={canEditWorkspaceScriptSettings}
{workspaceScriptNoEditReason}
onEditWorkspaceScript={openWorkspaceScriptSettings}
/>
</div>
{:else if advancedSelected === 'early-stop'}
<FlowModuleEarlyStop bind:flowModule />
@@ -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>('FlowEditorContext')
const dispatch = createEventDispatcher()
@@ -107,26 +108,54 @@
{/if}
{#if module.value.type === 'script'}
{#if !module.value.path.startsWith('hub/') && customUi?.scriptEdit != false}
<Button
unifiedSize="sm"
variant="subtle"
onClick={async () => {
if (module.value.type == 'script') {
const hash =
module.value.hash ??
(await getLatestHashForScript(module.value.path, opWorkspace?.()))
$scriptEditorDrawer?.openDrawer(hash, () => {
dispatch('reload')
sendUserToast('Script has been updated')
})
}
}}
startIcon={{ icon: Pen }}
iconOnly={false}
disabled={module.value.hash != undefined}
>
Edit
</Button>
<Popover notClickable placement="bottom">
<Button
unifiedSize="sm"
variant="subtle"
onClick={async () => {
if (module.value.type == 'script') {
const hash =
module.value.hash ??
(await getLatestHashForScript(module.value.path, opWorkspace?.()))
$scriptEditorDrawer?.openDrawer(hash, () => {
dispatch('reload')
sendUserToast('Script has been updated')
})
}
}}
startIcon={{ icon: Pen }}
iconOnly
aria-label="Edit the script's code"
disabled={module.value.hash != undefined}
/>
{#snippet text()}Edit the script's code{/snippet}
</Popover>
<!-- Only when the settings drawer is actually mounted (not in the local-dev
editors, which provide the context store but never render it). -->
{#if $workspaceScriptSettingsDrawer}
<Popover notClickable placement="bottom">
<Button
unifiedSize="sm"
variant="subtle"
onClick={() => {
if (module.value.type == 'script') {
$workspaceScriptSettingsDrawer?.openDrawer(
module.value.path,
module.value.hash,
() => {
dispatch('reload')
}
)
}
}}
startIcon={{ icon: Settings }}
iconOnly
aria-label="Edit the script's runtime settings"
disabled={module.value.hash != undefined}
/>
{#snippet text()}Edit the script's runtime settings (concurrency, cache, timeout, ...){/snippet}
</Popover>
{/if}
{/if}
{#if customUi?.tagEdit != false}
<FlowModuleWorkerTagSelect
@@ -139,15 +168,17 @@
/>
{/if}
{#if customUi?.scriptFork != false}
<Button
unifiedSize="sm"
variant="subtle"
on:click={() => dispatch('fork')}
startIcon={{ icon: GitFork }}
iconOnly={false}
>
Fork
</Button>
<Popover notClickable placement="bottom">
<Button
unifiedSize="sm"
variant="subtle"
on:click={() => dispatch('fork')}
startIcon={{ icon: GitFork }}
iconOnly
aria-label="Fork into an inline script"
/>
{#snippet text()}Fork into an inline script{/snippet}
</Popover>
{/if}
{:else if module.value.type === 'flow'}
<Button
@@ -6,10 +6,14 @@
import { ScriptService, type Preview, type Script } from '$lib/gen'
import { inferArgs } from '$lib/infer'
import { workspaceStore } from '$lib/stores'
import { Loader2, Save, DiffIcon } from 'lucide-svelte'
import { Loader2, Save, DiffIcon, Settings } from 'lucide-svelte'
import ScriptAdvancedSettings from '$lib/components/ScriptAdvancedSettings.svelte'
import ScriptSettingsBadges from '$lib/components/ScriptSettingsBadges.svelte'
import Popover from '$lib/components/Popover.svelte'
import {
cleanValueProperties,
emptySchema,
emptyString,
orderedJsonStringify,
sendUserToast
} from '$lib/utils'
@@ -56,6 +60,20 @@
on_behalf_of_email?: string
auto_kind?: string
has_preprocessor?: boolean
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
priority?: number
delete_after_secs?: number
}
| undefined = $state(undefined)
@@ -77,6 +95,20 @@
on_behalf_of_email?: string
auto_kind?: string
has_preprocessor?: boolean
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
priority?: number
delete_after_secs?: number
}
| undefined = $state(undefined)
@@ -102,6 +134,11 @@
is_template: false,
tag: script.tag,
kind: script.kind as Script['kind'] | undefined,
// Empty keys are shared global keys server-side; treat cleared inputs as unset.
concurrency_key: emptyString(script.concurrency_key)
? undefined
: script.concurrency_key,
debounce_key: emptyString(script.debounce_key) ? undefined : script.debounce_key,
lock: undefined
}
})
@@ -135,6 +172,7 @@
let args = $state({})
let displayEditor = $state(true)
let settingsDrawer: Drawer | undefined = $state()
</script>
<ConfirmationModal
@@ -239,6 +277,20 @@
</div>
{/if}
{#snippet actions()}
{#if script}
<ScriptSettingsBadges settings={script} onclick={() => settingsDrawer?.openDrawer()} />
{/if}
<Popover notClickable placement="bottom">
<Button
disabled={!script}
variant="default"
iconOnly
startIcon={{ icon: Settings }}
aria-label="Runtime settings"
on:click={() => settingsDrawer?.openDrawer()}
/>
{#snippet text()}Runtime settings (concurrency, cache, timeout, ...){/snippet}
</Popover>
<Button
disabled={!savedScript || !script}
variant="default"
@@ -290,3 +342,19 @@
displayEditor = true
}}
/>
<Drawer bind:this={settingsDrawer} size="600px">
<DrawerContent title="Script settings" on:close={() => settingsDrawer?.closeDrawer()}>
{#if script}
<div class="flex flex-col gap-4">
<p class="text-xs text-secondary">
These runtime settings are saved together with the script when you press Save.
</p>
<ScriptAdvancedSettings {script} workspaceId={opWs} />
</div>
{/if}
{#snippet actions()}
<Button variant="border" on:click={() => settingsDrawer?.closeDrawer()}>Done</Button>
{/snippet}
</DrawerContent>
</Drawer>
@@ -0,0 +1,61 @@
<script lang="ts">
import Button from '$lib/components/common/button/Button.svelte'
import { Loader2, Settings } from 'lucide-svelte'
interface Props {
// Human label of the setting, e.g. "Concurrency limit" or "Cache".
label: string
// Whether the setting is currently configured on the referenced script.
active: boolean
// Rendered summary of the current value (only shown when active).
valueText?: string | undefined
loading?: boolean
// Set when loading the referenced script failed, to distinguish that from "not set".
error?: string | undefined
// Editing is possible only for non-hub, non-hash-pinned workspace scripts.
canEdit: boolean
// Explanation shown when editing isn't possible (varies: hub, pinned, dev editor).
noEditReason?: string | undefined
onEdit?: () => void
}
let {
label,
active,
valueText,
loading = false,
error = undefined,
canEdit,
noEditReason = undefined,
onEdit
}: Props = $props()
</script>
<div class="flex flex-col gap-2 rounded-md border p-3 bg-surface-secondary">
<div class="flex flex-row items-center justify-between gap-2">
<span class="text-xs text-secondary">
{label} is managed on the referenced workspace script.
</span>
{#if canEdit}
<Button size="xs" variant="border" startIcon={{ icon: Settings }} on:click={() => onEdit?.()}>
Edit script settings
</Button>
{/if}
</div>
<div class="text-xs">
{#if loading}
<span class="text-tertiary inline-flex items-center gap-1">
<Loader2 class="animate-spin" size={12} /> Loading current value…
</span>
{:else if error}
<span class="text-red-500">Could not load the current value: {error}</span>
{:else if active}
<span class="font-semibold text-emphasis">{valueText}</span>
{:else}
<span class="text-tertiary">Not set on the script.</span>
{/if}
</div>
{#if !canEdit && noEditReason}
<span class="text-2xs text-tertiary">{noEditReason}</span>
{/if}
</div>
@@ -0,0 +1,150 @@
<script lang="ts">
import { Button, Drawer, DrawerContent } from '$lib/components/common'
import Alert from '$lib/components/common/alert/Alert.svelte'
import ScriptAdvancedSettings from '$lib/components/ScriptAdvancedSettings.svelte'
import ScriptSettingsBadges from '$lib/components/ScriptSettingsBadges.svelte'
import { ScriptService, type Script } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { emptyString, sendUserToast } from '$lib/utils'
import { Loader2, Save } from 'lucide-svelte'
import { getContext } from 'svelte'
import type { FlowEditorContext } from '../types'
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
let drawer: Drawer | undefined = $state()
// cache_ignore_s3_path lives on NewScript but not the returned Script type;
// ScriptAdvancedSettings still edits it, so widen the local type to keep it.
let script: (Script & { cache_ignore_s3_path?: boolean }) | undefined = $state(undefined)
let loading = $state(false)
let loadError = $state<string | undefined>(undefined)
let saving = $state(false)
let callback: (() => void) | undefined = undefined
let current: { path: string; hash: string | undefined } | undefined = undefined
// Guards against a slow load for a previously-opened script resolving after
// the drawer was reopened for a different one and overwriting its target.
let openSeq = 0
// Open the settings drawer for the workspace script referenced by `path`.
// A specific `hash` may be passed to load that version.
export async function openDrawer(
path: string,
hash: string | undefined,
cb?: () => void
): Promise<void> {
callback = cb
drawer?.openDrawer?.()
await load(path, hash)
}
async function load(path: string, hash: string | undefined): Promise<void> {
const seq = ++openSeq
script = undefined
loadError = undefined
current = { path, hash }
loading = true
// A save still in flight for the previous target no longer clears this flag
// (it is seq-guarded), so reset it here or the new target's Save stays disabled.
saving = false
try {
const loaded = hash
? await ScriptService.getScriptByHash({ workspace: opWs!, hash })
: await ScriptService.getScriptByPath({ workspace: opWs!, path })
if (seq !== openSeq) return
script = loaded
} catch (e) {
if (seq === openSeq) loadError = `${e?.body ?? e}`
} finally {
if (seq === openSeq) loading = false
}
}
async function save(): Promise<void> {
if (!script) return
// The drawer is a singleton: bind this save to the target that started it, so a
// reopen for another script can't receive its callback or get closed under the
// user. The captured callback still fires — it refreshes the script it belongs to.
const seq = openSeq
const cb = callback
saving = true
try {
// Spread the full loaded script so fields we don't edit here (codebase,
// labels, envs, on_behalf_of_email, ...) survive the new version; only
// override lineage and normalize the edited settings.
// parent_hash without auto_parent is an optimistic-concurrency guard: if the
// script was deployed elsewhere while the drawer was open, the save fails
// loudly (non-linear lineage) instead of silently reverting that deploy.
// preserve_on_behalf_of/skip_draft_deletion keep a settings-only save from
// hijacking the execution identity or discarding the author's code draft.
await ScriptService.createScript({
workspace: opWs!,
requestBody: {
...script,
summary: script.summary ?? '',
description: script.description ?? '',
parent_hash: script.hash,
lock: script.lock,
concurrency_key: emptyString(script.concurrency_key) ? undefined : script.concurrency_key,
debounce_key: emptyString(script.debounce_key) ? undefined : script.debounce_key,
preserve_on_behalf_of: true,
skip_draft_deletion: true
}
})
sendUserToast('Script settings saved')
cb?.()
if (seq === openSeq) drawer?.closeDrawer()
} catch (e) {
if (seq === openSeq) sendUserToast(`Could not save script settings: ${e.body ?? e}`, true)
} finally {
if (seq === openSeq) saving = false
}
}
</script>
<Drawer bind:this={drawer} size="600px">
<DrawerContent title="Script settings" on:close={() => drawer?.closeDrawer()}>
{#if loading}
<div class="center-center flex-col h-full text-tertiary">
<Loader2 class="animate-spin" size={16} />
<span class="text-xs mt-1">Loading</span>
</div>
{:else if loadError || !script}
<div class="center-center flex-col h-full gap-3">
<Alert type="error" title="Could not load script settings" size="xs">
{loadError ?? 'Script not found.'}
</Alert>
{#if current}
<Button
size="xs"
variant="border"
on:click={() => current && load(current.path, current.hash)}
>
Retry
</Button>
{/if}
</div>
{:else}
<div class="flex flex-col gap-4">
<div class="flex flex-row items-center gap-2 flex-wrap">
<span class="text-xs text-tertiary">{script.path}</span>
<ScriptSettingsBadges settings={script} />
</div>
<p class="text-xs text-secondary">
Saving creates a new version of the workspace script with these runtime settings. The code
is left unchanged.
</p>
<ScriptAdvancedSettings {script} workspaceId={opWs} />
</div>
{/if}
{#snippet actions()}
<Button
on:click={save}
disabled={!script || loading || saving}
startIcon={{ icon: saving ? Loader2 : Save }}
>
Save
</Button>
{/snippet}
</DrawerContent>
</Drawer>
@@ -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<CurrentEditor>
previewArgs: StateStore<Record<string, any>>
scriptEditorDrawer: Writable<ScriptEditorDrawer | undefined>
workspaceScriptSettingsDrawer: Writable<WorkspaceScriptSettingsDrawer | undefined>
flowEditorDrawer: Writable<FlowEditorDrawer | undefined>
history: History<OpenFlow>
pathStore: Writable<string>
@@ -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<ScriptAdvancedSettingsFields | undefined>(undefined)
let loading = $state(false)
let error = $state<string | undefined>(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())
}
}
}
@@ -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'
)
})
})
@@ -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
}
+8 -2
View File
@@ -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}
</div>
<div class="flex justify-center pt-1 z-50 absolute gap-2 {compactPreview ? 'left-1/2 -translate-x-1/2 top-14' : 'right-2 top-2'}">
<div
class="flex justify-center pt-1 z-50 absolute gap-2 {compactPreview
? 'left-1/2 -translate-x-1/2 top-14'
: 'right-2 top-2'}"
>
<FlowPreviewButtons bind:this={flowPreviewButtons} {suspendStatus} />
</div>
<Splitpanes horizontal class="max-h-screen grow min-h-0">