Remove buttons: confirm + block program removal with active deployments
The Stage 2 editor split dropped the old confirm() — Remove program/service/job
deleted immediately on click. Restored confirmation on all three (FormFooter
window.confirm with a per-type message).
Removing a program that still has services/jobs referencing it would orphan
their program: ref. Now blocked:
- API: DELETE /config/programs/{name} → 409 'Programs with active jobs or
services cannot be removed. Delete these first: …' (authoritative).
- App: ProgramFields shows the blocked reason instead of a Remove button when
the program has deployments.
- CLI: castle delete refuses a program with referencing deployments not named
the same (which would survive the delete dangling).
Verified live: DELETE program lakehouse → 409. api 52 / cli 24 green; build clean.
This commit is contained in:
@@ -74,6 +74,7 @@ export function JobFields({ job, onSave, onDelete }: Props) {
|
||||
onSave={handleSave}
|
||||
onDelete={onDelete ? () => onDelete(job.id) : undefined}
|
||||
deleteLabel="Remove job"
|
||||
confirmMessage={`Remove job "${job.id}" from castle.yaml? Run a deploy afterward to tear down its timer.`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -95,6 +95,12 @@ export function ProgramFields({ program, onSave, onDelete }: Props) {
|
||||
onSave={handleSave}
|
||||
onDelete={onDelete ? () => onDelete(program.id) : undefined}
|
||||
deleteLabel="Remove program"
|
||||
confirmMessage={`Remove program "${program.id}" from castle.yaml? (Source on disk is untouched.)`}
|
||||
deleteBlocked={
|
||||
program.services.length + program.jobs.length > 0
|
||||
? "Programs with active jobs or services cannot be removed — delete those first."
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -119,6 +119,7 @@ export function ServiceFields({ service, onSave, onDelete }: Props) {
|
||||
onSave={handleSave}
|
||||
onDelete={onDelete ? () => onDelete(service.id) : undefined}
|
||||
deleteLabel="Remove service"
|
||||
confirmMessage={`Remove service "${service.id}" from castle.yaml? Run a deploy afterward to tear down its unit.`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -122,18 +122,27 @@ export function FormFooter({
|
||||
onSave,
|
||||
onDelete,
|
||||
deleteLabel,
|
||||
confirmMessage,
|
||||
deleteBlocked,
|
||||
}: {
|
||||
saving: boolean
|
||||
saved: boolean
|
||||
onSave: () => void
|
||||
onDelete?: () => void
|
||||
deleteLabel: string
|
||||
confirmMessage?: string
|
||||
/** When set, removal is disallowed and this reason is shown instead of the button. */
|
||||
deleteBlocked?: string
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between pt-3 border-t border-[var(--border)]">
|
||||
{onDelete ? (
|
||||
{deleteBlocked ? (
|
||||
<span className="text-xs text-amber-400">{deleteBlocked}</span>
|
||||
) : onDelete ? (
|
||||
<button
|
||||
onClick={onDelete}
|
||||
onClick={() => {
|
||||
if (window.confirm(confirmMessage ?? `${deleteLabel}?`)) onDelete()
|
||||
}}
|
||||
className="flex items-center gap-1.5 text-xs text-red-400 hover:text-red-300"
|
||||
>
|
||||
<Trash2 size={12} /> {deleteLabel}
|
||||
|
||||
Reference in New Issue
Block a user