diff --git a/src/__tests__/watch-dashboard-actions.test.tsx b/src/__tests__/watch-dashboard-actions.test.tsx new file mode 100644 index 00000000..4cf4a40f --- /dev/null +++ b/src/__tests__/watch-dashboard-actions.test.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { render } from 'ink'; +import { Readable, Writable } from 'node:stream'; +import { WatchDashboard } from '../ui/WatchDashboard'; + +const k8sApiMock = vi.hoisted(() => ({ + deleteNamespacedPod: vi.fn(async () => undefined), + readNamespacedPodLog: vi.fn(async () => 'sample log'), +})); + +vi.mock('../kubernetes/pods', () => ({ + getRunningPods: vi.fn(async () => [{ name: 'api-pod', namespace: 'default', status: 'Running', restarts: 0, node: 'node-1' }]), + getK8sClusterStats: vi.fn(async () => null), +})); +vi.mock('../docker/containers', () => ({ + getRunningContainers: vi.fn(async () => []), + getDockerSystemStats: vi.fn(async () => null), + formatDockerBytes: vi.fn(() => '0 B'), +})); +vi.mock('../kubernetes/client', () => ({ + getK8sApi: vi.fn(() => k8sApiMock), +})); + +class TestStdout extends Writable { + output = ''; + isTTY = true; + columns = 100; + rows = 30; + _write(chunk: any, _encoding: string, callback: (error?: Error | null) => void) { + this.output += chunk.toString(); + callback(); + } +} +class TestStdin extends Readable { + isTTY = true; + setRawMode = vi.fn(); + setEncoding = vi.fn(); + ref = vi.fn(); + unref = vi.fn(); + _read() {} + send(value: string) { this.push(Buffer.from(value)); } +} +const wait = () => new Promise((resolve) => setTimeout(resolve, 80)); + +describe('WatchDashboard actions', () => { + let app: ReturnType | undefined; + let stdin: TestStdin | undefined; + + afterEach(() => { + app?.unmount(); + stdin?.push(null); + app = undefined; + stdin = undefined; + vi.clearAllMocks(); + }); + + it('confirms Pod deletion and lets b close the log snapshot', async () => { + const stdout = new TestStdout(); + stdin = new TestStdin(); + k8sApiMock.deleteNamespacedPod.mockClear(); + k8sApiMock.readNamespacedPodLog.mockClear(); + app = render(, { stdin, stdout, debug: true }); + await wait(); + + stdin.send('r'); + await wait(); + expect(stdout.output).toContain('Press Y to confirm'); + expect(k8sApiMock.deleteNamespacedPod).not.toHaveBeenCalled(); + stdin.send('n'); + await wait(); + expect(k8sApiMock.deleteNamespacedPod).not.toHaveBeenCalled(); + + stdin.send('r'); + await wait(); + stdin.send('y'); + await wait(); + expect(k8sApiMock.deleteNamespacedPod).toHaveBeenCalledWith({ name: 'api-pod', namespace: 'default' }); + + stdin.send('l'); + await wait(); + expect(stdout.output).toContain('[SNAPSHOT]'); + expect(stdout.output).toContain('sample log'); + stdin.send('b'); + await wait(); + expect(stdout.output).toContain('KDM Split-Pane Monitoring Dashboard'); + + }); +}); diff --git a/src/ui/WatchDashboard.tsx b/src/ui/WatchDashboard.tsx index 1663ad61..f993afcd 100644 --- a/src/ui/WatchDashboard.tsx +++ b/src/ui/WatchDashboard.tsx @@ -56,6 +56,7 @@ export const WatchDashboard: React.FC = ({ onBack, onExit } const [runningAi, setRunningAi] = useState(false); const [actionMessage, setActionMessage] = useState(null); + const [confirmRestart, setConfirmRestart] = useState(false); const handleResize = () => { setColumns(process.stdout.columns || 80); @@ -207,7 +208,7 @@ export const WatchDashboard: React.FC = ({ onBack, onExit } name: res.name, namespace: res.namespace, }); - setActionMessage(`Pod ${res.name} deleted (restarting via Deployment).`); + setActionMessage(`Pod ${res.name} deleted. A controller may recreate it.`); } else { const docker = getDockerClient(); await docker.getContainer(res.id).restart(); @@ -238,9 +239,15 @@ export const WatchDashboard: React.FC = ({ onBack, onExit } useInput((input, key) => { const lowerInput = input.toLowerCase(); + if (confirmRestart) { + if (lowerInput === 'y' && selectedResource) void restartResource(selectedResource); + if (lowerInput === 'y' || lowerInput === 'n' || key.escape) setConfirmRestart(false); + return; + } + // Close overlays if active if (showLogs) { - if (key.escape || lowerInput === 'q') { + if (key.escape || lowerInput === 'q' || lowerInput === 'b') { setShowLogs(false); } return; @@ -253,13 +260,6 @@ export const WatchDashboard: React.FC = ({ onBack, onExit } return; } - if (showLogs) { - if (key.escape || lowerInput === 'q' || lowerInput === 'b') { - setShowLogs(false); - } - return; - } - if (key.escape || lowerInput === 'b') { triggerBack(onBack, exit); return; @@ -310,7 +310,7 @@ export const WatchDashboard: React.FC = ({ onBack, onExit } return; } if (lowerInput === 'r') { - void restartResource(selectedResource); + setConfirmRestart(true); return; } } @@ -327,7 +327,7 @@ export const WatchDashboard: React.FC = ({ onBack, onExit } return ( - Logs: {selectedResource?.name} [STREAMING] + Logs: {selectedResource?.name} [SNAPSHOT] Press [ESC/B] Back @@ -358,6 +358,11 @@ export const WatchDashboard: React.FC = ({ onBack, onExit } return ( + {confirmRestart && selectedResource && ( + + Delete/restart {selectedResource.name}? Press Y to confirm or N/Esc to cancel. + + )} KDM Split-Pane Monitoring Dashboard