Rework the dashboards onto the flow canvas

One shell for both editors. Flows and dashboards each get a searchable
overview under the padded shell, their editors move to the full-bleed
canvas, and the floating chrome is shared: a title bar that only says
what you are looking at, and a bottom dock carrying everything else —
the flow bar's status, settings and Publish moved down there, the
add-flow button moved to the overview.

Dashboards gain the rest of M4's visualization work:

- widgets are picked by clicking them, with the header as the drag
  handle so a slider still slides and a switch still flips while
  editing; settings moved into the flows' SidePanel
- react-grid-layout for drag and edge-resize, so the stored x/y finally
  mean something; a dashboard nobody arranged is shelf-packed once
- a per-dashboard grid size, so a panel can be matched to its screen
- the chart widget, drawn with uPlot: several messages on one axis, fed
  from the stored history plus the live socket tail, coloured from the
  new --chart-1..5 ramp
- /view/{name}: the URL a wall panel is pointed at — no sidebar, no
  footer, no editing, and no editor code, since routes are split
- a widget wired to a payload type it cannot carry, or wired to nothing
  at all, carries the same red dot a failing node does; the picker
  records the type it bound and WidgetDef refuses a mismatch on save

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
This commit is contained in:
2026-08-16 17:13:10 +02:00
co-authored by Claude Fable 5
parent 74bb956805
commit 0af09eedbe
24 changed files with 1799 additions and 1558 deletions
+73
View File
@@ -1,12 +1,15 @@
import { useReactFlow } from "@xyflow/react"
import {
AlertCircle,
Check,
Loader2,
Maximize2,
Pause,
Pencil,
Play,
PlayCircle,
Plus,
WifiOff,
ZoomIn,
ZoomOut,
} from "lucide-react"
@@ -28,6 +31,7 @@ import {
import { slideUp, transitions } from "@/lib/motion"
import { cn } from "@/lib/utils"
import { type LogsFilter, LogsPanel } from "./LogsPanel"
import { useLiveConnection } from "./liveStore"
/**
* What "fit" means on this canvas: the view a flow opens with, and the one the
@@ -40,6 +44,10 @@ export const FIT_VIEW = { padding: 0.25, maxZoom: 1.2 }
/**
* The action bar, floating bottom-centre. Run is the one brand-secondary
* affordance on this view; everything else stays quiet.
*
* Everything the flow bar used to carry is here too — whether the work is
* saved, the flow's own settings, and putting it live — so the top of the
* canvas is left to say which flow this is.
*/
export function FlowDock({
flow,
@@ -47,25 +55,36 @@ export function FlowDock({
running,
enabled,
paused,
saving,
hasDraft,
publishing,
logs,
onAddNode,
onRun,
onTogglePause,
onFocusNode,
onEditFlow,
onPublish,
}: {
flow: string
issues: ValidationIssue[]
running: boolean
enabled: boolean
paused: boolean
saving: boolean
hasDraft: boolean
publishing: boolean
/** Owned by the editor, because a failing node can open it too. */
logs: LogsFilter
onAddNode: () => void
onRun: () => void
onTogglePause: () => void
onFocusNode: (nodeId: string) => void
onEditFlow: () => void
onPublish: () => void
}) {
const { zoomIn, zoomOut, fitView } = useReactFlow()
const connected = useLiveConnection()
return (
<motion.div
@@ -219,6 +238,60 @@ export function FlowDock({
{enabled ? "Run every node once" : "Start the flow to run it"}
</TooltipContent>
</Tooltip>
<Separator orientation="vertical" className="mx-0.5 !h-5" />
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-11 text-muted-foreground md:size-8"
onClick={onEditFlow}
aria-label="Flow settings"
data-testid="edit-flow"
>
<Pencil />
</Button>
</TooltipTrigger>
<TooltipContent>Flow settings</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<span className="flex size-8 shrink-0 items-center justify-center text-muted-foreground">
{!connected ? (
<WifiOff className="size-3.5" />
) : saving ? (
<Loader2 className="size-3.5 animate-spin" />
) : (
<Check className="size-3.5" />
)}
</span>
</TooltipTrigger>
<TooltipContent>
{!connected
? "Reconnecting to the engine"
: saving
? "Saving"
: hasDraft
? "Saved — publish to put it live"
: "All changes saved"}
</TooltipContent>
</Tooltip>
{hasDraft ? (
<Button
variant="outline"
size="sm"
className="h-11 shrink-0 rounded-full md:h-8"
onClick={onPublish}
disabled={publishing}
data-testid="publish-flow"
>
{publishing ? "Publishing…" : "Publish"}
</Button>
) : null}
</motion.div>
)
}