mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-13 08:05:23 +00:00
* feat: responsive modal step panel for the flow editor in sessions On narrow layouts the flow editor's step-details pane opens as a modal (double-click a graph node) instead of a split pane, with a dock/float toggle. Scoped to sessions via allowModalPanel; the full-page editor is unchanged. - FlowEditor: modal/docked modes gated by mount width + allowModalPanel, small header (step-id Badge + subtle dock/close), standing double-click hint, and a per-step hint in the name tooltip - selectionManager: onSelectIntent hook so flow-level panels (settings, input, triggers…) open the modal on single click - PropPickerWrapper: collapse the prop picker until connect and animate it in via AnimatedPane (runs-page pattern), no blue connect ring in modal mode - StepInputGen: drop the TAB/Wand autocompletion button + spinner (feature still works via focus + Tab) - InputTransformForm: decouple the Help dropdown from the AI suggestion - FlowModuleHeader: move 'Save to workspace' into an ellipsis dropdown Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: loop editor rendering and nested splitpanes splitters in the sessions modal - Loop iterator/parallelism: keep the picker split pane (forceExpanded) so the editor fills its box and the picker shows; the collapse-until-connect mode stays for the step inputs - Remove the intrusive AI TAB/Wand autocompletion button from IteratorGen (generation still runs headless via focus + Tab) - Size the iterator connect plug and restyle the loop header/labels/toggles - Scope the global `.splitter-hidden` splitter-hiding rule to direct children so it no longer leaks into nested Splitpanes under the sessions preview Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: redesign flow step advanced settings as a single toggle-first column Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: taller step test pane by default and restyle advanced section titles Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: show flow run-settings params disabled when a setting is toggled off Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: single-column for-loop panel reusing the run-settings accordion Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: single-column while-loop panel reusing the run-settings accordion Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: single-column branch panels reusing the run-settings accordion Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: auto-open modal panel when creating an AI agent tool Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: redesign branch panels with card layout and shared predicate editor Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor: remove per-setting status badges from flow map nodes Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: sync package-lock after windmill-utils-internal bump Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style: polish prop-picker plug button and branch panel layouts * fix: persist skip-if-stopped toggles in early stop settings * fix: open the step panel modal on demand and cap its width * fix: restore graph step setting badges, strip panel header chips instead * feat: docked panel header with detach action and open-details step menu * feat: width-based panel mode on every surface with inline detach action * refactor: single source for flow step settings and their defaults * docs: pin flow editor vocabulary in CONTEXT.md * fix: open the trigger panel on double click or a specific trigger * fix: keep module pickers inside their pane and dismissable * fix: drop the misleading chevron on the MCP tool entry * fix: resolve flow approvals against the job's workspace, not the nav one * refactor: derive the approval workspace from the job, not from callers * fix: restore S3 snippets and gate params while their setting is off * fix: restore branch mock controls and address review findings * chore: drop stray debug log from the flow map item * feat: pinned output section for loop and branch panels * fix: open the panel for deliberate navigation from the flow header * perf: mount branch predicate editors on demand * fix: skip predicate picker previews the previous step's result * fix: flow-level graph nodes open their panel on a single click * fix: open the step panel for AI chat selections, not for undo * chore: drop dead console.log and duplicated modalPanel doc * fix: re-sync expression editors and scope error-handler settings * fix: match the failure module exactly and ignore unselectable nodes * fix: keep concurrency editable, honour module cache_ttl, tighten panel ids * fix: open panel from indirect selections, use presence for value-driven toggles * fix: don't open settings on error-handler delete, flush editors on unmount * fix: guard editor destroy flush, keep retry kind reachable * refactor: name the run settings panel after the domain vocabulary * fix: only write editor flushes to the step they belong to * fix: bind step panels by id so a delete can't retarget editor writes * fix: don't let the trigger picker's escape close the drawer beneath it * docs: condense two comments to the constraint they record * fix: arbitrate escape through the overlay stack instead of deferring to it * fix: key nested step blocks by identity so anchored bindings can't go stale * fix: untrack the overlay-stack push and drop the frozen branch binding * chore: state the escape rationale once, key branch lists, format * fix: let the topmost overlay own escape instead of the graph * fix: keep the dynamic-input help box out of static template fields * fix: restore the graph connect on the for-loop iterator * fix: end connect mode with the modal and keep it to docked panels * fix: never enter graph connect mode from the modal panel * fix: reveal inserted steps, restore editor pane size, unleak the drawer stack * fix: keep the enable-AI popover reachable in session panes * feat: add the connect policy and its single armed slot * refactor: one picker for every expression input * refactor: route every connect through one armed slot * fix: give every connect button the same footprint * fix: keep the connect ring from showing through the button * fix: keep flow card actions right-aligned beside the detach button * fix: give the connect ring an opaque ground to mask against * feat: dock the panel back without reopening it * feat: dock the panel from the graph control bar * style: round the graph control bar and size its glyphs * style: customize the graph controls through their supported api * style: build the graph control bar from lucide icons * fix: use the graph's tooltip component in the zoom controls * style: pad the graph controls and enlarge their glyphs * style: pad the graph controls and put dock at the bar's end * refactor: give settings rows the same popover picker as other expressions * fix: pass the wrapper's pickable properties to nested inputs * refactor: stack step settings and render every expression through the step input form * feat: split loop panels into tabs and rework the approval form * feat: anchor drawers to their host pane and give them a size floor * fix: mark the loop iterator expression as required * refactor: badge ee-only toggles instead of a warning line * fix: flag an empty loop iterator expression as an error * refactor: pick the early-stop flow status from one toggle group * fix: keep parallel loops uncapped unless a limit is opted into * fix: scope the overlay stack to its host and disarm connect on dismissal * fix: anchor the trigger picker to its host pane * feat: move diff into the menu when the top bar is narrow * fix: gate the result logs toggle to the graph popover * feat: raise the modal-panel breakpoint to 1280 * fix: anchor flow editor popovers and fullscreen to their host pane * fix: anchor overlays to their host pane and mute them when hidden * fix: portal hosted modals and menus into the pane they anchor to * fix: keep non-listening dialogs off the overlay stack * fix: drop the topmost gate from confirmation dialogs * fix: silence overlays in a collapsed preview panel * feat: rework the branch panels with tabs, reordering and add/delete * refactor: fold the detached-panel chrome into the card header * fix: give every flow panel a titled card header * fix: stop the step panel oscillating on an auto-height editor * feat: consolidate script panel actions and restore branch predicate AI * fix: restore the logs toggle on the flow result popover * fix: collapse the idle property picker in modal step panels * fix: stop the docked pane scrolling alongside its panel * fix: space the last settings row off the panel bottom * revert: always show the property picker pane in step panels * chore: keep the inline script AI button identical to main * fix: ask for AI input suggestions on click, not on hover * fix: keep graph connects armed and remount the parallelism input * style: reveal the predicate AI button on row hover * style: give branch cards a handle and delete column * refactor: arbitrate flow overlay escape through Disposable * fix: give the popover picker its results and re-narrow the EE badge * docs: correct loopSubset and guard the modal width measurement * fix: insert picked properties at the cursor in expression inputs * fix: give the expanded-subflow panel the shared header chrome * style: rename the suspend setting to Suspend until approval/resume * feat: open a step's modal when clicking the step already selected * feat: add an auto/attached/detached toggle for the step panel * refactor: pick the step panel's placement from one named menu * refactor: keep the panel-mode module's exports to what is consumed * feat: show each configured setting's value on its badge * fix: carry the suspend rename into the step settings registry * docs: name both gestures in the step explore hint * test: pin where the step panel goes for a given width and preference --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
363 lines
12 KiB
TypeScript
363 lines
12 KiB
TypeScript
import {
|
|
ChevronsUp,
|
|
CircleStop,
|
|
Combine,
|
|
Database,
|
|
Gauge,
|
|
Hand,
|
|
Moon,
|
|
RefreshCw,
|
|
ShieldAlert,
|
|
SkipForward,
|
|
Timer,
|
|
Trash2
|
|
} from 'lucide-svelte'
|
|
import type { FlowModule } from '$lib/gen'
|
|
import type { ScriptAdvancedSettingsFields } from '$lib/components/scriptSettings'
|
|
import { validateRetryConfig } from '$lib/utils'
|
|
|
|
// Single source for the per-step runtime settings: which apply, which are configured,
|
|
// and how each reads back. Graph badges, the run-settings accordion and the setting
|
|
// editors all read it here so they cannot drift. Script-level twin: `scriptSettings.ts`.
|
|
|
|
export type StepSettingKey =
|
|
| 'skip'
|
|
| 'early-stop'
|
|
| 'suspend'
|
|
| 'sleep'
|
|
| 'retries'
|
|
| 'error-handling'
|
|
| 'timeout'
|
|
| 'concurrency'
|
|
| 'priority'
|
|
| 'cache'
|
|
| 'debounce'
|
|
| 'lifetime'
|
|
|
|
export type StepSettingSummary = {
|
|
text: string
|
|
state: 'configured' | 'default' | 'invalid'
|
|
/** Render the text as code (it is a user-written expression). */
|
|
mono?: boolean
|
|
}
|
|
|
|
export type StepSettingView = {
|
|
key: StepSettingKey
|
|
label: string
|
|
/** Longer wording for hover surfaces; falls back to `label`. */
|
|
tooltip: string
|
|
icon: any
|
|
/** The setting's config is present on the step. Deliberately not "the runtime
|
|
* would behave differently" — a configured setting can still be a no-op
|
|
* (a sleep of 0), and `summary` says so rather than claiming it is off. */
|
|
configured: boolean
|
|
summary: StepSettingSummary
|
|
}
|
|
|
|
/** A step whose script polls an external system and returns the new items. */
|
|
export function isTriggerStep(module: FlowModule | undefined): boolean {
|
|
return (
|
|
module?.value != undefined &&
|
|
(module.value.type === 'script' || module.value.type === 'rawscript') &&
|
|
module.value.is_trigger === true
|
|
)
|
|
}
|
|
|
|
const def = (text: string): StepSettingSummary => ({ text, state: 'default' })
|
|
const inv = (text: string): StepSettingSummary => ({ text, state: 'invalid' })
|
|
const cfg = (text: string, mono = false): StepSettingSummary => ({
|
|
text,
|
|
state: 'configured',
|
|
mono
|
|
})
|
|
|
|
function formatDur(s: number | undefined): string {
|
|
if (s == null) return ''
|
|
if (s < 60) return `${s}s`
|
|
if (s < 3600) return `${Math.round(s / 60)} min`
|
|
return `${Math.round(s / 3600)} h`
|
|
}
|
|
|
|
/** Describe a user-written predicate. An empty expression is still configured —
|
|
* the setting is on, it just has nothing to evaluate yet. */
|
|
function exprSummary(expr: string | undefined): StepSettingSummary {
|
|
const e = expr?.trim()
|
|
if (!e) return cfg('No expression')
|
|
return e.length <= 24 ? cfg(e, true) : cfg('Expression set')
|
|
}
|
|
|
|
type Ctx = { referenced?: ScriptAdvancedSettingsFields }
|
|
|
|
type SettingSpec = {
|
|
label: string
|
|
tooltip?: (mod: FlowModule) => string
|
|
icon: any
|
|
applies?: (mod: FlowModule) => boolean
|
|
configured: (mod: FlowModule, ctx: Ctx) => boolean
|
|
summarize: (mod: FlowModule, ctx: Ctx) => StepSettingSummary
|
|
}
|
|
|
|
// A value the step itself carries counts as configured even when the runtime would
|
|
// ignore it — `summary` is what reports the no-op, and the setting editors keep their
|
|
// controls live on presence so clearing a field mid-edit can't disable it. Only values
|
|
// read off a referenced workspace script use the runtime's `> 0` test: nobody is typing
|
|
// into those, and 0 there simply means the script has no limit.
|
|
const isWorkspaceScript = (mod: FlowModule) => mod.value.type === 'script'
|
|
const inlineConcurrentLimit = (mod: FlowModule) =>
|
|
mod.value.type === 'rawscript' ? mod.value.concurrent_limit : undefined
|
|
const effectiveCacheTtl = (mod: FlowModule, ctx: Ctx) =>
|
|
mod.cache_ttl ?? (isWorkspaceScript(mod) ? ctx.referenced?.cache_ttl : undefined)
|
|
|
|
/** Whether an inline step carries a concurrency limit at all. The setting editor keeps its
|
|
* controls live on presence, so clearing the field mid-edit can't disable the input. A
|
|
* present-but-non-positive limit is surfaced as invalid rather than silently as "None".
|
|
* Strict: an emptied number input binds to `null`, which is still a value being typed. */
|
|
export function hasInlineConcurrency(mod: FlowModule): boolean {
|
|
return inlineConcurrentLimit(mod) !== undefined
|
|
}
|
|
|
|
/** Canonical order — every surface lists settings in this sequence. */
|
|
const SPECS: { key: StepSettingKey; spec: SettingSpec }[] = [
|
|
{
|
|
key: 'skip',
|
|
spec: {
|
|
label: 'Skip if',
|
|
icon: SkipForward,
|
|
configured: (m) => Boolean(m.skip_if),
|
|
summarize: (m) => (m.skip_if ? exprSummary(m.skip_if.expr) : def('Off'))
|
|
}
|
|
},
|
|
{
|
|
key: 'early-stop',
|
|
spec: {
|
|
label: 'Early stop / break',
|
|
tooltip: (m) =>
|
|
isTriggerStep(m) ? 'Stop early if there are no new events' : 'Early stop / break',
|
|
icon: CircleStop,
|
|
configured: (m) => m.stop_after_if != undefined || m.stop_after_all_iters_if != undefined,
|
|
summarize: (m) => {
|
|
// Both can be set on a sequential loop, so pick the first that carries an
|
|
// expression instead of letting an empty one mask the other.
|
|
const exprs = [m.stop_after_if?.expr, m.stop_after_all_iters_if?.expr].filter(
|
|
(e) => e != undefined
|
|
)
|
|
if (exprs.length === 0) return def('Off')
|
|
return exprSummary(exprs.find((e) => e?.trim()) ?? exprs[0])
|
|
}
|
|
}
|
|
},
|
|
{
|
|
key: 'suspend',
|
|
spec: {
|
|
label: 'Suspend until approval/resume',
|
|
icon: Hand,
|
|
configured: (m) => Boolean(m.suspend),
|
|
summarize: (m) => {
|
|
if (!m.suspend) return def('Off')
|
|
const n = m.suspend.required_events ?? 1
|
|
return cfg(`${n} approval${n > 1 ? 's' : ''}`)
|
|
}
|
|
}
|
|
},
|
|
{
|
|
key: 'sleep',
|
|
spec: {
|
|
label: 'Sleep',
|
|
icon: Moon,
|
|
configured: (m) => Boolean(m.sleep),
|
|
summarize: (m) => {
|
|
const s = m.sleep
|
|
if (!s) return def('Off')
|
|
if (s.type === 'static') {
|
|
const v = Number(s.value)
|
|
return Number.isFinite(v) ? cfg(`${formatDur(v)} after`) : cfg('Dynamic')
|
|
}
|
|
return cfg('Dynamic')
|
|
}
|
|
}
|
|
},
|
|
{
|
|
key: 'retries',
|
|
spec: {
|
|
label: 'Retries',
|
|
icon: RefreshCw,
|
|
configured: (m) => m.retry?.constant != undefined || m.retry?.exponential != undefined,
|
|
summarize: (m) => {
|
|
const r = m.retry
|
|
if (r?.constant == undefined && r?.exponential == undefined) return def('None')
|
|
if (validateRetryConfig(r)) return { text: 'Invalid', state: 'invalid' }
|
|
const isConstant = r?.constant != undefined
|
|
const n = (isConstant ? r?.constant?.attempts : r?.exponential?.attempts) ?? 0
|
|
const kind = isConstant ? 'constant' : 'exponential'
|
|
return cfg(`${n} attempt${n === 1 ? '' : 's'}, ${kind}`)
|
|
}
|
|
}
|
|
},
|
|
{
|
|
key: 'error-handling',
|
|
spec: {
|
|
label: 'Error handling',
|
|
icon: ShieldAlert,
|
|
configured: (m) => Boolean(m.continue_on_error),
|
|
summarize: (m) => (m.continue_on_error ? cfg('Continue on error') : def('Off'))
|
|
}
|
|
},
|
|
{
|
|
key: 'timeout',
|
|
spec: {
|
|
label: 'Timeout',
|
|
icon: Timer,
|
|
configured: (m) => m.timeout != null,
|
|
summarize: (m) => {
|
|
const t = m.timeout
|
|
if (t == null) return def('None')
|
|
if (typeof t === 'number') return cfg(formatDur(t))
|
|
if (t.type === 'static') {
|
|
const v = Number(t.value)
|
|
return Number.isFinite(v) ? cfg(formatDur(v)) : cfg('Dynamic')
|
|
}
|
|
return cfg('Dynamic')
|
|
}
|
|
}
|
|
},
|
|
{
|
|
key: 'concurrency',
|
|
spec: {
|
|
label: 'Concurrency limit',
|
|
icon: Gauge,
|
|
applies: (m) => m.value.type === 'rawscript' || m.value.type === 'script',
|
|
// Presence for the step's own limit (the user may be mid-edit, and `summary` says
|
|
// when it is a no-op), effectiveness for the referenced script's — a remote
|
|
// value nobody is typing into, where 0 just means the script has no limit.
|
|
configured: (m, ctx) =>
|
|
isWorkspaceScript(m)
|
|
? ctx.referenced?.concurrent_limit != undefined && ctx.referenced.concurrent_limit > 0
|
|
: hasInlineConcurrency(m),
|
|
summarize: (m, ctx) => {
|
|
if (isWorkspaceScript(m)) {
|
|
const l = ctx.referenced?.concurrent_limit
|
|
return l != undefined && l > 0 ? cfg(`Max ${l}`) : def('None')
|
|
}
|
|
const l = inlineConcurrentLimit(m)
|
|
if (l === undefined) return def('None')
|
|
if (!(l > 0)) return inv('Invalid limit')
|
|
const key = m.value.type === 'rawscript' ? m.value.custom_concurrency_key : undefined
|
|
return cfg(`Max ${l}${key ? ' per key' : ''}`)
|
|
}
|
|
}
|
|
},
|
|
{
|
|
key: 'priority',
|
|
spec: {
|
|
label: 'Priority',
|
|
icon: ChevronsUp,
|
|
configured: (m) => m.priority !== undefined,
|
|
summarize: (m) => {
|
|
if (m.priority === undefined) return def('Off')
|
|
// 0 is how the runtime spells "no priority".
|
|
return m.priority > 0 ? cfg('High priority') : inv('No priority set')
|
|
}
|
|
}
|
|
},
|
|
{
|
|
key: 'cache',
|
|
spec: {
|
|
label: 'Cache results',
|
|
icon: Database,
|
|
// The worker takes the module's cache_ttl over the referenced script's, so a
|
|
// module-level TTL must show here even for a workspace-script step.
|
|
configured: (m, ctx) =>
|
|
m.cache_ttl !== undefined || (isWorkspaceScript(m) && (ctx.referenced?.cache_ttl ?? 0) > 0),
|
|
summarize: (m, ctx) => {
|
|
const ttl = effectiveCacheTtl(m, ctx)
|
|
if (ttl === undefined) return def('Off')
|
|
return ttl > 0 ? cfg(formatDur(ttl)) : inv('No TTL set')
|
|
}
|
|
}
|
|
},
|
|
{
|
|
key: 'debounce',
|
|
spec: {
|
|
label: 'Debounce',
|
|
icon: Combine,
|
|
configured: (m) => m.debouncing?.debounce_delay_s !== undefined,
|
|
summarize: (m) => {
|
|
const d = m.debouncing?.debounce_delay_s
|
|
if (d === undefined) return def('Off')
|
|
return d > 0 ? cfg(`${formatDur(d)} debounce`) : inv('No delay set')
|
|
}
|
|
}
|
|
},
|
|
{
|
|
key: 'lifetime',
|
|
spec: {
|
|
label: 'Lifetime',
|
|
icon: Trash2,
|
|
configured: (m) => m.delete_after_secs != null,
|
|
summarize: (m) => {
|
|
const s = m.delete_after_secs
|
|
if (s == null) return def('Off')
|
|
return s === 0 ? cfg('Delete now') : cfg(`Delete after ${formatDur(s)}`)
|
|
}
|
|
}
|
|
}
|
|
]
|
|
|
|
/** The settings that apply to this step, in canonical order.
|
|
* `referenced` supplies the workspace script's own settings for `script` steps,
|
|
* whose concurrency and cache live on the script rather than on the step. */
|
|
export function describeStepSettings(
|
|
mod: FlowModule,
|
|
referenced?: ScriptAdvancedSettingsFields
|
|
): StepSettingView[] {
|
|
const ctx: Ctx = { referenced }
|
|
return SPECS.filter(({ spec }) => spec.applies?.(mod) ?? true).map(({ key, spec }) => ({
|
|
key,
|
|
label: spec.label,
|
|
tooltip: spec.tooltip?.(mod) ?? spec.label,
|
|
icon: spec.icon,
|
|
configured: spec.configured(mod, ctx),
|
|
summary: spec.summarize(mod, ctx)
|
|
}))
|
|
}
|
|
|
|
/** How a trigger step decides it has nothing to process. Stored on the step at
|
|
* creation, so changing it only affects newly created steps. */
|
|
// Falsy-or-empty, not `result == undefined`: a trigger returning nothing new may say so
|
|
// with any empty value, and stopping is always the right response.
|
|
const TRIGGER_STOP_EXPR = '!result || (Array.isArray(result) && result.length == 0)'
|
|
|
|
/** The config a setting is seeded with when it is switched on. Read by the setting
|
|
* editors and by every path that creates a step, so both agree. Settings absent from
|
|
* this map have no seeded config (the editor writes the value directly). */
|
|
const DEFAULTS = {
|
|
skip: () => ({ expr: 'false' }),
|
|
'early-stop': (kind?: 'trigger' | 'end') =>
|
|
kind === 'trigger'
|
|
? { expr: TRIGGER_STOP_EXPR, skip_if_stopped: true }
|
|
: kind === 'end'
|
|
? { expr: 'true', skip_if_stopped: false }
|
|
: {
|
|
expr: 'result == undefined',
|
|
skip_if_stopped: false,
|
|
error_message: undefined,
|
|
error_include_result: false
|
|
},
|
|
suspend: () => ({ required_events: 1, timeout: 1800 }),
|
|
sleep: () => ({ type: 'static' as const, value: 0 }),
|
|
cache: () => 600,
|
|
lifetime: () => 0,
|
|
priority: () => 100
|
|
} satisfies Partial<Record<StepSettingKey, (kind?: 'trigger' | 'end') => unknown>>
|
|
|
|
export type SeededSettingKey = keyof typeof DEFAULTS
|
|
|
|
/** Seeded config for a setting. Typed per key, so an unhandled key is a compile
|
|
* error rather than a silent `undefined`. */
|
|
export function stepSettingDefaults<K extends SeededSettingKey>(
|
|
key: K,
|
|
kind?: 'trigger' | 'end'
|
|
): ReturnType<(typeof DEFAULTS)[K]> {
|
|
return DEFAULTS[key](kind) as ReturnType<(typeof DEFAULTS)[K]>
|
|
}
|