The trend curve was two components: a rich one in the node panel and on edges,
and a bare line in the flow table. It is one `Sparkline` now, taking the colour
token, the height and whether the live dot and the readout show. The flow table
draws its rollup in the chart ramp with no dot — the rollups are polled, so the
right edge is the last completed slice rather than this instant — and keeps its
"nothing yet" state, as the panel keeps its three distinct silences.
All of them fade out to the left, through an SVG mask over the curve and its
area. The dot sits outside the mask: the newest reading is the one thing that
must stay solid.
`si` replaces `compact` and the ad-hoc "k" the uPlot axis carried. It prefixes
k/M/G and m/µ, but only outside 0.01–1000, where the plain number is already
the shortest thing to read and a written unit ("0.4 ms") stays honest. The
sparkline readout asks for four digits, so two neighbouring readings never
collapse into one string. Exact counts and anything the user acts on — a
payload, a form field, the edge inspector's value — are left unrounded.
`src/lib/utils.check.ts` asserts the rounding cases.
The activity table now spans its card: the flow name anchors the left, the four
numbers read down their own centre, and the trend takes the slack on the right.
Panel rhythm steps up one notch, gap-5 to gap-6 outside and gap-2 to gap-3
within a section.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XC2jX6Hdj7pxGGKzBTrbqB
190 lines
5.7 KiB
TypeScript
190 lines
5.7 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,
|
|
discarding,
|
|
onDiscardDraft,
|
|
enabled,
|
|
toggling,
|
|
onToggleEnabled,
|
|
onClose,
|
|
}: {
|
|
open: boolean
|
|
definition: FlowDef_Input
|
|
nodeCount: number
|
|
onChange: (next: FlowDef_Input) => void
|
|
onDelete: () => void
|
|
hasDraft: boolean
|
|
discarding: boolean
|
|
onDiscardDraft: () => void
|
|
enabled: boolean
|
|
toggling: boolean
|
|
onToggleEnabled: (next: boolean) => void
|
|
onClose: () => void
|
|
}) {
|
|
const [confirmOpen, setConfirmOpen] = useState(false)
|
|
const [discardOpen, setDiscardOpen] = 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.
|
|
</p>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-8 justify-self-start"
|
|
disabled={discarding}
|
|
onClick={() => setDiscardOpen(true)}
|
|
data-testid="discard-draft"
|
|
>
|
|
{discarding ? "Discarding…" : "Discard changes"}
|
|
</Button>
|
|
</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>
|
|
|
|
<Dialog open={discardOpen} onOpenChange={setDiscardOpen}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Discard the unpublished changes?</DialogTitle>
|
|
<DialogDescription>
|
|
The canvas goes back to the version the engine is running. What
|
|
you edited since is dropped, though the flow store's git history
|
|
keeps it.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setDiscardOpen(false)}>
|
|
Keep editing
|
|
</Button>
|
|
<Button
|
|
variant="destructive"
|
|
onClick={() => {
|
|
setDiscardOpen(false)
|
|
onDiscardDraft()
|
|
}}
|
|
data-testid="confirm-discard-draft"
|
|
>
|
|
Discard changes
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
)
|
|
}
|