diff --git a/frontend/src/lib/components/ScriptEditor.svelte b/frontend/src/lib/components/ScriptEditor.svelte index 1b14b38ed7..353a782e74 100644 --- a/frontend/src/lib/components/ScriptEditor.svelte +++ b/frontend/src/lib/components/ScriptEditor.svelte @@ -161,6 +161,13 @@ * orientation the flow editor uses for module step editing. */ previewLayout?: 'right' | 'bottom' + /** + * Fires whenever the test/preview run state changes — used by the + * pipeline editor to mirror the running script onto the canvas + * (animate its incoming/outgoing edges) so Test feels equivalent + * to clicking a run button on the graph. + */ + onTestStateChange?: (running: boolean) => void } let { @@ -195,9 +202,14 @@ modules = $bindable(undefined), editorBarRight, enablePreprocessorSnippet = false, - previewLayout = 'right' + previewLayout = 'right', + onTestStateChange }: Props = $props() + $effect(() => { + onTestStateChange?.(testIsLoading) + }) + let initialArgs = structuredClone($state.snapshot(args)) let jsonView = $state(false) let schemaHeight = $state(0) @@ -1485,8 +1497,16 @@ {@render editorContent()} {/if} - -
+ +
{#if showTabs}
@@ -1604,171 +1624,244 @@
{/if} - {#key previewLayout} - - - {#if previewLayout === 'bottom' && !(debugMode && isDebuggableScript)} -
- {#if testIsLoading} - - {:else} - - {/if} -
- {/if} - {#if jsonView} -
- { - if (e.detail) { - if (activeModuleTab !== null) { - testPanelArgs = e.detail - } else { - args = e.detail - } - } - }} - updateOnBlur={false} - placeholder={`Write args as JSON.

Example:

{
  "foo": "12"
}`} + {#if customUi?.previewPanel?.hideArgs} + +
+
+ {#if testIsLoading} +
+ Cancel + {:else} -
-
- {#key argsRender} - {#if activeModuleTab !== null} - runTest()} + unifiedSize="sm" + btnClasses="shadow-md" + variant="accent-secondary" + startIcon={{ icon: Play, classes: 'animate-none' }} + shortCut={{ Icon: CornerDownLeft }} + > + Test + + {/if} +
+ + {#if scriptProgress && !debugMode} + + {/if} + +
+ {:else} + {#key previewLayout} + + + {#if previewLayout === 'bottom' && !(debugMode && isDebuggableScript)} +
+ {#if testIsLoading} + + {:else} + {/if}
-
- {/if} - - - - {#if scriptProgress && !debugMode} - - {/if} - {#snippet capturesTab()} -
- + { + if (e.detail) { + if (activeModuleTab !== null) { + testPanelArgs = e.detail + } else { + args = e.detail + } + } + }} + updateOnBlur={false} + placeholder={`Write args as JSON.

Example:

{
  "foo": "12"
}`} />
- {/snippet} - {#snippet customResultPanel()} - - {/snippet} -
-
- - {/key} + {:else} +
+
+ {#key argsRender} + {#if activeModuleTab !== null} + + {:else} + + {/if} + {/key} + {#if showPsCommonParams} +
+ +
+ {/if} +
+
+ {/if} + + + + {#if scriptProgress && !debugMode} + + + {/if} + {#snippet capturesTab()} +
+ +
+ {/snippet} + {#snippet customResultPanel()} + + {/snippet} +
+
+ + {/key} + {/if} {/if}
diff --git a/frontend/src/lib/components/assets/AssetGraph/AssetGraphDetailsPane.svelte b/frontend/src/lib/components/assets/AssetGraph/AssetGraphDetailsPane.svelte index 088999fa64..c35102582b 100644 --- a/frontend/src/lib/components/assets/AssetGraph/AssetGraphDetailsPane.svelte +++ b/frontend/src/lib/components/assets/AssetGraph/AssetGraphDetailsPane.svelte @@ -110,6 +110,19 @@ // gets a "Recomputing…" banner so the user knows the rendered // snapshot is about to be replaced. activeRunnable?: { kind: 'script' | 'flow'; path: string } | undefined + // Forwarded from ScriptEditor when the user hits the Test button — + // gives the page a chance to mark the script as the active + // runnable on the canvas (animates its edges) so Test and the + // canvas Run button feel equivalent. + onTestStateChange?: (running: boolean) => void + // Counter pattern (cf. `requestRemoveSignal`) — bumped by the + // page when the user clicks the canvas Run button on the + // currently-open script, so we can route the dispatch through + // ScriptEditor.runTest() and surface the running state in the + // preview panel (logs/result/cancel) instead of just animating + // the edges. Counter rather than boolean so back-to-back runs + // re-fire the effect even if no other prop changed. + requestRunSignal?: number // Folder-scoped non-editable prefix shown next to the suffix // editor when the user renames a draft (e.g. `f//`). The // new path = pathPrefix + suffix. @@ -144,11 +157,24 @@ runsPendingJobId, onRunCompleted, activeRunnable, + onTestStateChange, + requestRunSignal, pathPrefix = '', onDraftPathChange, requestRemoveSignal }: 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 } | undefined = $state(undefined) + $effect(() => { + // Track the counter; ignore the initial 0/undefined. + const sig = requestRunSignal + if (sig === undefined || sig === 0) return + void scriptEditorRef?.runTest() + }) + // 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 @@ -671,6 +697,7 @@ {:else if script} {#key (script.hash ?? `draft:${script.path}`) + script.language} {#snippet editorBarRight()} diff --git a/frontend/src/lib/components/custom_ui.ts b/frontend/src/lib/components/custom_ui.ts index a5b0977d2d..8f0de35de7 100644 --- a/frontend/src/lib/components/custom_ui.ts +++ b/frontend/src/lib/components/custom_ui.ts @@ -61,6 +61,16 @@ export type PreviewPanelUi = { disableVariablePicker?: boolean disableDownload?: boolean tagLabel?: string + // Hide the args/SchemaForm pane entirely (use cases where the script + // is known to take no inputs — e.g. the pipeline editor's per-script + // preview). When set, the Test/Cancel button is rendered as a small + // floating affordance at the top-left of the preview area instead of + // inside the (now-absent) args column. + hideArgs?: boolean + // Render the LogPanel's logs/result as a left/right split instead of + // the default top/bottom. Pairs naturally with `hideArgs` when the + // preview area is the full bottom band. + logsResultSideBySide?: boolean } export type EditorBarUi = { diff --git a/frontend/src/lib/components/scriptEditor/LogPanel.svelte b/frontend/src/lib/components/scriptEditor/LogPanel.svelte index 23274d9c35..1842e65983 100644 --- a/frontend/src/lib/components/scriptEditor/LogPanel.svelte +++ b/frontend/src/lib/components/scriptEditor/LogPanel.svelte @@ -175,7 +175,7 @@
{:else} - + (undefined) + // Counter bumped when the canvas Run button targets the currently-open + // script — the pane intercepts and routes through ScriptEditor.runTest + // so logs/result/cancel land in the test panel instead of going off + // into nowhere with only edge animation as feedback. Counter rather + // than boolean so back-to-back runs re-fire. + let requestRunSignal = $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 @@ -1092,6 +1098,20 @@ // immediately — its background poll only kicks in // for already-listed in-flight jobs. if (!$workspaceStore || producer.kind !== 'script') return undefined + // 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 + // and the user can cancel from there. Same UX as + // hitting the Test button directly. + const openPath = + activeDraftPath ?? + (selection?.kind === 'runnable' && selection.runnable_kind === 'script' + ? selection.path + : undefined) + if (openPath === producer.path) { + requestRunSignal++ + return undefined + } let jobId: string | undefined if (producer.unsaved) { const draft = drafts.get(producer.path) @@ -1150,7 +1170,25 @@ {runsPendingJobId} {activeRunnable} onRunCompleted={() => (activeRunnable = undefined)} + onTestStateChange={(running) => { + // Bridge: ScriptEditor's Test button triggers the + // same canvas-level "is running" hint as the + // per-node Run button. The currently-edited script + // is whichever path is open in the pane (active + // draft, or the persisted-script selection). + const openPath = + activeDraftPath ?? + (selection?.kind === 'runnable' && selection.runnable_kind === 'script' + ? selection.path + : undefined) + if (running && openPath) { + activeRunnable = { kind: 'script', path: openPath } + } else if (!running && activeRunnable?.path === openPath) { + activeRunnable = undefined + } + }} {requestRemoveSignal} + {requestRunSignal} draftScript={activeDraft?.script} {pathPrefix} onDraftPathChange={renameDraft}