Files
app/frontend/src/components/Flow/FlowPanel.tsx
T
stroblme 6fa44bacf3 Flow delete offers to delete its runs, history included
DELETE /runs?flow= removes every Run row of the flow but left its flow_run
rows, so a deleted flow kept appearing in the history. history=true now
drops those too, and the flow delete dialogs (editor and bulk) send it when
"Also delete its runs" is ticked.
2026-10-15 10:52:33 +02:00

203 lines
5.9 KiB
TypeScript

import { useId, useState } from "react"
import type { FlowDef_Input } from "@/client"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Switch } from "@/components/ui/switch"
import { BoundarySections } from "./FlowBoundary"
import { PanelSection, 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: (withRuns: boolean) => void
hasDraft: boolean
enabled: boolean
toggling: boolean
onToggleEnabled: (next: boolean) => void
onClose: () => void
}) {
const [confirmOpen, setConfirmOpen] = useState(false)
const [withRuns, setWithRuns] = 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={() => {
setWithRuns(false)
setConfirmOpen(true)
}}
data-testid="delete-flow"
>
Delete flow
</Button>
}
>
<div className="grid gap-6 p-4">
<PanelSection
title="Running"
className="gap-3"
help="While this is on, the engine keeps the flow's subscriptions, schedules and webhooks live. Off, none of it is subscribed, scheduled or reachable."
>
<div className="flex items-center justify-between gap-3">
<p className="text-sm text-muted-foreground">
{enabled ? "Live" : "Stopped"}
</p>
<Switch
checked={enabled}
disabled={toggling}
onCheckedChange={onToggleEnabled}
aria-label="Run this flow"
data-testid="flow-enabled"
/>
</div>
</PanelSection>
<BoundarySections
flow={definition.name}
definition={definition}
running={enabled}
onChange={onChange}
/>
<PanelSection
title="Contents"
className="gap-3"
help={
<>
Every message in this flow is named under{" "}
<span className="font-mono">{definition.name}</span>, so two
flows can use the same short name without colliding.
</>
}
>
<p className="text-sm text-muted-foreground">
{nodeCount === 0
? "No nodes yet."
: `${nodeCount} node${nodeCount === 1 ? "" : "s"}`}
</p>
</PanelSection>
{hasDraft ? (
<PanelSection
title="Unpublished changes"
className="gap-3"
help="Edits are kept as a draft until you publish them. The bar below publishes this one, or discards it."
>
<p className="text-sm text-muted-foreground">
The engine is still running the last published version.
</p>
</PanelSection>
) : 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>
<AlsoDeleteRuns checked={withRuns} onCheckedChange={setWithRuns} />
<DialogFooter>
<Button variant="outline" onClick={() => setConfirmOpen(false)}>
Keep it
</Button>
<Button
variant="destructive"
onClick={() => {
setConfirmOpen(false)
onDelete(withRuns)
}}
data-testid="confirm-delete-flow"
>
Delete flow
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
)
}
/**
* Whether deleting a flow takes its runs along. Off by default: a run is a
* record of what ran, and keeping one keeps the artifacts it recorded.
*/
export function AlsoDeleteRuns({
checked,
onCheckedChange,
plural = false,
}: {
checked: boolean
onCheckedChange: (checked: boolean) => void
plural?: boolean
}) {
const id = useId()
return (
<div className="grid gap-1">
<label htmlFor={id} className="flex items-center gap-2 text-sm">
<Checkbox
id={id}
checked={checked}
onCheckedChange={(next) => onCheckedChange(next === true)}
data-testid="delete-flow-runs"
/>
Also delete {plural ? "their" : "its"} runs
</label>
<p className="text-sm text-muted-foreground">
Otherwise the runs stay, and keep their artifacts on disk.
</p>
</div>
)
}