mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-28 19:27:41 +00:00
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:
@@ -12,12 +12,13 @@ function makeCtx(overrides: Partial<StackMenuCtx> = {}): StackMenuCtx {
|
||||
isBusy: false,
|
||||
isAdmin: true,
|
||||
canDelete: true,
|
||||
canDeploy: true,
|
||||
canEditLabels: true,
|
||||
canCreateLabels: true,
|
||||
isPinned: false,
|
||||
labels: [],
|
||||
assignedLabelIds: [],
|
||||
menuVisibility: { showDeploy: false, showStop: true, showRestart: true, showUpdate: false },
|
||||
menuVisibility: { showDeploy: false, showStop: true, showRestart: true, showUpdate: false, showTakeDown: true },
|
||||
openAlertSheet: vi.fn(),
|
||||
openAutoHeal: vi.fn(),
|
||||
checkUpdates: vi.fn(),
|
||||
@@ -26,6 +27,7 @@ function makeCtx(overrides: Partial<StackMenuCtx> = {}): StackMenuCtx {
|
||||
stop: vi.fn(),
|
||||
restart: vi.fn(),
|
||||
update: vi.fn(),
|
||||
takeDown: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
pin: vi.fn(),
|
||||
unpin: vi.fn(),
|
||||
@@ -162,17 +164,52 @@ describe('useStackMenuItems', () => {
|
||||
|
||||
it('lifecycle items follow menuVisibility flags', () => {
|
||||
const { result } = renderHook(() => useStackMenuItems('web.yml', makeCtx({
|
||||
menuVisibility: { showDeploy: true, showStop: false, showRestart: false, showUpdate: true },
|
||||
menuVisibility: { showDeploy: true, showStop: false, showRestart: false, showUpdate: true, showTakeDown: false },
|
||||
})));
|
||||
const lifecycle = result.current.find(g => g.id === 'lifecycle')!;
|
||||
const ids = lifecycle.items.map(i => i.id);
|
||||
expect(ids).toEqual(['deploy', 'update', 'schedule']);
|
||||
});
|
||||
|
||||
it('shows Take down in lifecycle when showTakeDown and canDeploy', () => {
|
||||
const takeDown = vi.fn();
|
||||
const { result } = renderHook(() => useStackMenuItems('web.yml', makeCtx({
|
||||
takeDown,
|
||||
menuVisibility: { showDeploy: false, showStop: true, showRestart: true, showUpdate: false, showTakeDown: true },
|
||||
})));
|
||||
const lifecycle = result.current.find(g => g.id === 'lifecycle')!;
|
||||
const item = lifecycle.items.find(i => i.id === 'take-down');
|
||||
expect(item?.label).toBe('Take down');
|
||||
expect(item?.shortcut).toBe('⌘↓');
|
||||
item?.onSelect();
|
||||
expect(takeDown).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('hides deploy lifecycle items when canDeploy is false', () => {
|
||||
const { result } = renderHook(() => useStackMenuItems('web.yml', makeCtx({
|
||||
canDeploy: false,
|
||||
menuVisibility: { showDeploy: true, showStop: true, showRestart: true, showUpdate: true, showTakeDown: true },
|
||||
})));
|
||||
const lifecycle = result.current.find(g => g.id === 'lifecycle')!;
|
||||
const ids = lifecycle.items.map(i => i.id);
|
||||
expect(ids).not.toContain('deploy');
|
||||
expect(ids).not.toContain('take-down');
|
||||
expect(ids).toEqual(['schedule']);
|
||||
});
|
||||
|
||||
it('disables take down for the self stack', () => {
|
||||
const { result } = renderHook(() => useStackMenuItems('sencho.yml', makeCtx({
|
||||
isSelfStack: true,
|
||||
menuVisibility: { showDeploy: false, showStop: true, showRestart: true, showUpdate: false, showTakeDown: true },
|
||||
})));
|
||||
const lifecycle = result.current.find(g => g.id === 'lifecycle')!;
|
||||
expect(lifecycle.items.find(i => i.id === 'take-down')?.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('disables action lifecycle items when isBusy but leaves schedule enabled', () => {
|
||||
const { result } = renderHook(() => useStackMenuItems('web.yml', makeCtx({
|
||||
isBusy: true,
|
||||
menuVisibility: { showDeploy: true, showStop: true, showRestart: true, showUpdate: true },
|
||||
menuVisibility: { showDeploy: true, showStop: true, showRestart: true, showUpdate: true, showTakeDown: false },
|
||||
})));
|
||||
const lifecycle = result.current.find(g => g.id === 'lifecycle')!;
|
||||
const actionItems = lifecycle.items.filter(i => i.id !== 'schedule');
|
||||
|
||||
@@ -22,26 +22,30 @@ export function useStackKeyboardShortcuts(
|
||||
const cmdOrCtrl = e.metaKey || e.ctrlKey;
|
||||
const key = e.key.toLowerCase();
|
||||
|
||||
const isCmdKey = cmdOrCtrl && ['enter', '.', 'r', 'arrowup', 'backspace'].includes(key);
|
||||
const isCmdKey = cmdOrCtrl && ['enter', '.', 'r', 'arrowup', 'arrowdown', 'backspace'].includes(key);
|
||||
const isSingleKey = !cmdOrCtrl && ['a', 'h', 'u', 'p'].includes(key);
|
||||
if (!isCmdKey && !isSingleKey) return;
|
||||
|
||||
const ctx = buildMenuCtxRef.current(file);
|
||||
const { showDeploy, showStop, showRestart, showUpdate } = ctx.menuVisibility;
|
||||
const { showDeploy, showStop, showRestart, showUpdate, showTakeDown } = ctx.menuVisibility;
|
||||
const canDeploy = (show: boolean) => ctx.canDeploy && show && !ctx.isBusy;
|
||||
|
||||
if (cmdOrCtrl) {
|
||||
if (key === 'enter' && showDeploy && !ctx.isBusy) {
|
||||
if (key === 'enter' && canDeploy(showDeploy)) {
|
||||
e.preventDefault();
|
||||
ctx.deploy();
|
||||
} else if (key === '.' && showStop && !ctx.isBusy) {
|
||||
} else if (key === '.' && canDeploy(showStop)) {
|
||||
e.preventDefault();
|
||||
ctx.stop();
|
||||
} else if (key === 'r' && showRestart && !ctx.isBusy) {
|
||||
} else if (key === 'r' && canDeploy(showRestart)) {
|
||||
e.preventDefault();
|
||||
ctx.restart();
|
||||
} else if (key === 'arrowup' && showUpdate && !ctx.isBusy) {
|
||||
} else if (key === 'arrowup' && canDeploy(showUpdate)) {
|
||||
e.preventDefault();
|
||||
ctx.update();
|
||||
} else if (key === 'arrowdown' && canDeploy(showTakeDown)) {
|
||||
e.preventDefault();
|
||||
ctx.takeDown();
|
||||
} else if (key === 'backspace' && ctx.canDelete && !ctx.isBusy) {
|
||||
e.preventDefault();
|
||||
ctx.remove();
|
||||
|
||||
@@ -14,18 +14,19 @@ import {
|
||||
Square,
|
||||
Tag,
|
||||
Trash2,
|
||||
ArrowDownToLine,
|
||||
} from 'lucide-react';
|
||||
import type { MenuGroup, MenuItem, StackMenuCtx } from '@/components/sidebar/sidebar-types';
|
||||
|
||||
export function useStackMenuItems(_file: string, ctx: StackMenuCtx): MenuGroup[] {
|
||||
const {
|
||||
stackStatus, isSelfStack, canOpenApp, isBusy, isAdmin, canDelete, canEditLabels, isPinned, labels,
|
||||
stackStatus, isSelfStack, canOpenApp, isBusy, isAdmin, canDelete, canDeploy, canEditLabels, isPinned, labels,
|
||||
openAlertSheet, openAutoHeal, checkUpdates, openStackApp,
|
||||
deploy, stop, restart, update, remove, pin, unpin, toggleLabel,
|
||||
deploy, stop, restart, update, takeDown, remove, pin, unpin, toggleLabel,
|
||||
menuVisibility, openScheduleTask,
|
||||
canMuteNotifications, muteStackAll, muteStackDeploySuccess, muteStackMonitor, openStackMuteRules,
|
||||
} = ctx;
|
||||
const { showDeploy, showStop, showRestart, showUpdate } = menuVisibility;
|
||||
const { showDeploy, showStop, showRestart, showUpdate, showTakeDown } = menuVisibility;
|
||||
|
||||
return useMemo(() => {
|
||||
const groups: MenuGroup[] = [];
|
||||
@@ -78,10 +79,13 @@ export function useStackMenuItems(_file: string, ctx: StackMenuCtx): MenuGroup[]
|
||||
groups.push({ id: 'organize', items: organize });
|
||||
|
||||
const lifecycle: MenuItem[] = [];
|
||||
if (showDeploy) lifecycle.push({ id: 'deploy', label: 'Deploy', icon: Play, shortcut: '⌘↵', onSelect: deploy, disabled: isBusy });
|
||||
if (showStop) lifecycle.push({ id: 'stop', label: 'Stop', icon: Square, shortcut: '⌘.', onSelect: stop, disabled: isBusy });
|
||||
if (showRestart) lifecycle.push({ id: 'restart', label: 'Restart', icon: RotateCw, shortcut: '⌘R', onSelect: restart, disabled: isBusy });
|
||||
if (showUpdate) lifecycle.push({ id: 'update', label: 'Update', icon: Download, shortcut: '⌘↑', onSelect: update, disabled: isBusy });
|
||||
if (canDeploy) {
|
||||
if (showDeploy) lifecycle.push({ id: 'deploy', label: 'Deploy', icon: Play, shortcut: '⌘↵', onSelect: deploy, disabled: isBusy });
|
||||
if (showStop) lifecycle.push({ id: 'stop', label: 'Stop', icon: Square, shortcut: '⌘.', onSelect: stop, disabled: isBusy });
|
||||
if (showRestart) lifecycle.push({ id: 'restart', label: 'Restart', icon: RotateCw, shortcut: '⌘R', onSelect: restart, disabled: isBusy });
|
||||
if (showUpdate) lifecycle.push({ id: 'update', label: 'Update', icon: Download, shortcut: '⌘↑', onSelect: update, disabled: isBusy });
|
||||
if (showTakeDown) lifecycle.push({ id: 'take-down', label: 'Take down', icon: ArrowDownToLine, shortcut: '⌘↓', onSelect: takeDown, disabled: isBusy || isSelfStack });
|
||||
}
|
||||
if (isAdmin) lifecycle.push({ id: 'schedule', label: 'Schedule task', icon: CalendarClock, onSelect: openScheduleTask });
|
||||
if (lifecycle.length > 0) groups.push({ id: 'lifecycle', items: lifecycle });
|
||||
|
||||
@@ -102,10 +106,10 @@ export function useStackMenuItems(_file: string, ctx: StackMenuCtx): MenuGroup[]
|
||||
|
||||
return groups;
|
||||
}, [
|
||||
stackStatus, isSelfStack, canOpenApp, isBusy, isAdmin, canDelete, canEditLabels, isPinned, labels,
|
||||
showDeploy, showStop, showRestart, showUpdate,
|
||||
stackStatus, isSelfStack, canOpenApp, isBusy, isAdmin, canDelete, canDeploy, canEditLabels, isPinned, labels,
|
||||
showDeploy, showStop, showRestart, showUpdate, showTakeDown,
|
||||
openAlertSheet, openAutoHeal, checkUpdates, openStackApp,
|
||||
deploy, stop, restart, update, remove, pin, unpin, toggleLabel, openScheduleTask,
|
||||
deploy, stop, restart, update, takeDown, remove, pin, unpin, toggleLabel, openScheduleTask,
|
||||
canMuteNotifications, muteStackAll, muteStackDeploySuccess, muteStackMonitor, openStackMuteRules,
|
||||
]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user