mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-20 00:02:19 +00:00
feat(pipeline): cascade option on graph Run + match button heights
This commit is contained in:
@@ -679,7 +679,15 @@
|
||||
args = nargs
|
||||
}
|
||||
|
||||
export async function runTest() {
|
||||
export async function runTest(opts?: { cascade?: boolean }) {
|
||||
// When the caller forces a cascade choice (e.g. the canvas runnable
|
||||
// menu's "Run + trigger N downstream"), also flip the persistent
|
||||
// `cascadeDownstream` state so the split button's label/icon reflect
|
||||
// the active mode after the run kicks off — keeps "what mode am I in"
|
||||
// visible instead of having a one-off run silently disagree with the
|
||||
// UI. The caller is welcome to bump the signal repeatedly; we just
|
||||
// keep the latest choice as the active mode.
|
||||
if (opts?.cascade !== undefined) cascadeDownstream = opts.cascade
|
||||
// Discard any previous recording when running a normal test
|
||||
if (!scriptRecording.active) {
|
||||
lastRecording = undefined
|
||||
@@ -1717,9 +1725,13 @@
|
||||
contentClasses="p-0"
|
||||
>
|
||||
{#snippet trigger()}
|
||||
<!-- min-h-7 matches Button's unifiedSize="sm" (28px) so
|
||||
the caret stretches to the same height as the
|
||||
primary Test button. self-stretch falls back if the
|
||||
parent's items-stretch is overridden upstream. -->
|
||||
<button
|
||||
type="button"
|
||||
class="px-1.5 flex items-center justify-center bg-blue-100 dark:bg-blue-900/30 hover:bg-blue-200 dark:hover:bg-blue-900/60 text-primary border-l border-blue-300 dark:border-blue-700 transition-colors"
|
||||
class="self-stretch min-h-7 px-1.5 flex items-center justify-center bg-blue-100 dark:bg-blue-900/30 hover:bg-blue-200 dark:hover:bg-blue-900/60 text-primary border-l border-blue-300 dark:border-blue-700 transition-colors"
|
||||
title="Run options"
|
||||
aria-label="Run options"
|
||||
>
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
kind: 'script' | 'flow'
|
||||
path: string
|
||||
unsaved?: boolean
|
||||
// Whether to let the asset-trigger cascade fan out to downstream
|
||||
// subscribers after this run succeeds. Undefined = use the caller's
|
||||
// default (no skip arg injected); true = explicit cascade; false =
|
||||
// inject `_wmill_skip_asset_dispatch: true` to suppress.
|
||||
cascade?: boolean
|
||||
}) => Promise<string | undefined>
|
||||
// Page-supplied dispatch for the per-runnable-node action menu.
|
||||
// Drafts are discarded immediately by the page; persisted scripts
|
||||
@@ -172,6 +177,34 @@
|
||||
producersByAsset.set(key, list)
|
||||
}
|
||||
|
||||
// Downstream subscriber count per producer script. Counts distinct
|
||||
// script subscribers (excluding self and flow subs, mirroring the V1
|
||||
// dispatch policy) across all assets the script writes. Drives the
|
||||
// "Run + trigger N downstream" menu item on RunnableNode and lets the
|
||||
// pipeline page short-circuit cascade UX when there's nothing to fan
|
||||
// out to.
|
||||
const subscribersByAsset = new Map<string, Set<string>>()
|
||||
for (const t of g.triggers ?? []) {
|
||||
if (t.trigger_kind !== 'asset' || t.runnable_kind !== 'script') continue
|
||||
const key = `${t.asset_kind}:${t.asset_path}`
|
||||
const set = subscribersByAsset.get(key) ?? new Set<string>()
|
||||
set.add(t.runnable_path)
|
||||
subscribersByAsset.set(key, set)
|
||||
}
|
||||
const downstreamSetsByScript = new Map<string, Set<string>>()
|
||||
for (const e of g.edges ?? []) {
|
||||
if (e.runnable_kind !== 'script') continue
|
||||
const access = e.access_type ?? 'r'
|
||||
if (access !== 'w' && access !== 'rw') continue
|
||||
const subs = subscribersByAsset.get(`${e.asset_kind}:${e.asset_path}`)
|
||||
if (!subs) continue
|
||||
const merged = downstreamSetsByScript.get(e.runnable_path) ?? new Set<string>()
|
||||
for (const s of subs) if (s !== e.runnable_path) merged.add(s)
|
||||
downstreamSetsByScript.set(e.runnable_path, merged)
|
||||
}
|
||||
const downstreamByScript = new Map<string, number>()
|
||||
for (const [path, set] of downstreamSetsByScript) downstreamByScript.set(path, set.size)
|
||||
|
||||
for (const a of g.assets) {
|
||||
const assetId = `asset:${a.kind}:${a.path}`
|
||||
nodes.push({
|
||||
@@ -206,13 +239,15 @@
|
||||
// click → run with no extra UI clutter.
|
||||
onRunSelf:
|
||||
r.usage_kind === 'script' && onRunProducer
|
||||
? () =>
|
||||
? (opts?: { cascade?: boolean }) =>
|
||||
onRunProducer({
|
||||
kind: 'script',
|
||||
path: r.path,
|
||||
unsaved: r.unsaved ?? false
|
||||
unsaved: r.unsaved ?? false,
|
||||
cascade: opts?.cascade
|
||||
})
|
||||
: undefined,
|
||||
downstreamCount: downstreamByScript.get(r.path) ?? 0,
|
||||
onSelectSelf: () =>
|
||||
onselect?.({
|
||||
kind: 'runnable',
|
||||
|
||||
@@ -151,6 +151,11 @@
|
||||
// cascade option when > 0. The page computes this from the graph
|
||||
// edges + triggers + currently-open path.
|
||||
downstreamSubscribers?: number
|
||||
// Sister to `requestRunSignal`. When bumped, the bridge calls
|
||||
// `ScriptEditor.runTest({ cascade: true })` — used by the canvas
|
||||
// runnable menu's "Run + trigger N downstream" item when the chosen
|
||||
// script happens to be the currently-open one.
|
||||
requestRunCascadeSignal?: number
|
||||
}
|
||||
let {
|
||||
selection,
|
||||
@@ -176,19 +181,28 @@
|
||||
pathPrefix = '',
|
||||
onDraftPathChange,
|
||||
requestRemoveSignal,
|
||||
downstreamSubscribers = 0
|
||||
downstreamSubscribers = 0,
|
||||
requestRunCascadeSignal
|
||||
}: Props = $props()
|
||||
|
||||
// Held ref to ScriptEditor so we can route a canvas-side Run dispatch
|
||||
// through .runTest() — gives the test panel logs/result/cancel for
|
||||
// runs initiated from the graph, not just the in-pane Test button.
|
||||
let scriptEditorRef: { runTest: () => Promise<unknown> } | undefined = $state(undefined)
|
||||
let scriptEditorRef: { runTest: (opts?: { cascade?: boolean }) => Promise<unknown> } | undefined =
|
||||
$state(undefined)
|
||||
$effect(() => {
|
||||
// Track the counter; ignore the initial 0/undefined.
|
||||
const sig = requestRunSignal
|
||||
if (sig === undefined || sig === 0) return
|
||||
void scriptEditorRef?.runTest()
|
||||
})
|
||||
$effect(() => {
|
||||
// Cascade-explicit sister signal: same pattern, but forces the
|
||||
// runTest call to opt into the asset-trigger cascade for this run.
|
||||
const sig = requestRunCascadeSignal
|
||||
if (sig === undefined || sig === 0) return
|
||||
void scriptEditorRef?.runTest({ cascade: true })
|
||||
})
|
||||
|
||||
// True when the script that writes to the currently-selected asset is
|
||||
// running right now. Drives the "Recomputing…" banner above the
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
Loader2,
|
||||
Play,
|
||||
Timer,
|
||||
Trash2
|
||||
Trash2,
|
||||
Zap
|
||||
} from 'lucide-svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { preventDefault, stopPropagation } from 'svelte/legacy'
|
||||
@@ -33,8 +34,16 @@
|
||||
// unsaved → runScriptPreview with the locally-cached draft content).
|
||||
// Wired only for script runnables — flows are ignored upstream. When
|
||||
// undefined, the play button is hidden — matches the asset-node
|
||||
// behaviour outside editor contexts.
|
||||
onRunSelf?: () => Promise<string | undefined>
|
||||
// behaviour outside editor contexts. `opts.cascade` lets the
|
||||
// dispatcher decide whether to skip the asset-trigger cascade — the
|
||||
// node's default-click sends `cascade: false` (just this step) to
|
||||
// stay consistent with the editor's Test button.
|
||||
onRunSelf?: (opts?: { cascade?: boolean }) => Promise<string | undefined>
|
||||
// Number of script subscribers that listen on assets this script
|
||||
// writes. When > 0, the hover-menu exposes a "Run + trigger N
|
||||
// downstream" alternative; the round Play button stays a single-
|
||||
// click default. Undefined / 0 hides the cascade menu item.
|
||||
downstreamCount?: number
|
||||
// Called before running so the details pane focuses this script —
|
||||
// mirrors AssetNode.onSelectAsset, keeps the runs/output in view
|
||||
// instead of dispatching into nowhere.
|
||||
@@ -72,7 +81,7 @@
|
||||
// already running (so the loader doesn't disappear under the cursor).
|
||||
let showRun = $derived(canRun && (hover || selected || running))
|
||||
|
||||
async function runSelf(e: MouseEvent) {
|
||||
async function runSelf(e: MouseEvent, cascade?: boolean) {
|
||||
e.stopPropagation()
|
||||
if (!$workspaceStore || running || !data.onRunSelf) return
|
||||
// Focus this runnable so the details pane opens to its editor — same
|
||||
@@ -80,7 +89,7 @@
|
||||
if (!selected) data.onSelectSelf?.()
|
||||
running = true
|
||||
try {
|
||||
await data.onRunSelf()
|
||||
await data.onRunSelf(cascade != undefined ? { cascade } : undefined)
|
||||
} catch (err: any) {
|
||||
sendUserToast(`Failed to run: ${err.body ?? err.message}`, true)
|
||||
} finally {
|
||||
@@ -88,8 +97,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
let menuItems: Item[] = $derived(
|
||||
data.onRequestRemove
|
||||
let menuItems: Item[] = $derived([
|
||||
// Cascade option lives at the top of the menu — the round Run button
|
||||
// is the no-cascade default, this surfaces the alternative when there
|
||||
// are downstream subscribers to fan out to.
|
||||
...(canRun && (data.downstreamCount ?? 0) > 0
|
||||
? [
|
||||
{
|
||||
displayName: `Run + trigger ${data.downstreamCount} downstream`,
|
||||
icon: Zap,
|
||||
action: () => {
|
||||
// Fake MouseEvent-shaped arg so runSelf can stopPropagation.
|
||||
void runSelf({ stopPropagation: () => {} } as MouseEvent, true)
|
||||
}
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(data.onRequestRemove
|
||||
? [
|
||||
{
|
||||
displayName: data.unsaved ? 'Discard' : 'Delete…',
|
||||
@@ -98,8 +122,8 @@
|
||||
action: () => data.onRequestRemove?.()
|
||||
}
|
||||
]
|
||||
: []
|
||||
)
|
||||
: [])
|
||||
])
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
|
||||
@@ -923,6 +923,11 @@
|
||||
// into nowhere with only edge animation as feedback. Counter rather
|
||||
// than boolean so back-to-back runs re-fire.
|
||||
let requestRunSignal = $state(0)
|
||||
// Sister counter: bumped instead of requestRunSignal when the canvas user
|
||||
// picks "Run + trigger N downstream" for the currently-open script. Lets
|
||||
// ScriptEditor.runTest run with cascade=true without permanently flipping
|
||||
// its persistent cascade choice.
|
||||
let requestRunCascadeSignal = $state(0)
|
||||
// Counter bumped from the runnable-node action menu to ask the pane to
|
||||
// open its archive/delete confirmation modal for the loaded script.
|
||||
// Counter (vs boolean) so successive triggers re-fire even if the user
|
||||
@@ -1281,6 +1286,13 @@
|
||||
// immediately — its background poll only kicks in
|
||||
// for already-listed in-flight jobs.
|
||||
if (!$workspaceStore || producer.kind !== 'script') return undefined
|
||||
// Cascade default: same as the Test button — `cascade`
|
||||
// undefined / false skips the asset-trigger dispatch
|
||||
// via `_wmill_skip_asset_dispatch`; explicit `true`
|
||||
// lets the dispatch fire normally. The asset-node
|
||||
// affordance still passes `undefined` (legacy callers),
|
||||
// which we treat as "skip" for consistency.
|
||||
const cascade = producer.cascade === true
|
||||
// If the producer being run is the script currently
|
||||
// edited in the pane, route through ScriptEditor's
|
||||
// Test path — the test panel then shows logs/result
|
||||
@@ -1292,9 +1304,11 @@
|
||||
? selection.path
|
||||
: undefined)
|
||||
if (openPath === producer.path) {
|
||||
requestRunSignal++
|
||||
if (cascade) requestRunCascadeSignal++
|
||||
else requestRunSignal++
|
||||
return undefined
|
||||
}
|
||||
const skipArg = cascade ? {} : { _wmill_skip_asset_dispatch: true }
|
||||
let jobId: string | undefined
|
||||
if (producer.unsaved) {
|
||||
const draft = drafts.get(producer.path)
|
||||
@@ -1305,14 +1319,14 @@
|
||||
content: draft.script.content,
|
||||
language: draft.script.language,
|
||||
path: producer.path,
|
||||
args: {}
|
||||
args: { ...skipArg }
|
||||
}
|
||||
})
|
||||
} else {
|
||||
jobId = await JobService.runScriptByPath({
|
||||
workspace: $workspaceStore,
|
||||
path: producer.path,
|
||||
requestBody: {}
|
||||
requestBody: { ...skipArg }
|
||||
})
|
||||
}
|
||||
if (jobId) {
|
||||
@@ -1373,6 +1387,7 @@
|
||||
}}
|
||||
{requestRemoveSignal}
|
||||
{requestRunSignal}
|
||||
{requestRunCascadeSignal}
|
||||
draftScript={activeDraft?.script}
|
||||
{pathPrefix}
|
||||
onDraftPathChange={renameDraft}
|
||||
|
||||
Reference in New Issue
Block a user