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
@@ -0,0 +1,154 @@
|
||||
import { useState } from "react"
|
||||
|
||||
import type { FlowDef_Input } from "@/client"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { PANEL_SECTION, SidePanel } from "./SidePanel"
|
||||
|
||||
const NAME_PATTERN = /^[a-z][a-z0-9_]*$/
|
||||
|
||||
/**
|
||||
* The flow's own settings, in the same panel its nodes use.
|
||||
*
|
||||
* The name is also the namespace of every message in the flow, which is why
|
||||
* renaming goes through the server rather than being another autosaved field.
|
||||
*/
|
||||
export function FlowPanel({
|
||||
open,
|
||||
definition,
|
||||
nodeCount,
|
||||
renaming,
|
||||
onChange,
|
||||
onRename,
|
||||
onDelete,
|
||||
onClose,
|
||||
}: {
|
||||
open: boolean
|
||||
definition: FlowDef_Input
|
||||
nodeCount: number
|
||||
renaming: boolean
|
||||
onChange: (next: FlowDef_Input) => void
|
||||
onRename: (newName: string) => void
|
||||
onDelete: () => void
|
||||
onClose: () => void
|
||||
}) {
|
||||
const [name, setName] = useState(definition.name)
|
||||
const [confirmOpen, setConfirmOpen] = useState(false)
|
||||
|
||||
const valid = NAME_PATTERN.test(name)
|
||||
const changed = name !== definition.name
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidePanel
|
||||
open={open}
|
||||
label="Flow settings"
|
||||
testId="flow-panel"
|
||||
bodyKey={definition.name}
|
||||
onClose={onClose}
|
||||
header={
|
||||
<Input
|
||||
value={definition.title ?? ""}
|
||||
placeholder={definition.name}
|
||||
aria-label="Flow title"
|
||||
className="h-8 flex-1 text-sm font-medium"
|
||||
onChange={(event) =>
|
||||
onChange({ ...definition, title: event.target.value })
|
||||
}
|
||||
/>
|
||||
}
|
||||
footer={
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-destructive hover:text-destructive"
|
||||
onClick={() => setConfirmOpen(true)}
|
||||
data-testid="delete-flow"
|
||||
>
|
||||
Delete flow
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className="grid gap-5 p-4">
|
||||
<div className="grid gap-2">
|
||||
<span className={PANEL_SECTION}>Name</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
value={name}
|
||||
aria-label="Flow name"
|
||||
autoComplete="off"
|
||||
className="h-8 flex-1 font-mono text-sm"
|
||||
onChange={(event) => setName(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" && valid && changed) {
|
||||
onRename(name)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-8"
|
||||
disabled={!valid || !changed || renaming}
|
||||
onClick={() => onRename(name)}
|
||||
>
|
||||
{renaming ? "Renaming…" : "Rename"}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{valid || !name
|
||||
? "Messages in this flow are named after it, so other flows reading them follow the rename."
|
||||
: "Lowercase letters, digits and underscores, starting with a letter."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<span className={PANEL_SECTION}>Contents</span>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{nodeCount === 0
|
||||
? "No nodes yet."
|
||||
: `${nodeCount} node${nodeCount === 1 ? "" : "s"}.`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</SidePanel>
|
||||
|
||||
<Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Delete {definition.title || definition.name}?
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
This removes the flow and the code of its{" "}
|
||||
{nodeCount === 1 ? "node" : `${nodeCount} nodes`}. Its history
|
||||
stays in the flow store's git repository.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setConfirmOpen(false)}>
|
||||
Keep it
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
setConfirmOpen(false)
|
||||
onDelete()
|
||||
}}
|
||||
data-testid="confirm-delete-flow"
|
||||
>
|
||||
Delete flow
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user