Flow inputs and outputs are visible and editable in the UI
A flow's inputs are the messages it takes from outside — a dashboard control, a run, the API — and its outputs are what a batch run reports. Both existed in the document and in the engine, and neither had any UI: the values looked hard-coded on the canvas and the Run button always used the declared defaults. The canvas now draws each as a labelled endpoint, the way it already draws a dashboard tile or another flow, skipping an input something else already accounts for. The flow panel edits them — mode, name, type, starting value, and for a live flow the value it currently holds with a way to put a new one in. Pressing Run on a batch flow asks for its parameters first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUb8YpL2s3gmN9WTACTt4q
This commit is contained in:
@@ -47,7 +47,13 @@ import { inCodeEditor, useShortcuts } from "@/lib/shortcuts"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CanvasTitle } from "./CanvasTitle"
|
||||
import { CommandPalette } from "./CommandPalette"
|
||||
import { bindingsKey, deriveEdges, portOf, qualify } from "./deriveEdges"
|
||||
import {
|
||||
bindingsKey,
|
||||
boundaryKey,
|
||||
deriveEdges,
|
||||
portOf,
|
||||
qualify,
|
||||
} from "./deriveEdges"
|
||||
import { EdgeInspector, type InspectedEdge } from "./EdgeInspector"
|
||||
import { EndpointNode } from "./EndpointNode"
|
||||
import { deriveEndpoints, ENDPOINT_TYPE, isEndpointNode } from "./endpoints"
|
||||
@@ -57,6 +63,7 @@ import { FlowPanel } from "./FlowPanel"
|
||||
import { LiveEdge } from "./LiveEdge"
|
||||
import { type Direction, layoutGraph } from "./layout"
|
||||
import { NodePanel } from "./NodePanel"
|
||||
import { RunDialog } from "./RunDialog"
|
||||
import "./flow.css"
|
||||
import { liveStore, useFlowPaused } from "./liveStore"
|
||||
import {
|
||||
@@ -228,6 +235,8 @@ function FlowEditorInner({
|
||||
const [rebind, setRebind] = useState<Rebind | null>(null)
|
||||
const [renamed, setRenamed] = useState<MessageRename | null>(null)
|
||||
const [flowPanelOpen, setFlowPanelOpen] = useState(false)
|
||||
// A batch run is asked for its parameters before it is submitted.
|
||||
const [runOpen, setRunOpen] = useState(false)
|
||||
// Throwing an edit away is offered from the dock, so its confirmation lives
|
||||
// here rather than inside the settings panel.
|
||||
const [discardOpen, setDiscardOpen] = useState(false)
|
||||
@@ -368,8 +377,9 @@ function FlowEditorInner({
|
||||
)
|
||||
|
||||
// A cheap fingerprint of the wiring, and the only thing the layout depends
|
||||
// on: what the graph looks like follows from what is wired to what.
|
||||
const key = bindingsKey(definitions)
|
||||
// on: what the graph looks like follows from what is wired to what. The
|
||||
// flow's own boundary is drawn too, so a declared input counts as wiring.
|
||||
const key = `${bindingsKey(definitions)}|${boundaryKey(flowDoc)}`
|
||||
|
||||
// Offer the names already in play: everything published is worth reading,
|
||||
// and an input nobody provides yet is worth publishing.
|
||||
@@ -395,7 +405,11 @@ function FlowEditorInner({
|
||||
const openEndpoint = useCallback(
|
||||
(id: string) => {
|
||||
const [kind, rest] = id.split(":", 2)
|
||||
if (kind === "dashboard") {
|
||||
if (kind === "input" || kind === "output") {
|
||||
// This flow's own boundary, declared in its settings.
|
||||
setSelectedId(null)
|
||||
setFlowPanelOpen(true)
|
||||
} else if (kind === "dashboard") {
|
||||
navigate({
|
||||
to: "/dashboards/$name",
|
||||
params: { name: (rest ?? "").split(":")[0] },
|
||||
@@ -448,7 +462,12 @@ function FlowEditorInner({
|
||||
// canvasNodes, so an autosave, an undo or a delete cannot reach them.
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: the key covers the wiring, which is all these depend on.
|
||||
const external = useMemo(() => {
|
||||
const built = deriveEndpoints(detail.endpoints ?? [], definitions, flowName)
|
||||
const built = deriveEndpoints(
|
||||
detail.endpoints ?? [],
|
||||
definitions,
|
||||
flowName,
|
||||
flowDoc,
|
||||
)
|
||||
return {
|
||||
...built,
|
||||
nodes: built.nodes.map((node) => {
|
||||
@@ -552,8 +571,8 @@ function FlowEditorInner({
|
||||
}, [direction, definitions.length, external.nodes.length, fitView])
|
||||
|
||||
const runMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
FlowsService.runFlow({ name: flowName, requestBody: { inputs: {} } }),
|
||||
mutationFn: (inputs: Record<string, unknown> = {}) =>
|
||||
FlowsService.runFlow({ name: flowName, requestBody: { inputs } }),
|
||||
onSuccess: (state) => {
|
||||
liveStore.setValues(
|
||||
Object.fromEntries(
|
||||
@@ -568,6 +587,19 @@ function FlowEditorInner({
|
||||
showErrorToast("The flow could not run. Check the node errors."),
|
||||
})
|
||||
|
||||
/**
|
||||
* Pressing Run.
|
||||
*
|
||||
* A batch flow is submitted as a run, and a run is identified by its
|
||||
* parameters — so it asks for them rather than quietly using the defaults.
|
||||
*/
|
||||
const startRun = useCallback(async () => {
|
||||
// Running executes what is stored, so the queued edit goes first.
|
||||
await flush()
|
||||
if (latest.current.mode === "batch") setRunOpen(true)
|
||||
else runMutation.mutate({})
|
||||
}, [flush, runMutation])
|
||||
|
||||
const enableMutation = useMutation({
|
||||
mutationFn: (next: boolean) =>
|
||||
next
|
||||
@@ -1044,11 +1076,7 @@ function FlowEditorInner({
|
||||
onClearNode: () => setLogsNode(null),
|
||||
}}
|
||||
onAddNode={() => setPaletteOpen(true)}
|
||||
onRun={async () => {
|
||||
// Running executes what is stored, so the queued edit goes first.
|
||||
await flush()
|
||||
runMutation.mutate()
|
||||
}}
|
||||
onRun={startRun}
|
||||
onTogglePause={() => pauseMutation.mutate(!paused)}
|
||||
onStep={() => stepMutation.mutate()}
|
||||
stepping={stepMutation.isPending}
|
||||
@@ -1125,7 +1153,18 @@ function FlowEditorInner({
|
||||
flows={flows.data}
|
||||
onAddNode={addNode}
|
||||
onAddSharedNode={(libName) => addNode("python", libName)}
|
||||
onRun={() => runMutation.mutate()}
|
||||
onRun={startRun}
|
||||
/>
|
||||
|
||||
<RunDialog
|
||||
open={runOpen}
|
||||
definition={flowDoc}
|
||||
pending={runMutation.isPending}
|
||||
onOpenChange={setRunOpen}
|
||||
onRun={(params) => {
|
||||
setRunOpen(false)
|
||||
runMutation.mutate(params)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
|
||||
Reference in New Issue
Block a user