fix(kube): add namespace to PodInfo; pod actions use pod.namespace not filter
Pod actions (logs, shell, attach, edit, delete) were receiving namespace='all' from the UI filter prop and passing it to kubectl as -n all. Fixes by adding namespace field to PodInfo (Rust + TypeScript) and using pod.namespace in all action command calls in PodList.
This commit is contained in:
parent
bf8443c9f5
commit
84bac9aa34
@ -91,6 +91,7 @@ pub struct PortForwardResponse {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PodInfo {
|
||||
pub name: String,
|
||||
pub namespace: String,
|
||||
pub status: String,
|
||||
pub ready: String,
|
||||
pub age: String,
|
||||
@ -1107,6 +1108,13 @@ fn parse_pods_json(json_str: &str) -> Result<Vec<PodInfo>, String> {
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
let namespace = item
|
||||
.get("metadata")
|
||||
.and_then(|m| m.get("namespace"))
|
||||
.and_then(|n| n.as_str())
|
||||
.unwrap_or("default")
|
||||
.to_string();
|
||||
|
||||
let status = item
|
||||
.get("status")
|
||||
.and_then(|s| s.get("phase"))
|
||||
@ -1153,6 +1161,7 @@ fn parse_pods_json(json_str: &str) -> Result<Vec<PodInfo>, String> {
|
||||
|
||||
pods.push(PodInfo {
|
||||
name,
|
||||
namespace,
|
||||
status,
|
||||
ready,
|
||||
age,
|
||||
|
||||
@ -32,6 +32,9 @@ export function PodList({ pods, clusterId, namespace, onRefresh }: PodListProps)
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [editError, setEditError] = useState<string | null>(null);
|
||||
|
||||
// namespace prop is retained for API compatibility (parent uses it to drive list fetches)
|
||||
void namespace;
|
||||
|
||||
const getPodStatusColor = (status: string) => {
|
||||
switch (status.toLowerCase()) {
|
||||
case "running":
|
||||
@ -52,7 +55,7 @@ export function PodList({ pods, clusterId, namespace, onRefresh }: PodListProps)
|
||||
const openEdit = async (pod: PodInfo) => {
|
||||
setEditError(null);
|
||||
try {
|
||||
const yaml = await getResourceYamlCmd(clusterId, "pods", namespace, pod.name);
|
||||
const yaml = await getResourceYamlCmd(clusterId, "pods", pod.namespace, pod.name);
|
||||
setActiveModal({ type: "edit", pod, yaml });
|
||||
} catch (err) {
|
||||
setEditError(err instanceof Error ? err.message : String(err));
|
||||
@ -65,9 +68,9 @@ export function PodList({ pods, clusterId, namespace, onRefresh }: PodListProps)
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
if (force) {
|
||||
await forceDeleteResourceCmd(clusterId, "pods", namespace, modal.pod.name);
|
||||
await forceDeleteResourceCmd(clusterId, "pods", modal.pod.namespace, modal.pod.name);
|
||||
} else {
|
||||
await deleteResourceCmd(clusterId, "pods", namespace, modal.pod.name);
|
||||
await deleteResourceCmd(clusterId, "pods", modal.pod.namespace, modal.pod.name);
|
||||
}
|
||||
setActiveModal(null);
|
||||
onRefresh?.();
|
||||
@ -167,7 +170,7 @@ export function PodList({ pods, clusterId, namespace, onRefresh }: PodListProps)
|
||||
open
|
||||
onOpenChange={(o) => { if (!o) setActiveModal(null); }}
|
||||
clusterId={clusterId}
|
||||
namespace={namespace}
|
||||
namespace={activeModal.pod.namespace}
|
||||
podName={activeModal.pod.name}
|
||||
containers={activeModal.pod.containers}
|
||||
/>
|
||||
@ -178,7 +181,7 @@ export function PodList({ pods, clusterId, namespace, onRefresh }: PodListProps)
|
||||
open
|
||||
onOpenChange={(o) => { if (!o) setActiveModal(null); }}
|
||||
clusterId={clusterId}
|
||||
namespace={namespace}
|
||||
namespace={activeModal.pod.namespace}
|
||||
podName={activeModal.pod.name}
|
||||
containers={activeModal.pod.containers}
|
||||
/>
|
||||
@ -189,7 +192,7 @@ export function PodList({ pods, clusterId, namespace, onRefresh }: PodListProps)
|
||||
open
|
||||
onOpenChange={(o) => { if (!o) setActiveModal(null); }}
|
||||
clusterId={clusterId}
|
||||
namespace={namespace}
|
||||
namespace={activeModal.pod.namespace}
|
||||
podName={activeModal.pod.name}
|
||||
containers={activeModal.pod.containers}
|
||||
/>
|
||||
@ -199,7 +202,7 @@ export function PodList({ pods, clusterId, namespace, onRefresh }: PodListProps)
|
||||
<EditResourceModal
|
||||
isOpen
|
||||
clusterId={clusterId}
|
||||
namespace={namespace}
|
||||
namespace={activeModal.pod.namespace}
|
||||
resourceType="pods"
|
||||
resourceName={activeModal.pod.name}
|
||||
initialYaml={activeModal.yaml}
|
||||
|
||||
@ -795,6 +795,7 @@ export interface PortForwardResponse {
|
||||
|
||||
export interface PodInfo {
|
||||
name: string;
|
||||
namespace: string;
|
||||
status: string;
|
||||
ready: string;
|
||||
age: string;
|
||||
|
||||
@ -17,6 +17,7 @@ const mockInvoke = invoke as MockedInvoke;
|
||||
|
||||
const mockPod: PodInfo = {
|
||||
name: "nginx-abc123",
|
||||
namespace: "default",
|
||||
status: "Running",
|
||||
ready: "2/2",
|
||||
age: "3h",
|
||||
|
||||
223
tests/unit/PodList.test.tsx
Normal file
223
tests/unit/PodList.test.tsx
Normal file
@ -0,0 +1,223 @@
|
||||
import React from "react";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { PodList } from "@/components/Kubernetes/PodList";
|
||||
import type { PodInfo } from "@/lib/tauriCommands";
|
||||
|
||||
vi.mock("@tauri-apps/api/core");
|
||||
|
||||
// Silence console.error noise from modal portals in jsdom
|
||||
vi.mock("@/components/Kubernetes/LogsModal", () => ({
|
||||
LogsModal: ({ namespace }: { namespace: string }) => (
|
||||
<div data-testid="logs-modal" data-namespace={namespace} />
|
||||
),
|
||||
}));
|
||||
vi.mock("@/components/Kubernetes/ShellExecModal", () => ({
|
||||
ShellExecModal: ({ namespace }: { namespace: string }) => (
|
||||
<div data-testid="shell-modal" data-namespace={namespace} />
|
||||
),
|
||||
}));
|
||||
vi.mock("@/components/Kubernetes/AttachModal", () => ({
|
||||
AttachModal: ({ namespace }: { namespace: string }) => (
|
||||
<div data-testid="attach-modal" data-namespace={namespace} />
|
||||
),
|
||||
}));
|
||||
vi.mock("@/components/Kubernetes/EditResourceModal", () => ({
|
||||
EditResourceModal: ({ namespace }: { namespace: string }) => (
|
||||
<div data-testid="edit-modal" data-namespace={namespace} />
|
||||
),
|
||||
}));
|
||||
vi.mock("@/components/Kubernetes/ConfirmDeleteDialog", () => ({
|
||||
ConfirmDeleteDialog: ({
|
||||
onConfirm,
|
||||
resourceName,
|
||||
}: {
|
||||
onConfirm: () => void;
|
||||
resourceName: string;
|
||||
}) => (
|
||||
<div data-testid="confirm-delete">
|
||||
<span>{resourceName}</span>
|
||||
<button onClick={onConfirm}>confirm</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
type MockedInvoke = typeof invoke & {
|
||||
mockResolvedValue: (v: unknown) => void;
|
||||
mockRejectedValue: (e: Error) => void;
|
||||
mockImplementation: (fn: (cmd: string) => Promise<unknown>) => void;
|
||||
};
|
||||
|
||||
const mockInvoke = invoke as MockedInvoke;
|
||||
|
||||
// A pod whose own namespace ("default") differs from the filter prop ("all")
|
||||
const mockPod: PodInfo = {
|
||||
name: "test-pod",
|
||||
namespace: "default",
|
||||
status: "Running",
|
||||
ready: "1/1",
|
||||
age: "1h",
|
||||
containers: ["app"],
|
||||
};
|
||||
|
||||
function openActionMenu() {
|
||||
const trigger = screen.getByRole("button", { name: /actions/i });
|
||||
fireEvent.click(trigger);
|
||||
}
|
||||
|
||||
describe("PodList — namespace isolation", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('Edit action calls getResourceYamlCmd with pod.namespace ("default"), not filter "all"', async () => {
|
||||
mockInvoke.mockImplementation((cmd: string) => {
|
||||
if (cmd === "get_resource_yaml") {
|
||||
return Promise.resolve("apiVersion: v1\nkind: Pod");
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
|
||||
render(
|
||||
<PodList
|
||||
pods={[mockPod]}
|
||||
clusterId="c1"
|
||||
namespace="all"
|
||||
onRefresh={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
openActionMenu();
|
||||
fireEvent.click(screen.getByRole("button", { name: /^edit$/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockInvoke).toHaveBeenCalledWith("get_resource_yaml", {
|
||||
clusterId: "c1",
|
||||
namespace: "default",
|
||||
resourceType: "pods",
|
||||
resourceName: "test-pod",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Delete action calls deleteResourceCmd with pod.namespace ("default"), not filter "all"', async () => {
|
||||
mockInvoke.mockResolvedValue(undefined);
|
||||
|
||||
render(
|
||||
<PodList
|
||||
pods={[mockPod]}
|
||||
clusterId="c1"
|
||||
namespace="all"
|
||||
onRefresh={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
openActionMenu();
|
||||
fireEvent.click(screen.getByRole("button", { name: /^delete$/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("confirm-delete")).toBeDefined();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /confirm/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockInvoke).toHaveBeenCalledWith("delete_resource", {
|
||||
clusterId: "c1",
|
||||
namespace: "default",
|
||||
resourceType: "pods",
|
||||
resourceName: "test-pod",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Force Delete action calls forceDeleteResourceCmd with pod.namespace ("default"), not filter "all"', async () => {
|
||||
mockInvoke.mockResolvedValue(undefined);
|
||||
|
||||
// Force Delete is only visible when pod is Running or Pending
|
||||
render(
|
||||
<PodList
|
||||
pods={[mockPod]}
|
||||
clusterId="c1"
|
||||
namespace="all"
|
||||
onRefresh={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
openActionMenu();
|
||||
fireEvent.click(screen.getByRole("button", { name: /^force delete$/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("confirm-delete")).toBeDefined();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /confirm/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockInvoke).toHaveBeenCalledWith("force_delete_resource", {
|
||||
clusterId: "c1",
|
||||
namespace: "default",
|
||||
resourceType: "pods",
|
||||
resourceName: "test-pod",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Logs modal receives pod.namespace ("default"), not filter "all"', async () => {
|
||||
render(
|
||||
<PodList
|
||||
pods={[mockPod]}
|
||||
clusterId="c1"
|
||||
namespace="all"
|
||||
onRefresh={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
openActionMenu();
|
||||
fireEvent.click(screen.getByRole("button", { name: /^logs$/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
const modal = screen.getByTestId("logs-modal");
|
||||
expect(modal.getAttribute("data-namespace")).toBe("default");
|
||||
});
|
||||
});
|
||||
|
||||
it('Shell modal receives pod.namespace ("default"), not filter "all"', async () => {
|
||||
render(
|
||||
<PodList
|
||||
pods={[mockPod]}
|
||||
clusterId="c1"
|
||||
namespace="all"
|
||||
onRefresh={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
openActionMenu();
|
||||
fireEvent.click(screen.getByRole("button", { name: /^shell$/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
const modal = screen.getByTestId("shell-modal");
|
||||
expect(modal.getAttribute("data-namespace")).toBe("default");
|
||||
});
|
||||
});
|
||||
|
||||
it('Attach modal receives pod.namespace ("default"), not filter "all"', async () => {
|
||||
render(
|
||||
<PodList
|
||||
pods={[mockPod]}
|
||||
clusterId="c1"
|
||||
namespace="all"
|
||||
onRefresh={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
openActionMenu();
|
||||
fireEvent.click(screen.getByRole("button", { name: /^attach$/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
const modal = screen.getByTestId("attach-modal");
|
||||
expect(modal.getAttribute("data-namespace")).toBe("default");
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user