From 516161c9087f5def1c5786f3b7a30d9303505897 Mon Sep 17 00:00:00 2001 From: utkarsh patrikar Date: Fri, 13 Nov 2026 22:03:42 +0530 Subject: [PATCH 1/4] fix(ui): confirm watch actions and close log overlay --- src/ui/WatchDashboard.tsx | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) 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 From d747ecb98ba28c440db09b080c645d23688b8e0e Mon Sep 17 00:00:00 2001 From: utkarsh patrikar Date: Fri, 13 Nov 2026 22:22:18 +0530 Subject: [PATCH 2/4] test(ui): cover watch confirmations and log back action --- .../watch-dashboard-actions.test.tsx | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/__tests__/watch-dashboard-actions.test.tsx diff --git a/src/__tests__/watch-dashboard-actions.test.tsx b/src/__tests__/watch-dashboard-actions.test.tsx new file mode 100644 index 00000000..e20fcf3b --- /dev/null +++ b/src/__tests__/watch-dashboard-actions.test.tsx @@ -0,0 +1,86 @@ +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'; +import { getK8sApi } from '../kubernetes/client'; + +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(() => ({ + deleteNamespacedPod: vi.fn(async () => undefined), + readNamespacedPodLog: vi.fn(async () => 'sample log'), + })), +})); + +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(); + const api = getK8sApi() as any; + app = render(, { stdin, stdout, debug: true }); + await wait(); + + stdin.send('r'); + await wait(); + expect(stdout.output).toContain('Press Y to confirm'); + expect(api.deleteNamespacedPod).not.toHaveBeenCalled(); + stdin.send('n'); + await wait(); + expect(api.deleteNamespacedPod).not.toHaveBeenCalled(); + + 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'); + + stdin.send('r'); + await wait(); + stdin.send('y'); + await wait(); + expect(api.deleteNamespacedPod).toHaveBeenCalledWith({ name: 'api-pod', namespace: 'default' }); + }); +}); From 5d509976dfa5fa7906fbbbaae5fa320366588451 Mon Sep 17 00:00:00 2001 From: utkarsh patrikar Date: Fri, 13 Nov 2026 22:24:18 +0530 Subject: [PATCH 3/4] test(ui): confirm watch pod action before log overlay --- src/__tests__/watch-dashboard-actions.test.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/__tests__/watch-dashboard-actions.test.tsx b/src/__tests__/watch-dashboard-actions.test.tsx index e20fcf3b..8a69ff8f 100644 --- a/src/__tests__/watch-dashboard-actions.test.tsx +++ b/src/__tests__/watch-dashboard-actions.test.tsx @@ -69,6 +69,12 @@ describe('WatchDashboard actions', () => { await wait(); expect(api.deleteNamespacedPod).not.toHaveBeenCalled(); + stdin.send('r'); + await wait(); + stdin.send('y'); + await wait(); + expect(api.deleteNamespacedPod).toHaveBeenCalledWith({ name: 'api-pod', namespace: 'default' }); + stdin.send('l'); await wait(); expect(stdout.output).toContain('[SNAPSHOT]'); @@ -77,10 +83,5 @@ describe('WatchDashboard actions', () => { await wait(); expect(stdout.output).toContain('KDM Split-Pane Monitoring Dashboard'); - stdin.send('r'); - await wait(); - stdin.send('y'); - await wait(); - expect(api.deleteNamespacedPod).toHaveBeenCalledWith({ name: 'api-pod', namespace: 'default' }); }); }); From 685f10c8a008279b7acbdf7dc33b077d29e5befe Mon Sep 17 00:00:00 2001 From: utkarsh patrikar Date: Fri, 13 Nov 2026 22:27:11 +0530 Subject: [PATCH 4/4] test(ui): share WatchDashboard Kubernetes API mock --- .../watch-dashboard-actions.test.tsx | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/__tests__/watch-dashboard-actions.test.tsx b/src/__tests__/watch-dashboard-actions.test.tsx index 8a69ff8f..4cf4a40f 100644 --- a/src/__tests__/watch-dashboard-actions.test.tsx +++ b/src/__tests__/watch-dashboard-actions.test.tsx @@ -3,7 +3,11 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { render } from 'ink'; import { Readable, Writable } from 'node:stream'; import { WatchDashboard } from '../ui/WatchDashboard'; -import { getK8sApi } from '../kubernetes/client'; + +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' }]), @@ -15,10 +19,7 @@ vi.mock('../docker/containers', () => ({ formatDockerBytes: vi.fn(() => '0 B'), })); vi.mock('../kubernetes/client', () => ({ - getK8sApi: vi.fn(() => ({ - deleteNamespacedPod: vi.fn(async () => undefined), - readNamespacedPodLog: vi.fn(async () => 'sample log'), - })), + getK8sApi: vi.fn(() => k8sApiMock), })); class TestStdout extends Writable { @@ -57,23 +58,24 @@ describe('WatchDashboard actions', () => { it('confirms Pod deletion and lets b close the log snapshot', async () => { const stdout = new TestStdout(); stdin = new TestStdin(); - const api = getK8sApi() as any; + 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(api.deleteNamespacedPod).not.toHaveBeenCalled(); + expect(k8sApiMock.deleteNamespacedPod).not.toHaveBeenCalled(); stdin.send('n'); await wait(); - expect(api.deleteNamespacedPod).not.toHaveBeenCalled(); + expect(k8sApiMock.deleteNamespacedPod).not.toHaveBeenCalled(); stdin.send('r'); await wait(); stdin.send('y'); await wait(); - expect(api.deleteNamespacedPod).toHaveBeenCalledWith({ name: 'api-pod', namespace: 'default' }); + expect(k8sApiMock.deleteNamespacedPod).toHaveBeenCalledWith({ name: 'api-pod', namespace: 'default' }); stdin.send('l'); await wait();