feat: add confirmed Take down stack action with optional volume removal (#1599)

* feat: add confirmed Take down stack action with optional volume removal

Expose Take down in the stack header and sidebar with a confirmation dialog
that runs compose down while keeping the stack definition on disk. Optional
volume removal is gated by node capability and stack:deploy permission, with
remote gateway preflight before proxying removeVolumes requests.

Closes #1582

* fix: reset take-down volume checkbox when dialog closes

* test: align getStackMenuVisibility assertions with showTakeDown key

getStackMenuVisibility now returns a fifth lifecycle flag, showTakeDown,
but three exhaustive toEqual assertions still listed only the prior four
keys and failed. Add the expected showTakeDown value to each: true for
the partial and exited running-stack cases, false for the self stack.

* test: cover Take down visibility for running non-self stacks

The getStackMenuVisibility assertions exercised the partial and exited
branches and the self-stack guard, but not the raw === 'running' literal
that drives showTakeDown for a normal running stack. Add a case so a
regression dropping 'running' from that check is caught.

* fix: drop Take down from header overflow and wire activity shortcut

Remove duplicate Take down from More actions.

Keep inline button when running, sidebar menu, and Cmd+ArrowDown.

Record stack_taken_down in activity on successful POST /down.
This commit is contained in:
Anso
2026-07-09 12:20:13 -04:00
committed by GitHub
parent 296ddff2a0
commit d113004359
48 changed files with 952 additions and 110 deletions
@@ -18,6 +18,7 @@ import { isDockerUnavailableError } from '../routes/stacks';
const {
mockDeployStack,
mockRunCommand,
mockRunDown,
mockUpdateStack,
mockGetContainersByStack,
mockRestartContainer,
@@ -26,6 +27,7 @@ const {
} = vi.hoisted(() => ({
mockDeployStack: vi.fn(),
mockRunCommand: vi.fn(),
mockRunDown: vi.fn(),
mockUpdateStack: vi.fn(),
mockGetContainersByStack: vi.fn(),
mockRestartContainer: vi.fn(),
@@ -44,6 +46,7 @@ vi.mock('../services/ComposeService', async () => {
getInstance: () => ({
deployStack: mockDeployStack,
runCommand: mockRunCommand,
runDown: mockRunDown,
updateStack: mockUpdateStack,
}),
},
@@ -98,6 +101,7 @@ afterAll(() => {
beforeEach(() => {
mockDeployStack.mockReset();
mockRunCommand.mockReset();
mockRunDown.mockReset();
mockUpdateStack.mockReset();
mockGetContainersByStack.mockReset();
mockRestartContainer.mockReset();
@@ -211,8 +215,8 @@ describe('POST /api/stacks/:name/deploy with daemon down', () => {
});
describe('POST /api/stacks/:name/down with daemon down', () => {
it('returns 503 with code: docker_unavailable when runCommand surfaces the daemon error', async () => {
mockRunCommand.mockRejectedValue(composeDaemonDownError());
it('returns 503 with code: docker_unavailable when runDown surfaces the daemon error', async () => {
mockRunDown.mockRejectedValue(composeDaemonDownError());
const res = await request(app)
.post('/api/stacks/web/down')