Просмотр исходного кода

feat: show when an execution was only a single-node test

executeToNode ("Run This Node") deliberately trims a loop body to one
item and skips everything downstream of the target. Until now the
stored record kept no trace of that, so such a run rendered exactly
like a full one - a loop that read 10 items and ran one iteration
looked broken.

Build against the engine's new singleNodeTarget field on the execution
record (the id of the targeted node, absent on a normal run and on any
execution recorded before this landed). Carry it through both live
"Run This Node" runs (known immediately, before the record exists) and
pinned/viewed past executions (read back from the stored record), and
show a plain banner in the results panel naming the node and stating
that one loop iteration is expected here, not a fault.
fszontagh 1 месяц назад
Родитель
Сommit
59211a98b4

+ 7 - 0
webui/src/api/workflows.ts

@@ -496,6 +496,13 @@ export interface ExecutionDetail {
   stoppedNodeId?: string
   output?: any
   workflowSnapshot?: WorkflowSnapshot
+  // Set only when this run came from "Test this node": the id of the node
+  // that was targeted. The run executed that node plus its upstream, and a
+  // loop body was deliberately trimmed to a single item, so a loop showing
+  // one iteration here is expected, not a fault. Absent on a normal run, and
+  // absent entirely on executions recorded before the engine started sending
+  // it - treat its absence as "not a single-node test", never as an error.
+  singleNodeTarget?: string
 }
 
 export interface ExecutionListItem {

+ 28 - 1
webui/src/components/workflow/ExecutionResultsPanel.tsx

@@ -1,6 +1,6 @@
 import { useEffect, useMemo, useState } from 'react'
 import { Node, Edge } from 'reactflow'
-import { X, ChevronRight, ChevronDown, Loader2, CheckCircle, XCircle, SkipForward, Ban, Repeat } from 'lucide-react'
+import { X, ChevronRight, ChevronDown, Loader2, CheckCircle, XCircle, SkipForward, Ban, Repeat, FlaskConical } from 'lucide-react'
 import { NodeExecutionState } from './WorkflowNode'
 import { NodeOutputView } from './NodeOutputView'
 import type { NodeExecution } from '../../api/workflows'
@@ -18,6 +18,9 @@ interface ExecutionState {
   // record) means "fall back to the flat per-node list", which is exactly
   // how this panel behaved before grouping existed.
   rawNodeExecutions?: NodeExecution[]
+  // The targeted node's id when this run was a "Test this node" run - see
+  // WorkflowEditorPage. Drives the banner below; absent means a normal run.
+  singleNodeTarget?: string
 }
 
 // One pass of a loop body: every node execution that shares a loopNodeId and
@@ -576,6 +579,14 @@ export function ExecutionResultsPanel({
     )
   }
 
+  // Label for the "Test this node" banner below. Falls back to the raw id
+  // if the node isn't on the canvas anymore (renamed, deleted since the run).
+  const singleNodeTargetLabel = useMemo(() => {
+    if (!executionState.singleNodeTarget) return null
+    const node = nodes.find(n => n.id === executionState.singleNodeTarget)
+    return node?.data.config?._customLabel || node?.data.label || executionState.singleNodeTarget
+  }, [executionState.singleNodeTarget, nodes])
+
   const hasAnyResults = timeline
     ? timeline.length > 0
     : Object.keys(executionState.nodeStates).length > 0
@@ -631,6 +642,22 @@ export function ExecutionResultsPanel({
         )}
       </div>
 
+      {/* This run only tested one node (plus its upstream) - the single fact
+          that would have saved an hour of "why did the loop only do one
+          item" confusion. Shown as its own block so it can't be missed the
+          way a line buried in the summary above could be. */}
+      {singleNodeTargetLabel && (
+        <div className="flex-shrink-0 px-4 py-3 border-b border-gray-200 dark:border-slate-700 bg-amber-50 dark:bg-amber-900/20 flex items-start gap-2">
+          <FlaskConical className="w-4 h-4 text-amber-600 dark:text-amber-400 shrink-0 mt-0.5" />
+          <div className="text-xs text-amber-800 dark:text-amber-300">
+            <span className="font-medium">Single-node test:</span> this run tested only{' '}
+            <span className="font-medium">{singleNodeTargetLabel}</span> and the nodes upstream
+            of it - nothing downstream ran. If a loop appears here, it ran a single item on
+            purpose. One iteration is expected, not a fault.
+          </div>
+        </div>
+      )}
+
       {/* Node results, in the order they actually ran. Loop bodies collapse
           into one card per iteration when the execution has loopNodeId /
           loopIteration on its records; otherwise this is the same flat,

+ 10 - 0
webui/src/pages/WorkflowEditorPage.tsx

@@ -75,6 +75,11 @@ interface ExecutionState {
   // iteration using loopNodeId/loopIteration. Absent for a live run and for
   // executions recorded before the engine sent those fields.
   rawNodeExecutions?: NodeExecution[]
+  // Set when this run is (or was) a "Test this node" run - the id of the
+  // targeted node. Set directly at execute time for a live run, and read
+  // back from the stored record for a pinned/viewed one; absent for a normal
+  // run and for anything recorded before the engine sent this field.
+  singleNodeTarget?: string
 }
 
 interface StorageCollection {
@@ -545,6 +550,7 @@ function WorkflowEditorInner() {
         // run keeps building nodeStates incrementally via the websocket
         // handlers below and never populates this.
         rawNodeExecutions: sortedExecutions,
+        singleNodeTarget: pinnedExecution.singleNodeTarget,
       }
     }
     return executionState
@@ -1118,6 +1124,10 @@ function WorkflowEditorInner() {
       status: 'running',
       nodeStates: {},
       startedAt: Date.now(),
+      // Known up front, unlike everything else in this state - we're the
+      // ones asking for a single-node run, so the panel can say so from the
+      // first frame rather than waiting for the stored record to come back.
+      singleNodeTarget: targetNodeId,
     })
     setSelectedIterations({})
     setShowExecutionPanel(true)