Files
app/frontend/src/components/Flow/FlowPanel.tsx
T
stroblmeandClaude Opus 5 39ee0e0aa5 Computed flow layout, and mobile written into the design
The canvas lays itself out: a layered graph, left to right on a desktop and
top to bottom on a phone, with room reserved for the value each edge carries.
Nodes cannot be dragged and `NodeDef.position` is gone from the document —
a graph nobody can arrange is one worth keeping small, which is what keeps
flows atomic. Endpoints join the same layout, so their lanes and the
localStorage that remembered where they were dragged go too.

Mobile, per the new Responsive section of DESIGN-GUIDELINES.md: the dock caps
its width and wraps instead of running off the screen, the dashboard stacks
into one column rather than shrinking a wall panel to a fifth of its size, and
Home stops widening its grid track past the viewport. A Playwright project at
a phone's width fails the build when a screen no longer fits.

Along the way: publish is the checkmark that was already there rather than a
button that appears and disappears, with discard beside it on both the flow
and the dashboard; the brain reveals a neuron's name on the first tap; and the
port sparklines get room to breathe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VDSXaRhvqHYNevgDGmNAto
2026-08-17 17:35:14 +02:00

147 lines
4.3 KiB
TypeScript

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 { Switch } from "@/components/ui/switch"
import { PANEL_SECTION, PanelTitle, SidePanel } from "./SidePanel"
/**
* The flow's own settings, in the same panel its nodes use — down to where the
* name sits and how confirming a new one works.
*/
export function FlowPanel({
open,
definition,
nodeCount,
onChange,
onDelete,
hasDraft,
enabled,
toggling,
onToggleEnabled,
onClose,
}: {
open: boolean
definition: FlowDef_Input
nodeCount: number
onChange: (next: FlowDef_Input) => void
onDelete: () => void
hasDraft: boolean
enabled: boolean
toggling: boolean
onToggleEnabled: (next: boolean) => void
onClose: () => void
}) {
const [confirmOpen, setConfirmOpen] = useState(false)
return (
<>
<SidePanel
open={open}
label="Flow settings"
testId="flow-panel"
bodyKey={definition.name}
onClose={onClose}
header={
<PanelTitle
value={definition.title ?? ""}
placeholder={definition.name}
label="Flow title"
onConfirm={(title) => onChange({ ...definition, title })}
/>
}
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-6 p-4">
<div className="grid gap-3">
<span className={PANEL_SECTION}>Running</span>
<div className="flex items-center justify-between gap-3">
<p className="text-sm text-muted-foreground">
{enabled
? "The engine runs this flow: subscriptions, schedules and webhooks are live."
: "Stopped. Nothing of this flow is subscribed, scheduled or reachable."}
</p>
<Switch
checked={enabled}
disabled={toggling}
onCheckedChange={onToggleEnabled}
aria-label="Run this flow"
data-testid="flow-enabled"
/>
</div>
</div>
<div className="grid gap-3">
<span className={PANEL_SECTION}>Contents</span>
<p className="text-sm text-muted-foreground">
<span className="font-mono">{definition.name}</span> namespaces
every message in here.{" "}
{nodeCount === 0
? "No nodes yet."
: `${nodeCount} node${nodeCount === 1 ? "" : "s"}.`}
</p>
</div>
{hasDraft ? (
<div className="grid gap-3">
<span className={PANEL_SECTION}>Unpublished changes</span>
<p className="text-sm text-muted-foreground">
The engine is still running the last published version of this
flow. The bar below publishes it, or throws the edit away.
</p>
</div>
) : null}
</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>
</>
)
}