Add flow settings, pulse emitting nodes, and simplify node state
- One dot per node now carries the whole story: primary while running, sage after a good run, red when anything is wrong, with the explanation on hover. The corner badge is gone, along with the second way of saying the same thing. - A node that publishes something flashes a ring, so a running flow is legible without reading the edge values. Nodes that consume but publish nothing stay quiet, which is why the event carries an output count. - Flow settings open in the same panel its nodes use, from a pencil in the dock: the title, the name, and deleting the flow. NodePanel and FlowPanel share the panel chrome rather than each drawing their own. - Renaming is a server operation, because a flow's name is the namespace of its messages: the directory moves and every other flow reading `old.message` is repointed, instead of being left pointing at a flow that no longer exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WzrvW7rjQbynnhF6pxh6i
This commit is contained in:
co-authored by
Claude Fable 5
parent
c254d487ba
commit
fd666743d2
@@ -41,12 +41,14 @@ import { bindingsKey, deriveEdges, portOf, qualify } from "./deriveEdges"
|
||||
import { EdgeInspector, type InspectedEdge } from "./EdgeInspector"
|
||||
import { FlowDock } from "./FlowDock"
|
||||
import { FlowNode, type FlowNodeData } from "./FlowNode"
|
||||
import { FlowPanel } from "./FlowPanel"
|
||||
import { FlowTabs } from "./FlowTabs"
|
||||
import { LiveEdge } from "./LiveEdge"
|
||||
import { NodePanel } from "./NodePanel"
|
||||
import "./flow.css"
|
||||
import { liveStore } from "./liveStore"
|
||||
import {
|
||||
flowKeys,
|
||||
flowQueryOptions,
|
||||
flowsQueryOptions,
|
||||
nodeTypesQueryOptions,
|
||||
@@ -96,6 +98,7 @@ function uniqueNodeId(existing: NodeDef_Input[], type: string): string {
|
||||
}
|
||||
|
||||
function FlowEditorInner({ flowName }: { flowName: string }) {
|
||||
const navigate = useNavigate()
|
||||
const queryClient = useQueryClient()
|
||||
const { showErrorToast } = useCustomToast()
|
||||
const { screenToFlowPosition, fitView } = useReactFlow()
|
||||
@@ -117,6 +120,7 @@ function FlowEditorInner({ flowName }: { flowName: string }) {
|
||||
const [paletteOpen, setPaletteOpen] = useState(false)
|
||||
const [inspected, setInspected] = useState<InspectedEdge | null>(null)
|
||||
const [rebind, setRebind] = useState<Rebind | null>(null)
|
||||
const [flowPanelOpen, setFlowPanelOpen] = useState(false)
|
||||
|
||||
const issues = detail.issues ?? []
|
||||
|
||||
@@ -228,6 +232,32 @@ function FlowEditorInner({ flowName }: { flowName: string }) {
|
||||
showErrorToast("The flow could not run. Check the node errors."),
|
||||
})
|
||||
|
||||
const renameMutation = useMutation({
|
||||
mutationFn: (newName: string) =>
|
||||
FlowsService.renameFlow({ name: flowName, requestBody: { new_name: newName } }),
|
||||
onSuccess: (detail) => {
|
||||
queryClient.invalidateQueries({ queryKey: flowKeys.all })
|
||||
setFlowPanelOpen(false)
|
||||
navigate({
|
||||
to: "/flows/$flowName",
|
||||
params: { flowName: detail.definition.name },
|
||||
replace: true,
|
||||
})
|
||||
},
|
||||
onError: () =>
|
||||
showErrorToast("That name is taken, or is not a valid flow name."),
|
||||
})
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: () => FlowsService.deleteFlow({ name: flowName }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: flowKeys.all })
|
||||
setFlowPanelOpen(false)
|
||||
navigate({ to: "/flows", replace: true })
|
||||
},
|
||||
onError: () => showErrorToast("The flow could not be deleted."),
|
||||
})
|
||||
|
||||
const sourceMutation = useMutation({
|
||||
mutationFn: ({ nodeId, code }: { nodeId: string; code: string }) =>
|
||||
FlowsService.saveNodeSource({
|
||||
@@ -406,7 +436,10 @@ function FlowEditorInner({ flowName }: { flowName: string }) {
|
||||
commit(definitions, mergeDragged(canvasNodes, dragged))
|
||||
}
|
||||
onNodesDelete={(deleted) => deleteNodes(deleted.map((node) => node.id))}
|
||||
onNodeClick={(_event, node) => setSelectedId(node.id)}
|
||||
onNodeClick={(_event, node) => {
|
||||
setFlowPanelOpen(false)
|
||||
setSelectedId(node.id)
|
||||
}}
|
||||
onPaneClick={() => {
|
||||
setSelectedId(null)
|
||||
setInspected(null)
|
||||
@@ -453,6 +486,10 @@ function FlowEditorInner({ flowName }: { flowName: string }) {
|
||||
issues={issues}
|
||||
running={runMutation.isPending}
|
||||
onAddNode={() => setPaletteOpen(true)}
|
||||
onEditFlow={() => {
|
||||
setSelectedId(null)
|
||||
setFlowPanelOpen(true)
|
||||
}}
|
||||
onRun={() => {
|
||||
flush()
|
||||
runMutation.mutate()
|
||||
@@ -475,6 +512,23 @@ function FlowEditorInner({ flowName }: { flowName: string }) {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<FlowPanel
|
||||
open={flowPanelOpen && !selected}
|
||||
definition={{ ...detail.definition, nodes: definitions }}
|
||||
nodeCount={definitions.length}
|
||||
renaming={renameMutation.isPending}
|
||||
onChange={(next) => {
|
||||
flush()
|
||||
save({ ...next, nodes: definitions })
|
||||
}}
|
||||
onRename={(newName) => {
|
||||
flush()
|
||||
renameMutation.mutate(newName)
|
||||
}}
|
||||
onDelete={() => deleteMutation.mutate()}
|
||||
onClose={() => setFlowPanelOpen(false)}
|
||||
/>
|
||||
|
||||
<NodePanel
|
||||
node={selected}
|
||||
flow={flowName}
|
||||
|
||||
Reference in New Issue
Block a user