mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
feat(stacks): add optional volume prune to delete confirmation (#788)
The Delete Stack dialog now includes an opt-in checkbox to also remove associated Docker volumes when the stack is deleted. The checkbox is unchecked by default and resets to unchecked on every open. Backend: DELETE /stacks/:name accepts ?pruneVolumes=true and calls pruneManagedOnly for volumes labeled with the stack project name after bringing the stack down. Prune failure is non-fatal and logged; the delete proceeds regardless.
This commit is contained in:
@@ -526,6 +526,7 @@ stacksRouter.delete('/:stackName', async (req: Request, res: Response) => {
|
||||
if (!isValidStackName(stackName)) {
|
||||
return res.status(400).json({ error: 'Invalid stack name' });
|
||||
}
|
||||
const pruneVolumes = req.query.pruneVolumes === 'true';
|
||||
try {
|
||||
try {
|
||||
await ComposeService.getInstance(req.nodeId).downStack(stackName);
|
||||
@@ -533,6 +534,15 @@ stacksRouter.delete('/:stackName', async (req: Request, res: Response) => {
|
||||
console.warn(`[Teardown] Docker down failed or nothing to clean up for ${stackName}`);
|
||||
}
|
||||
|
||||
if (pruneVolumes) {
|
||||
try {
|
||||
const result = await DockerController.getInstance().pruneManagedOnly('volumes', [stackName]);
|
||||
console.log(`[Stacks] Pruned volumes for ${stackName}: ${result.reclaimedBytes} bytes reclaimed`);
|
||||
} catch (pruneErr) {
|
||||
console.warn(`[Stacks] Volume prune failed for ${stackName}, continuing delete:`, pruneErr);
|
||||
}
|
||||
}
|
||||
|
||||
let fsErr: unknown = null;
|
||||
try {
|
||||
await FileSystemService.getInstance(req.nodeId).deleteStack(stackName);
|
||||
|
||||
@@ -272,6 +272,7 @@ export default function EditorLayout() {
|
||||
const [gitDeployNow, setGitDeployNow] = useState(false);
|
||||
const [creatingFromGit, setCreatingFromGit] = useState(false);
|
||||
const [stackToDelete, setStackToDelete] = useState<string | null>(null);
|
||||
const [pruneVolumesOnDelete, setPruneVolumesOnDelete] = useState(false);
|
||||
const [pendingUnsavedLoad, setPendingUnsavedLoad] = useState<string | null>(null);
|
||||
const [pendingUnsavedNode, setPendingUnsavedNode] = useState<Node | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -1557,7 +1558,10 @@ export default function EditorLayout() {
|
||||
if (isStackBusy(deleteKey)) return;
|
||||
setStackAction(deleteKey, 'delete');
|
||||
try {
|
||||
const response = await apiFetch(`/stacks/${stackToDelete}`, {
|
||||
const url = pruneVolumesOnDelete
|
||||
? `/stacks/${stackToDelete}?pruneVolumes=true`
|
||||
: `/stacks/${stackToDelete}`;
|
||||
const response = await apiFetch(url, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!response.ok) {
|
||||
@@ -1567,6 +1571,7 @@ export default function EditorLayout() {
|
||||
toast.success('Stack deleted successfully!');
|
||||
setDeleteDialogOpen(false);
|
||||
setStackToDelete(null);
|
||||
setPruneVolumesOnDelete(false);
|
||||
if (selectedFile === stackToDelete) {
|
||||
setSelectedFile(null);
|
||||
setContent('');
|
||||
@@ -1981,7 +1986,7 @@ export default function EditorLayout() {
|
||||
stop: () => executeStackActionByFile(file, 'stop', 'stop'),
|
||||
restart: () => executeStackActionByFile(file, 'restart', 'restart'),
|
||||
update: () => executeStackActionByFile(file, 'update', 'update'),
|
||||
remove: () => { setStackToDelete(stackName); setDeleteDialogOpen(true); },
|
||||
remove: () => { setStackToDelete(stackName); setPruneVolumesOnDelete(false); setDeleteDialogOpen(true); },
|
||||
pin: () => pin(file),
|
||||
unpin: () => unpin(file),
|
||||
setAutoUpdateEnabled: async (enabled: boolean) => {
|
||||
@@ -2488,6 +2493,7 @@ export default function EditorLayout() {
|
||||
disabled={loadingAction !== null}
|
||||
onClick={() => {
|
||||
setStackToDelete(selectedFile);
|
||||
setPruneVolumesOnDelete(false);
|
||||
setDeleteDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
@@ -2938,6 +2944,16 @@ export default function EditorLayout() {
|
||||
Are you sure you want to delete {stackToDelete}? This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<div className="flex items-center gap-2 px-1 py-1">
|
||||
<Checkbox
|
||||
id="prune-volumes"
|
||||
checked={pruneVolumesOnDelete}
|
||||
onCheckedChange={(v) => setPruneVolumesOnDelete(v === true)}
|
||||
/>
|
||||
<label htmlFor="prune-volumes" className="text-sm text-muted-foreground cursor-pointer select-none">
|
||||
Also remove associated volumes
|
||||
</label>
|
||||
</div>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={() => setDeleteDialogOpen(false)}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction className="bg-destructive text-destructive-foreground hover:bg-destructive/90" onClick={deleteStack}>Delete</AlertDialogAction>
|
||||
|
||||
Reference in New Issue
Block a user