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
147 lines
4.3 KiB
TypeScript
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>
|
|
</>
|
|
)
|
|
}
|