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.
203 lines
5.9 KiB
TypeScript
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>
|
|
)
|
|
}
|