mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 00:03:08 +00:00
feat(pipeline): add run button on script nodes + recomputing hint on preview
This commit is contained in:
@@ -195,6 +195,25 @@
|
||||
partition_kind: r.partition_kind,
|
||||
freshness: r.freshness,
|
||||
unsaved: r.unsaved ?? false,
|
||||
// Same dispatch the asset node uses, only routed when the
|
||||
// runnable is a script (the page handler short-circuits
|
||||
// flows). The run button mirrors the asset-node affordance:
|
||||
// click → run with no extra UI clutter.
|
||||
onRunSelf:
|
||||
r.usage_kind === 'script' && onRunProducer
|
||||
? () =>
|
||||
onRunProducer({
|
||||
kind: 'script',
|
||||
path: r.path,
|
||||
unsaved: r.unsaved ?? false
|
||||
})
|
||||
: undefined,
|
||||
onSelectSelf: () =>
|
||||
onselect?.({
|
||||
kind: 'runnable',
|
||||
runnable_kind: r.usage_kind,
|
||||
path: r.path
|
||||
}),
|
||||
onRequestRemove: onRunnableMenuRemove
|
||||
? () =>
|
||||
onRunnableMenuRemove({
|
||||
|
||||
@@ -105,6 +105,11 @@
|
||||
// `activeRunnable` overlay (which animates edges around the
|
||||
// running script) once execution finishes.
|
||||
onRunCompleted?: () => void
|
||||
// Runnable currently executing — shared with the canvas. When it
|
||||
// matches one of the selected asset's producers, the preview
|
||||
// gets a "Recomputing…" banner so the user knows the rendered
|
||||
// snapshot is about to be replaced.
|
||||
activeRunnable?: { kind: 'script' | 'flow'; path: string } | undefined
|
||||
// Folder-scoped non-editable prefix shown next to the suffix
|
||||
// editor when the user renames a draft (e.g. `f/<folder>/`). The
|
||||
// new path = pathPrefix + suffix.
|
||||
@@ -138,11 +143,23 @@
|
||||
runsRefreshKey,
|
||||
runsPendingJobId,
|
||||
onRunCompleted,
|
||||
activeRunnable,
|
||||
pathPrefix = '',
|
||||
onDraftPathChange,
|
||||
requestRemoveSignal
|
||||
}: Props = $props()
|
||||
|
||||
// True when the script that writes to the currently-selected asset is
|
||||
// running right now. Drives the "Recomputing…" banner above the
|
||||
// preview and pairs with `onRunCompleted` (which bumps
|
||||
// `previewRefreshKey`) to reload the preview when the run finishes.
|
||||
let producerRunning = $derived(
|
||||
!!activeRunnable &&
|
||||
selectionProducers.some(
|
||||
(p) => p.kind === activeRunnable!.kind && p.path === activeRunnable!.path
|
||||
)
|
||||
)
|
||||
|
||||
// Bound from ScriptEditor — populated by inferAssets on every code
|
||||
// change. Forwarded to the page so the canvas can re-derive write
|
||||
// edges as the user edits the body (e.g. renaming a CREATE TABLE
|
||||
@@ -588,21 +605,43 @@
|
||||
not by asset kind, so it's useful for every asset. -->
|
||||
<Splitpanes horizontal class="!h-full">
|
||||
<Pane size={55} minSize={20}>
|
||||
{#if selection.asset_kind === 's3object'}
|
||||
<S3FilePreview
|
||||
fileKey={selection.path}
|
||||
showMetadata
|
||||
class="h-full"
|
||||
refreshKey={previewRefreshKey}
|
||||
/>
|
||||
{:else if selection.asset_kind === 'datatable'}
|
||||
<DataTablePreview path={selection.path} class="h-full" refreshKey={previewRefreshKey} />
|
||||
{:else}
|
||||
<div class="p-3 text-xs text-secondary">
|
||||
No inline preview yet for {selection.asset_kind}. Use the producer/consumer arrows in
|
||||
the graph to navigate. Runs of the upstream script are below.
|
||||
<div class="flex flex-col h-full">
|
||||
{#if producerRunning}
|
||||
<!-- The asset's producing script is executing right now;
|
||||
the snapshot below is stale until the run finishes,
|
||||
at which point onRunCompleted bumps previewRefreshKey
|
||||
and the preview component refetches automatically. -->
|
||||
<div
|
||||
class="shrink-0 flex items-center gap-2 px-3 py-1.5 text-2xs bg-blue-50 dark:bg-blue-950/40 border-b border-blue-200 dark:border-blue-900/60 text-blue-700 dark:text-blue-300"
|
||||
>
|
||||
<Loader2 size={12} class="animate-spin shrink-0" />
|
||||
<span class="truncate">
|
||||
Recomputing — preview will refresh when {activeRunnable?.path} finishes
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-1 min-h-0 relative">
|
||||
{#if selection.asset_kind === 's3object'}
|
||||
<S3FilePreview
|
||||
fileKey={selection.path}
|
||||
showMetadata
|
||||
class="h-full"
|
||||
refreshKey={previewRefreshKey}
|
||||
/>
|
||||
{:else if selection.asset_kind === 'datatable'}
|
||||
<DataTablePreview
|
||||
path={selection.path}
|
||||
class="h-full"
|
||||
refreshKey={previewRefreshKey}
|
||||
/>
|
||||
{:else}
|
||||
<div class="p-3 text-xs text-secondary">
|
||||
No inline preview yet for {selection.asset_kind}. Use the producer/consumer arrows
|
||||
in the graph to navigate. Runs of the upstream script are below.
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane size={45} minSize={20}>
|
||||
<AssetRunsPanel
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { Handle, Position } from '@xyflow/svelte'
|
||||
import { Code2, EllipsisVertical, GitBranch, Layers, Timer, Trash2 } from 'lucide-svelte'
|
||||
import {
|
||||
Code2,
|
||||
EllipsisVertical,
|
||||
GitBranch,
|
||||
Layers,
|
||||
Loader2,
|
||||
Play,
|
||||
Timer,
|
||||
Trash2
|
||||
} from 'lucide-svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { preventDefault, stopPropagation } from 'svelte/legacy'
|
||||
import type { GraphUsageKind } from './types'
|
||||
import { NODE } from '$lib/components/graph/util'
|
||||
import DropdownV2 from '$lib/components/DropdownV2.svelte'
|
||||
import type { Item } from '$lib/utils'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/utils'
|
||||
|
||||
interface Props {
|
||||
data: {
|
||||
@@ -18,14 +29,28 @@
|
||||
// True for nodes synthesized from local drafts (script not yet
|
||||
// persisted). Same convention as `unsaved` on triggers/edges.
|
||||
unsaved?: boolean
|
||||
// Page-supplied dispatch that runs THIS node (saved → runScriptByPath,
|
||||
// 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>
|
||||
// Called before running so the details pane focuses this script —
|
||||
// mirrors AssetNode.onSelectAsset, keeps the runs/output in view
|
||||
// instead of dispatching into nowhere.
|
||||
onSelectSelf?: () => void
|
||||
// Wired by the canvas. When set, the node renders an
|
||||
// EllipsisVertical hover-button that opens a small action menu —
|
||||
// "Discard" for drafts, "Delete…" (which the page maps to its
|
||||
// archive/delete confirmation flow) for persisted scripts.
|
||||
onRequestRemove?: () => void
|
||||
}
|
||||
// SvelteFlow injects this when the user clicks the node. Combined with
|
||||
// hover state to drive the run-button visibility (same pattern as
|
||||
// AssetNode).
|
||||
selected?: boolean
|
||||
}
|
||||
let { data }: Props = $props()
|
||||
let { data, selected = false }: Props = $props()
|
||||
|
||||
// Icon + emerald accent already convey "pipeline script" vs "flow"; the
|
||||
// uppercase kind label was visually noisy and redundant. Tooltip on hover
|
||||
@@ -41,6 +66,27 @@
|
||||
|
||||
let hover = $state(false)
|
||||
let menuOpen = $state(false)
|
||||
let running = $state(false)
|
||||
let canRun = $derived(data.runnable_kind === 'script' && data.onRunSelf != undefined)
|
||||
// Reveal pattern matches AssetNode: visible while hovering, selected, or
|
||||
// already running (so the loader doesn't disappear under the cursor).
|
||||
let showRun = $derived(canRun && (hover || selected || running))
|
||||
|
||||
async function runSelf(e: MouseEvent) {
|
||||
e.stopPropagation()
|
||||
if (!$workspaceStore || running || !data.onRunSelf) return
|
||||
// Focus this runnable so the details pane opens to its editor — same
|
||||
// rationale as AssetNode.onSelectAsset before runProducers.
|
||||
if (!selected) data.onSelectSelf?.()
|
||||
running = true
|
||||
try {
|
||||
await data.onRunSelf()
|
||||
} catch (err: any) {
|
||||
sendUserToast(`Failed to run: ${err.body ?? err.message}`, true)
|
||||
} finally {
|
||||
running = false
|
||||
}
|
||||
}
|
||||
|
||||
let menuItems: Item[] = $derived(
|
||||
data.onRequestRemove
|
||||
@@ -103,6 +149,27 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if showRun}
|
||||
<!-- Run button revealed on hover/select. Matches the placement, size,
|
||||
and behaviour of AssetNode's run button so both nodes feel
|
||||
consistent. Drafts are runnable too (the page handler routes to
|
||||
runScriptPreview), so no greyed-out state. -->
|
||||
<div class="absolute -left-3 top-1/2 -translate-y-1/2 z-10">
|
||||
<button
|
||||
type="button"
|
||||
onclick={runSelf}
|
||||
disabled={running}
|
||||
class="bg-blue-500 hover:bg-blue-600 disabled:opacity-60 text-white rounded-full w-6 h-6 grid place-items-center shadow border-2 border-surface-secondary leading-none"
|
||||
title={`Run ${data.path}${data.unsaved ? ' (draft, runs as preview)' : ''}`}
|
||||
>
|
||||
{#if running}
|
||||
<Loader2 size={14} class="animate-spin" />
|
||||
{:else}
|
||||
<Play size={14} strokeWidth={2.5} class="translate-x-px" />
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if menuItems.length > 0}
|
||||
<!--
|
||||
|
||||
@@ -1148,6 +1148,7 @@
|
||||
selectionProducers={activeDraft ? [] : selectionProducers}
|
||||
{runsRefreshKey}
|
||||
{runsPendingJobId}
|
||||
{activeRunnable}
|
||||
onRunCompleted={() => (activeRunnable = undefined)}
|
||||
{requestRemoveSignal}
|
||||
draftScript={activeDraft?.script}
|
||||
|
||||
Reference in New Issue
Block a user