option to delete associated volumes when removing a stack (#655)

This commit is contained in:
jarek committed 2026-04-19 09:39:42 +02:00
1 parent 0af1ee6eb2
commit b8f06426e3
4 files changed
+39 -18

No files matched your search

+13 -6
View File
@@ -19,6 +19,7 @@
onConfirm: () => void;
onOpenChange: (open: boolean) => void;
children: Snippet<[{ open: boolean }]>;
extraContent?: Snippet;
}
let {
@@ -35,7 +36,8 @@
disabled = false,
onConfirm,
onOpenChange,
children
children,
extraContent
}: Props = $props();
const triggerClass = $derived(unstyled
@@ -103,11 +105,16 @@
align={position === 'left' ? 'start' : 'end'}
sideOffset={8}
>
<div class="flex items-center gap-2">
<span class="text-xs whitespace-nowrap">{action} {itemType} {#if displayName}<strong>{displayName}</strong>{/if}?</span>
<Button size="sm" {variant} class="h-6 px-2 text-xs" onclick={handleConfirm}>
{confirmText}
</Button>
<div class="flex flex-col gap-1.5">
<div class="flex items-center gap-2">
<span class="text-xs whitespace-nowrap">{action} {itemType} {#if displayName}<strong>{displayName}</strong>{/if}?</span>
<Button size="sm" {variant} class="h-6 px-2 text-xs" onclick={handleConfirm}>
{confirmText}
</Button>
</div>
{#if extraContent}
{@render extraContent()}
{/if}
</div>
</Popover.Content>
</Popover.Root>
+3 -1
View File
@@ -1982,7 +1982,8 @@ export async function downStack(
export async function removeStack(
stackName: string,
envId?: number | null,
force = false
force = false,
removeVolumes = false
): Promise<StackOperationResult> {
return withStackLock(stackName, async () => {
// Get compose file (may not exist for external stacks)
@@ -2000,6 +2001,7 @@ export async function removeStack(
{
stackName,
envId,
removeVolumes,
workingDir: composeResult.stackDir,
composePath: composeResult.composePath ?? undefined,
envPath: composeResult.envPath ?? undefined
+3 -2
View File
@@ -9,6 +9,7 @@ export const DELETE: RequestHandler = async (event) => {
const auth = await authorize(cookies);
const force = url.searchParams.get('force') === 'true';
const volumes = url.searchParams.get('volumes') === 'true';
const envId = url.searchParams.get('env');
const envIdNum = envId ? parseInt(envId) : undefined;
@@ -24,10 +25,10 @@ export const DELETE: RequestHandler = async (event) => {
try {
const stackName = decodeURIComponent(params.name);
const result = await removeStack(stackName, envIdNum, force);
const result = await removeStack(stackName, envIdNum, force, volumes);
// Audit log
await auditStack(event, 'delete', stackName, envIdNum, { force });
await auditStack(event, 'delete', stackName, envIdNum, { force, volumes });
if (!result.success) {
return json({ success: false, error: result.error }, { status: 400 });
+20 -9
View File
@@ -10,6 +10,7 @@
import { Badge } from '$lib/components/ui/badge';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Checkbox } from '$lib/components/ui/checkbox';
import * as Tooltip from '$lib/components/ui/tooltip';
import * as Popover from '$lib/components/ui/popover';
import MultiSelectFilter from '$lib/components/MultiSelectFilter.svelte';
@@ -418,6 +419,7 @@
let confirmDeleteName = $state<string | null>(null);
let confirmStopName = $state<string | null>(null);
let confirmDownName = $state<string | null>(null);
let deleteVolumes = $state(false);
// Stack operation loading state
let stackActionLoading = $state<string | null>(null);
@@ -968,15 +970,18 @@
async function removeStack(name: string) {
operationError = null;
const withVolumes = deleteVolumes;
deleteVolumes = false;
try {
const response = await fetch(appendEnvParam(`/api/stacks/${encodeURIComponent(name)}?force=true`, envId), { method: 'DELETE' });
const params = `force=true${withVolumes ? '&volumes=true' : ''}`;
const response = await fetch(appendEnvParam(`/api/stacks/${encodeURIComponent(name)}?${params}`, envId), { method: 'DELETE' });
if (!response.ok) {
const data = await response.json();
const errorMsg = data.error || 'Failed to remove stack';
showErrorDialog(`Failed to remove ${name}`, errorMsg);
return;
}
toast.success(`Removed ${name}`);
toast.success(`Removed ${name}${withVolumes ? ' (volumes deleted)' : ''}`);
await fetchStacks();
} catch (error) {
console.error('Failed to remove stack:', error);
@@ -1505,7 +1510,7 @@
<button
type="button"
class="font-medium text-xs hover:text-primary hover:underline cursor-pointer text-left"
onclick={() => editStack(stack.name)}
onclick={(e) => { e.stopPropagation(); editStack(stack.name); }}
>
{stack.name}
</button>
@@ -1648,7 +1653,7 @@
{@const stats = getStackStats(stack)}
<div class="text-right">
{#if stats}
<span class="text-xs font-mono text-muted-foreground" title="{formatBytes(stats.memoryUsage)} / {formatBytes(stats.memoryLimit)}">{formatBytes(stats.memoryUsage)}</span>
<span class="text-xs font-mono text-muted-foreground" title="{formatBytes(stats.memoryUsage)} / {formatBytes(stats.memoryLimit)}">{formatBytes(stats.memoryUsage)}<span class="text-muted-foreground/50">/{formatBytes(stats.memoryLimit, 0)}</span></span>
{:else if stack.status === 'running' || stack.status === 'partial' || stack.status === 'restarting'}
<span class="text-xs text-muted-foreground/50">...</span>
{:else}
@@ -1753,7 +1758,7 @@
{#if source.sourceType === 'git' && source.gitStack}
<button
type="button"
onclick={() => openGitModal(source.gitStack)}
onclick={(e) => { e.stopPropagation(); openGitModal(source.gitStack); }}
title="Edit git stack"
class="p-1 rounded hover:bg-muted transition-colors opacity-70 hover:opacity-100 cursor-pointer"
>
@@ -1763,7 +1768,7 @@
<!-- Internal stacks (including those needing file location) -->
<button
type="button"
onclick={() => editStack(stack.name)}
onclick={(e) => { e.stopPropagation(); editStack(stack.name); }}
title="Edit"
class="p-1 rounded hover:bg-muted transition-colors opacity-70 hover:opacity-100 cursor-pointer"
>
@@ -1774,7 +1779,7 @@
{#if stack.containers && stack.containers.length > 0}
<button
type="button"
onclick={() => viewStackLogs(stack)}
onclick={(e) => { e.stopPropagation(); viewStackLogs(stack); }}
title="View logs"
class="p-1 rounded hover:bg-muted transition-colors opacity-70 hover:opacity-100 cursor-pointer"
>
@@ -1852,7 +1857,7 @@
{#if $canAccess('stacks', 'start')}
<button
type="button"
onclick={() => startStack(stack.name)}
onclick={(e) => { e.stopPropagation(); startStack(stack.name); }}
title="Start"
class="p-1 rounded hover:bg-muted transition-colors opacity-70 hover:opacity-100 cursor-pointer"
>
@@ -1884,8 +1889,14 @@
itemName={stack.name}
title="Remove"
onConfirm={() => removeStack(stack.name)}
onOpenChange={(open) => confirmDeleteName = open ? stack.name : null}
onOpenChange={(open) => { confirmDeleteName = open ? stack.name : null; if (!open) deleteVolumes = false; }}
>
{#snippet extraContent()}
<label class="flex items-center gap-1.5 cursor-pointer">
<Checkbox bind:checked={deleteVolumes} />
<span class="text-xs text-muted-foreground">Also delete volumes</span>
</label>
{/snippet}
{#snippet children({ open })}
<Trash2 class="w-3 h-3 {open ? 'text-destructive' : 'text-muted-foreground hover:text-destructive'}" />
{/snippet}