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
@@ -16,6 +16,7 @@ import {
ArrowUpRight,
Copy,
CloudDownload,
ArrowDownToLine,
Layers,
List,
Maximize2,
@@ -133,6 +134,8 @@ export interface StackIdentityHeaderProps {
rollbackStack: () => Promise<void>;
scanStackConfig: () => Promise<void>;
requestDeleteStack: () => void;
requestTakeDownStack: (stackName: string) => void;
showTakeDown: boolean;
/** True when this stack is the running Sencho instance on the active node. */
isSelfStack?: boolean;
stackMuteActions?: ReturnType<typeof useStackMuteActions>;
@@ -158,6 +161,8 @@ export function StackIdentityHeader({
rollbackStack,
scanStackConfig,
requestDeleteStack,
requestTakeDownStack,
showTakeDown,
isSelfStack = false,
stackMuteActions,
}: StackIdentityHeaderProps) {
@@ -219,6 +224,20 @@ export function StackIdentityHeader({
{loadingAction === 'stop' ? 'Stopping...' : 'Stop'}
</Button>
)}
{isRunning && showTakeDown && (
<Button
type="button"
size="sm"
variant="outline"
data-testid="stack-take-down-button"
className="rounded-lg max-md:h-11 border-warning/40 text-warning hover:bg-warning/10"
onClick={() => requestTakeDownStack(stackName)}
disabled={loadingAction !== null || selfProtected}
>
<ArrowDownToLine className="w-4 h-4 mr-2" strokeWidth={1.5} />
{loadingAction === 'down' ? 'Taking down...' : 'Take down'}
</Button>
)}
<Button type="button" size="sm" variant="outline" className="rounded-lg max-md:h-11" onClick={updateStack} disabled={loadingAction !== null || selfProtected}>
<CloudDownload className="w-4 h-4 mr-2" strokeWidth={1.5} />
{loadingAction === 'update' ? 'Updating...' : 'Update'}