diff --git a/src/components/Kubernetes/DaemonSetList.tsx b/src/components/Kubernetes/DaemonSetList.tsx index 5c454426..fcb050fd 100644 --- a/src/components/Kubernetes/DaemonSetList.tsx +++ b/src/components/Kubernetes/DaemonSetList.tsx @@ -24,7 +24,7 @@ type ActiveModal = | { type: "delete"; ds: DaemonSetInfo } | null; -export function DaemonSetList({ daemonsets, clusterId, namespace, onRefresh }: DaemonSetListProps) { +export function DaemonSetList({ daemonsets, clusterId, namespace: _namespace, onRefresh }: DaemonSetListProps) { const [activeModal, setActiveModal] = useState(null); const [isActing, setIsActing] = useState(false); const [actionError, setActionError] = useState(null); @@ -32,7 +32,7 @@ export function DaemonSetList({ daemonsets, clusterId, namespace, onRefresh }: D const openEdit = async (ds: DaemonSetInfo) => { setActionError(null); try { - const yaml = await getResourceYamlCmd(clusterId, "daemonsets", namespace, ds.name); + const yaml = await getResourceYamlCmd(clusterId, "daemonsets", ds.namespace, ds.name); setActiveModal({ type: "edit", ds, yaml }); } catch (err) { setActionError(err instanceof Error ? err.message : String(err)); @@ -43,7 +43,7 @@ export function DaemonSetList({ daemonsets, clusterId, namespace, onRefresh }: D if (activeModal?.type !== "restart") return; setIsActing(true); try { - await restartDaemonsetCmd(clusterId, namespace, activeModal.ds.name); + await restartDaemonsetCmd(clusterId, activeModal.ds.namespace, activeModal.ds.name); setActiveModal(null); onRefresh?.(); } catch (err) { @@ -57,7 +57,7 @@ export function DaemonSetList({ daemonsets, clusterId, namespace, onRefresh }: D if (activeModal?.type !== "delete") return; setIsActing(true); try { - await deleteResourceCmd(clusterId, "daemonsets", namespace, activeModal.ds.name); + await deleteResourceCmd(clusterId, "daemonsets", activeModal.ds.namespace, activeModal.ds.name); setActiveModal(null); onRefresh?.(); } finally { @@ -146,7 +146,7 @@ export function DaemonSetList({ daemonsets, clusterId, namespace, onRefresh }: D (null); const [isActing, setIsActing] = useState(false); const [actionError, setActionError] = useState(null); @@ -37,7 +37,7 @@ export function DeploymentList({ deployments, clusterId, namespace, onRefresh }: const openEdit = async (deployment: DeploymentInfo) => { setActionError(null); try { - const yaml = await getResourceYamlCmd(clusterId, "deployments", namespace, deployment.name); + const yaml = await getResourceYamlCmd(clusterId, "deployments", deployment.namespace, deployment.name); setActiveModal({ type: "edit", deployment, yaml }); } catch (err) { setActionError(err instanceof Error ? err.message : String(err)); @@ -48,7 +48,7 @@ export function DeploymentList({ deployments, clusterId, namespace, onRefresh }: if (activeModal?.type !== "restart") return; setIsActing(true); try { - await restartDeploymentCmd(clusterId, namespace, activeModal.deployment.name); + await restartDeploymentCmd(clusterId, activeModal.deployment.namespace, activeModal.deployment.name); setActiveModal(null); onRefresh?.(); } catch (err) { @@ -62,7 +62,7 @@ export function DeploymentList({ deployments, clusterId, namespace, onRefresh }: if (activeModal?.type !== "rollback") return; setIsActing(true); try { - await rollbackDeploymentCmd(clusterId, namespace, activeModal.deployment.name); + await rollbackDeploymentCmd(clusterId, activeModal.deployment.namespace, activeModal.deployment.name); setActiveModal(null); onRefresh?.(); } catch (err) { @@ -76,7 +76,7 @@ export function DeploymentList({ deployments, clusterId, namespace, onRefresh }: if (activeModal?.type !== "delete") return; setIsActing(true); try { - await deleteResourceCmd(clusterId, "deployments", namespace, activeModal.deployment.name); + await deleteResourceCmd(clusterId, "deployments", activeModal.deployment.namespace, activeModal.deployment.name); setActiveModal(null); onRefresh?.(); } finally { @@ -165,7 +165,7 @@ export function DeploymentList({ deployments, clusterId, namespace, onRefresh }: resourceName={activeModal.deployment.name} currentReplicas={activeModal.deployment.replicas} onScale={(replicas) => - scaleDeploymentCmd(clusterId, namespace, activeModal.deployment.name, replicas).then(() => { + scaleDeploymentCmd(clusterId, activeModal.deployment.namespace, activeModal.deployment.name, replicas).then(() => { setActiveModal(null); onRefresh?.(); }) @@ -201,7 +201,7 @@ export function DeploymentList({ deployments, clusterId, namespace, onRefresh }: (null); const [isActing, setIsActing] = useState(false); const [actionError, setActionError] = useState(null); @@ -35,7 +35,7 @@ export function StatefulSetList({ statefulsets, clusterId, namespace, onRefresh const openEdit = async (ss: StatefulSetInfo) => { setActionError(null); try { - const yaml = await getResourceYamlCmd(clusterId, "statefulsets", namespace, ss.name); + const yaml = await getResourceYamlCmd(clusterId, "statefulsets", ss.namespace, ss.name); setActiveModal({ type: "edit", ss, yaml }); } catch (err) { setActionError(err instanceof Error ? err.message : String(err)); @@ -46,7 +46,7 @@ export function StatefulSetList({ statefulsets, clusterId, namespace, onRefresh if (activeModal?.type !== "restart") return; setIsActing(true); try { - await restartStatefulsetCmd(clusterId, namespace, activeModal.ss.name); + await restartStatefulsetCmd(clusterId, activeModal.ss.namespace, activeModal.ss.name); setActiveModal(null); onRefresh?.(); } catch (err) { @@ -60,7 +60,7 @@ export function StatefulSetList({ statefulsets, clusterId, namespace, onRefresh if (activeModal?.type !== "delete") return; setIsActing(true); try { - await deleteResourceCmd(clusterId, "statefulsets", namespace, activeModal.ss.name); + await deleteResourceCmd(clusterId, "statefulsets", activeModal.ss.namespace, activeModal.ss.name); setActiveModal(null); onRefresh?.(); } finally { @@ -140,7 +140,7 @@ export function StatefulSetList({ statefulsets, clusterId, namespace, onRefresh resourceName={activeModal.ss.name} currentReplicas={activeModal.ss.replicas} onScale={(replicas) => - scaleStatefulsetCmd(clusterId, namespace, activeModal.ss.name, replicas).then(() => { + scaleStatefulsetCmd(clusterId, activeModal.ss.namespace, activeModal.ss.name, replicas).then(() => { setActiveModal(null); onRefresh?.(); }) @@ -164,7 +164,7 @@ export function StatefulSetList({ statefulsets, clusterId, namespace, onRefresh void; + mockImplementation: (fn: (cmd: string) => Promise) => void; +}; + +const mockInvoke = invoke as MockedInvoke; + +// Helper: open the action menu for the first Actions button, then click a menu item by label +async function openMenuAndClick(label: string) { + const btn = screen.getAllByRole("button", { name: /actions/i })[0]; + fireEvent.click(btn); + const item = await screen.findByRole("button", { name: new RegExp(label, "i") }); + fireEvent.click(item); +} + +// ─── DeploymentList ────────────────────────────────────────────────────────── + +describe("DeploymentList — actions use item.namespace not filter prop", () => { + const deployment: DeploymentInfo = { + name: "nginx", + namespace: "kube-system", + ready: "1/1", + up_to_date: "1", + available: "1", + replicas: 1, + age: "1d", + labels: {}, + }; + + beforeEach(() => { + vi.clearAllMocks(); + mockInvoke.mockResolvedValue("apiVersion: apps/v1"); + }); + + it("openEdit calls getResourceYamlCmd with item.namespace, not 'all'", async () => { + render( + + ); + + await openMenuAndClick("Edit"); + + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith("get_resource_yaml", { + clusterId: "c1", + resourceType: "deployments", + namespace: "kube-system", + resourceName: "nginx", + }); + }); + }); + + it("handleDelete calls deleteResourceCmd with item.namespace, not 'all'", async () => { + mockInvoke.mockImplementation(async (cmd: string) => { + if (cmd === "delete_resource") return undefined; + return "yaml"; + }); + + render( + + ); + + await openMenuAndClick("Delete"); + const confirmBtn = await screen.findByRole("button", { name: /delete|confirm/i }); + fireEvent.click(confirmBtn); + + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith("delete_resource", { + clusterId: "c1", + resourceType: "deployments", + namespace: "kube-system", + resourceName: "nginx", + }); + }); + }); + + it("ScaleModal onScale calls scaleDeploymentCmd with item.namespace, not 'all'", async () => { + mockInvoke.mockResolvedValue(undefined); + + render( + + ); + + await openMenuAndClick("Scale"); + const scaleBtn = await screen.findByRole("button", { name: /scale/i }); + fireEvent.click(scaleBtn); + + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith("scale_deployment", { + clusterId: "c1", + namespace: "kube-system", + deploymentName: "nginx", + replicas: expect.any(Number), + }); + }); + }); + + it("handleRestart calls restartDeploymentCmd with item.namespace, not 'all'", async () => { + mockInvoke.mockImplementation(async (cmd: string) => { + if (cmd === "restart_deployment") return undefined; + return "yaml"; + }); + + render( + + ); + + await openMenuAndClick("Restart"); + const confirmBtn = await screen.findByRole("button", { name: /delete|confirm|restart/i }); + fireEvent.click(confirmBtn); + + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith("restart_deployment", { + clusterId: "c1", + namespace: "kube-system", + deploymentName: "nginx", + }); + }); + }); + + it("handleRollback calls rollbackDeploymentCmd with item.namespace, not 'all'", async () => { + mockInvoke.mockImplementation(async (cmd: string) => { + if (cmd === "rollback_deployment") return undefined; + return "yaml"; + }); + + render( + + ); + + await openMenuAndClick("Rollback"); + const confirmBtn = await screen.findByRole("button", { name: /delete|confirm|rollback/i }); + fireEvent.click(confirmBtn); + + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith("rollback_deployment", { + clusterId: "c1", + namespace: "kube-system", + deploymentName: "nginx", + }); + }); + }); +}); + +// ─── StatefulSetList ───────────────────────────────────────────────────────── + +describe("StatefulSetList — actions use item.namespace not filter prop", () => { + const ss: StatefulSetInfo = { + name: "postgres", + namespace: "kube-system", + ready: "1/1", + replicas: 1, + age: "2d", + labels: {}, + }; + + beforeEach(() => { + vi.clearAllMocks(); + mockInvoke.mockResolvedValue("apiVersion: apps/v1"); + }); + + it("openEdit calls getResourceYamlCmd with item.namespace, not 'all'", async () => { + render( + + ); + + await openMenuAndClick("Edit"); + + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith("get_resource_yaml", { + clusterId: "c1", + resourceType: "statefulsets", + namespace: "kube-system", + resourceName: "postgres", + }); + }); + }); + + it("handleDelete calls deleteResourceCmd with item.namespace, not 'all'", async () => { + mockInvoke.mockImplementation(async (cmd: string) => { + if (cmd === "delete_resource") return undefined; + return "yaml"; + }); + + render( + + ); + + await openMenuAndClick("Delete"); + const confirmBtn = await screen.findByRole("button", { name: /delete|confirm/i }); + fireEvent.click(confirmBtn); + + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith("delete_resource", { + clusterId: "c1", + resourceType: "statefulsets", + namespace: "kube-system", + resourceName: "postgres", + }); + }); + }); + + it("ScaleModal onScale calls scaleStatefulsetCmd with item.namespace, not 'all'", async () => { + mockInvoke.mockResolvedValue(undefined); + + render( + + ); + + await openMenuAndClick("Scale"); + const scaleBtn = await screen.findByRole("button", { name: /scale/i }); + fireEvent.click(scaleBtn); + + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith("scale_statefulset", { + clusterId: "c1", + namespace: "kube-system", + name: "postgres", + replicas: expect.any(Number), + }); + }); + }); + + it("handleRestart calls restartStatefulsetCmd with item.namespace, not 'all'", async () => { + mockInvoke.mockImplementation(async (cmd: string) => { + if (cmd === "restart_statefulset") return undefined; + return "yaml"; + }); + + render( + + ); + + await openMenuAndClick("Restart"); + const confirmBtn = await screen.findByRole("button", { name: /delete|confirm|restart/i }); + fireEvent.click(confirmBtn); + + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith("restart_statefulset", { + clusterId: "c1", + namespace: "kube-system", + name: "postgres", + }); + }); + }); +}); + +// ─── DaemonSetList ─────────────────────────────────────────────────────────── + +describe("DaemonSetList — actions use item.namespace not filter prop", () => { + const ds: DaemonSetInfo = { + name: "fluentd", + namespace: "kube-system", + desired: 3, + current: 3, + ready: 3, + up_to_date: 3, + available: 3, + age: "5d", + labels: {}, + }; + + beforeEach(() => { + vi.clearAllMocks(); + mockInvoke.mockResolvedValue("apiVersion: apps/v1"); + }); + + it("openEdit calls getResourceYamlCmd with item.namespace, not 'all'", async () => { + render( + + ); + + await openMenuAndClick("Edit"); + + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith("get_resource_yaml", { + clusterId: "c1", + resourceType: "daemonsets", + namespace: "kube-system", + resourceName: "fluentd", + }); + }); + }); + + it("handleDelete calls deleteResourceCmd with item.namespace, not 'all'", async () => { + mockInvoke.mockImplementation(async (cmd: string) => { + if (cmd === "delete_resource") return undefined; + return "yaml"; + }); + + render( + + ); + + await openMenuAndClick("Delete"); + const confirmBtn = await screen.findByRole("button", { name: /delete|confirm/i }); + fireEvent.click(confirmBtn); + + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith("delete_resource", { + clusterId: "c1", + resourceType: "daemonsets", + namespace: "kube-system", + resourceName: "fluentd", + }); + }); + }); + + it("handleRestart calls restartDaemonsetCmd with item.namespace, not 'all'", async () => { + mockInvoke.mockImplementation(async (cmd: string) => { + if (cmd === "restart_daemonset") return undefined; + return "yaml"; + }); + + render( + + ); + + await openMenuAndClick("Restart"); + const confirmBtn = await screen.findByRole("button", { name: /delete|confirm|restart/i }); + fireEvent.click(confirmBtn); + + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith("restart_daemonset", { + clusterId: "c1", + namespace: "kube-system", + name: "fluentd", + }); + }); + }); +}); + +// ─── ReplicaSetList ────────────────────────────────────────────────────────── + +describe("ReplicaSetList — actions use item.namespace not filter prop", () => { + const rs: ReplicaSetInfo = { + name: "nginx-abc12", + namespace: "kube-system", + replicas: 2, + ready: "2", + age: "3d", + labels: { app: "nginx" }, + }; + + beforeEach(() => { + vi.clearAllMocks(); + mockInvoke.mockResolvedValue("apiVersion: apps/v1"); + }); + + it("openEdit calls getResourceYamlCmd with item.namespace, not 'all'", async () => { + render( + + ); + + await openMenuAndClick("Edit"); + + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith("get_resource_yaml", { + clusterId: "c1", + resourceType: "replicasets", + namespace: "kube-system", + resourceName: "nginx-abc12", + }); + }); + }); + + it("handleDelete calls deleteResourceCmd with item.namespace, not 'all'", async () => { + mockInvoke.mockImplementation(async (cmd: string) => { + if (cmd === "delete_resource") return undefined; + return "yaml"; + }); + + render( + + ); + + await openMenuAndClick("Delete"); + const confirmBtn = await screen.findByRole("button", { name: /delete|confirm/i }); + fireEvent.click(confirmBtn); + + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith("delete_resource", { + clusterId: "c1", + resourceType: "replicasets", + namespace: "kube-system", + resourceName: "nginx-abc12", + }); + }); + }); + + it("ScaleModal onScale calls scaleReplicasetCmd with item.namespace, not 'all'", async () => { + mockInvoke.mockResolvedValue(undefined); + + render( + + ); + + await openMenuAndClick("Scale"); + const scaleBtn = await screen.findByRole("button", { name: /scale/i }); + fireEvent.click(scaleBtn); + + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith("scale_replicaset", { + clusterId: "c1", + namespace: "kube-system", + name: "nginx-abc12", + replicas: expect.any(Number), + }); + }); + }); +}); + +// ─── JobList ───────────────────────────────────────────────────────────────── + +describe("JobList — actions use item.namespace not filter prop", () => { + const job: JobInfo = { + name: "db-migrate", + namespace: "kube-system", + completions: "1/1", + duration: "45s", + age: "1d", + labels: {}, + }; + + beforeEach(() => { + vi.clearAllMocks(); + mockInvoke.mockResolvedValue("apiVersion: batch/v1"); + }); + + it("openEdit calls getResourceYamlCmd with item.namespace, not 'all'", async () => { + render( + + ); + + await openMenuAndClick("Edit"); + + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith("get_resource_yaml", { + clusterId: "c1", + resourceType: "jobs", + namespace: "kube-system", + resourceName: "db-migrate", + }); + }); + }); + + it("handleDelete calls deleteResourceCmd with item.namespace, not 'all'", async () => { + mockInvoke.mockImplementation(async (cmd: string) => { + if (cmd === "delete_resource") return undefined; + return "yaml"; + }); + + render( + + ); + + await openMenuAndClick("Delete"); + const confirmBtn = await screen.findByRole("button", { name: /delete|confirm/i }); + fireEvent.click(confirmBtn); + + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith("delete_resource", { + clusterId: "c1", + resourceType: "jobs", + namespace: "kube-system", + resourceName: "db-migrate", + }); + }); + }); +}); + +// ─── CronJobList ───────────────────────────────────────────────────────────── + +describe("CronJobList — actions use item.namespace not filter prop", () => { + const cj: CronJobInfo = { + name: "backup", + namespace: "kube-system", + schedule: "0 2 * * *", + active: 0, + last_schedule: "1h", + age: "10d", + labels: {}, + }; + + beforeEach(() => { + vi.clearAllMocks(); + mockInvoke.mockResolvedValue("apiVersion: batch/v1"); + }); + + it("openEdit calls getResourceYamlCmd with item.namespace, not 'all'", async () => { + render( + + ); + + await openMenuAndClick("Edit"); + + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith("get_resource_yaml", { + clusterId: "c1", + resourceType: "cronjobs", + namespace: "kube-system", + resourceName: "backup", + }); + }); + }); + + it("handleDelete calls deleteResourceCmd with item.namespace, not 'all'", async () => { + mockInvoke.mockImplementation(async (cmd: string) => { + if (cmd === "delete_resource") return undefined; + return "yaml"; + }); + + render( + + ); + + await openMenuAndClick("Delete"); + const confirmBtn = await screen.findByRole("button", { name: /delete|confirm/i }); + fireEvent.click(confirmBtn); + + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith("delete_resource", { + clusterId: "c1", + resourceType: "cronjobs", + namespace: "kube-system", + resourceName: "backup", + }); + }); + }); + + it("handleSuspend calls suspendCronjobCmd with item.namespace, not 'all'", async () => { + mockInvoke.mockImplementation(async (cmd: string) => { + if (cmd === "suspend_cronjob") return undefined; + return "yaml"; + }); + + render( + + ); + + await openMenuAndClick("Suspend"); + + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith("suspend_cronjob", { + clusterId: "c1", + namespace: "kube-system", + name: "backup", + }); + }); + }); + + it("handleTrigger calls triggerCronjobCmd with item.namespace, not 'all'", async () => { + mockInvoke.mockImplementation(async (cmd: string) => { + if (cmd === "trigger_cronjob") return undefined; + return "yaml"; + }); + + render( + + ); + + await openMenuAndClick("Trigger"); + + await waitFor(() => { + expect(invoke).toHaveBeenCalledWith("trigger_cronjob", { + clusterId: "c1", + namespace: "kube-system", + name: "backup", + }); + }); + }); +});