feat(kubernetes): implement Lens Desktop v5 feature-parity UI
Complete overhaul of the Kubernetes management page from a basic config
panel into a full Lens-style IDE shell with 26 resource types, real-time
data, and a comprehensive test suite.
Layout & navigation:
- Rewrite KubernetesPage as a Lens v5-style shell: collapsible sidebar
(Workloads / Services & Networking / Config & Storage / Access Control /
Cluster), top hotbar with cluster+namespace selectors, Ctrl+K command
palette
- All 26 resource types now accessible via sidebar navigation (previously 5)
New resource types (Rust + TypeScript + React):
- StorageClasses, NetworkPolicies, ResourceQuotas, LimitRanges
- 4 new Tauri commands registered in generate_handler![]
Component implementations (replacing stubs with real IPC):
- Terminal: full xterm.js with multi-tab sessions and exec_pod IPC
- YamlEditor: Monaco editor with YAML syntax highlighting
- MetricsChart: recharts LineChart/BarChart
- ClusterOverview: live node/pod/deployment/namespace counts
- ClusterDetails: real kubeconfig + node data
- PodDetail, DeploymentDetail, ServiceDetail, ConfigMapDetail, SecretDetail:
all connected to real IPC data, zero hardcoded values
- CreateResourceModal, EditResourceModal: wired to createResourceCmd /
editResourceCmd
- RbacViewer: live data from 4 RBAC IPC commands
- RbacEditor: create roles/cluster-roles via YAML editor
- CommandPalette: 12 real navigation commands, keyboard nav
Dependencies added: xterm@5, xterm-addon-fit, xterm-addon-web-links,
@monaco-editor/react@4, recharts@2
Tooling:
- Replace eslint-plugin-react (incompatible with ESLint 10) with
@eslint-react/eslint-plugin; fix eslint.config.js for flat config
- Fix pre-existing hoisting lint errors in Security.tsx, PortForwardForm.tsx
- Fix eventBus.ts: replace all `any` generics with `unknown`
Tests: 251 passing across 35 test files (was 94/19)
- 16 new test files covering all new and fixed components (TDD)
- npx tsc --noEmit: 0 errors
- cargo clippy -- -D warnings: 0 warnings
- cargo fmt --check: passes
- eslint src/ --max-warnings 0: 0 issues
2026-06-07 21:41:28 +00:00
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
|
|
|
import { render, screen, waitFor, act } from "@testing-library/react";
|
|
|
|
|
import userEvent from "@testing-library/user-event";
|
|
|
|
|
|
|
|
|
|
// ── xterm mocks ───────────────────────────────────────────────────────────────
|
|
|
|
|
// onData callbacks registered by the component — keyed by call order
|
|
|
|
|
const onDataHandlers: Array<(data: string) => void> = [];
|
|
|
|
|
|
|
|
|
|
const mockTerminalInstance = {
|
|
|
|
|
open: vi.fn(),
|
|
|
|
|
write: vi.fn(),
|
|
|
|
|
writeln: vi.fn(),
|
|
|
|
|
dispose: vi.fn(),
|
|
|
|
|
onData: vi.fn((cb: (data: string) => void) => {
|
|
|
|
|
onDataHandlers.push(cb);
|
|
|
|
|
}),
|
|
|
|
|
loadAddon: vi.fn(),
|
|
|
|
|
options: {} as Record<string, unknown>,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Must use function (not arrow) so `new` works
|
|
|
|
|
vi.mock("xterm", () => ({
|
|
|
|
|
Terminal: vi.fn(function () {
|
|
|
|
|
return mockTerminalInstance;
|
|
|
|
|
}),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const mockFitAddon = { fit: vi.fn(), dispose: vi.fn() };
|
|
|
|
|
vi.mock("xterm-addon-fit", () => ({
|
|
|
|
|
FitAddon: vi.fn(function () {
|
|
|
|
|
return mockFitAddon;
|
|
|
|
|
}),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const mockWebLinksAddon = { dispose: vi.fn() };
|
|
|
|
|
vi.mock("xterm-addon-web-links", () => ({
|
|
|
|
|
WebLinksAddon: vi.fn(function () {
|
|
|
|
|
return mockWebLinksAddon;
|
|
|
|
|
}),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// ── Tauri command mock ────────────────────────────────────────────────────────
|
|
|
|
|
vi.mock("@/lib/tauriCommands", () => ({
|
|
|
|
|
execPodCmd: vi.fn(),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
import * as tauriCommands from "@/lib/tauriCommands";
|
|
|
|
|
import { Terminal } from "@/components/Kubernetes/Terminal";
|
|
|
|
|
|
|
|
|
|
type MockedFn<T extends (...args: unknown[]) => unknown = (...args: unknown[]) => unknown> =
|
|
|
|
|
T & ReturnType<typeof vi.fn>;
|
|
|
|
|
|
|
|
|
|
const execPodCmdMock = tauriCommands.execPodCmd as MockedFn;
|
|
|
|
|
|
|
|
|
|
const defaultProps = {
|
|
|
|
|
clusterId: "cluster-1",
|
|
|
|
|
namespace: "default",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const withPodProps = {
|
|
|
|
|
...defaultProps,
|
|
|
|
|
podName: "nginx-abc",
|
|
|
|
|
containerName: "nginx",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ── helper: get the onData handler registered for a session ──────────────────
|
|
|
|
|
function getOnDataCallback(): (data: string) => void {
|
|
|
|
|
const cb = onDataHandlers[onDataHandlers.length - 1];
|
|
|
|
|
if (!cb) throw new Error("No onData handler registered — terminal may not have mounted");
|
|
|
|
|
return cb;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── tests ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
describe("Terminal component", () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
onDataHandlers.length = 0;
|
|
|
|
|
// Re-wire onData to push into our handler array after clearAllMocks
|
|
|
|
|
mockTerminalInstance.onData.mockImplementation((cb: (data: string) => void) => {
|
|
|
|
|
onDataHandlers.push(cb);
|
|
|
|
|
});
|
|
|
|
|
execPodCmdMock.mockResolvedValue({ stdout: "hello\nworld", stderr: "", exit_code: 0 });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
vi.restoreAllMocks();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("empty state", () => {
|
|
|
|
|
it("renders without crashing", () => {
|
|
|
|
|
render(<Terminal {...defaultProps} />);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("shows 'Select a pod to connect' when no pod/container is provided", () => {
|
|
|
|
|
render(<Terminal {...defaultProps} />);
|
|
|
|
|
expect(screen.getByText(/select a pod to connect/i)).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("does not show a tab bar when there are no sessions", () => {
|
|
|
|
|
render(<Terminal {...defaultProps} />);
|
|
|
|
|
expect(screen.queryByRole("tab")).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("session management", () => {
|
|
|
|
|
it("shows tab bar when a session is auto-created from props", async () => {
|
|
|
|
|
render(<Terminal {...withPodProps} />);
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(screen.getByRole("tab")).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("tab label contains pod/container name", async () => {
|
|
|
|
|
render(<Terminal {...withPodProps} />);
|
|
|
|
|
await waitFor(() => screen.getByRole("tab"));
|
|
|
|
|
expect(screen.getByRole("tab").textContent).toContain("nginx-abc");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("clicking '+' button adds a new session tab", async () => {
|
|
|
|
|
render(<Terminal {...withPodProps} />);
|
|
|
|
|
await waitFor(() => screen.getByRole("tab"));
|
|
|
|
|
|
|
|
|
|
expect(screen.getAllByRole("tab")).toHaveLength(1);
|
|
|
|
|
|
|
|
|
|
const addButton = screen.getByRole("button", { name: /add session/i });
|
|
|
|
|
await userEvent.click(addButton);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(screen.getAllByRole("tab")).toHaveLength(2);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("clicking the X on a tab removes that session", async () => {
|
|
|
|
|
render(<Terminal {...withPodProps} />);
|
|
|
|
|
await waitFor(() => screen.getByRole("tab"));
|
|
|
|
|
|
|
|
|
|
const closeBtn = screen.getByRole("button", { name: /close/i });
|
|
|
|
|
await userEvent.click(closeBtn);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(screen.queryByRole("tab")).not.toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("removing the last session goes back to the empty state", async () => {
|
|
|
|
|
render(<Terminal {...withPodProps} />);
|
|
|
|
|
await waitFor(() => screen.getByRole("tab"));
|
|
|
|
|
|
|
|
|
|
const closeBtn = screen.getByRole("button", { name: /close/i });
|
|
|
|
|
await userEvent.click(closeBtn);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(screen.getByText(/select a pod to connect/i)).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("IPC integration", () => {
|
|
|
|
|
it("calls execPodCmd with correct arguments when a command is entered", async () => {
|
|
|
|
|
render(<Terminal {...withPodProps} />);
|
|
|
|
|
await waitFor(() => screen.getByRole("tab"));
|
|
|
|
|
|
|
|
|
|
// onData must have been registered by now
|
|
|
|
|
expect(mockTerminalInstance.onData).toHaveBeenCalled();
|
|
|
|
|
const onDataCallback = getOnDataCallback();
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
onDataCallback("l");
|
|
|
|
|
onDataCallback("s");
|
|
|
|
|
onDataCallback("\r");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(execPodCmdMock).toHaveBeenCalledWith(
|
|
|
|
|
"cluster-1",
|
|
|
|
|
"default",
|
|
|
|
|
"nginx-abc",
|
|
|
|
|
"nginx",
|
|
|
|
|
"ls",
|
|
|
|
|
expect.any(String)
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("writes command output to the terminal after execution", async () => {
|
|
|
|
|
execPodCmdMock.mockResolvedValue({ stdout: "file1.txt\nfile2.txt", stderr: "", exit_code: 0 });
|
|
|
|
|
|
|
|
|
|
render(<Terminal {...withPodProps} />);
|
|
|
|
|
await waitFor(() => screen.getByRole("tab"));
|
|
|
|
|
|
|
|
|
|
const onDataCallback = getOnDataCallback();
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
onDataCallback("l");
|
|
|
|
|
onDataCallback("s");
|
|
|
|
|
onDataCallback("\r");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
const writeCalls = mockTerminalInstance.write.mock.calls.map((c: unknown[]) => c[0] as string);
|
|
|
|
|
expect(writeCalls.some((s) => s.includes("file1.txt"))).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("handles IPC errors gracefully by writing an error message to the terminal", async () => {
|
|
|
|
|
execPodCmdMock.mockRejectedValue(new Error("connection refused"));
|
|
|
|
|
|
|
|
|
|
render(<Terminal {...withPodProps} />);
|
|
|
|
|
await waitFor(() => screen.getByRole("tab"));
|
|
|
|
|
|
|
|
|
|
const onDataCallback = getOnDataCallback();
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
onDataCallback("e");
|
|
|
|
|
onDataCallback("c");
|
|
|
|
|
onDataCallback("h");
|
|
|
|
|
onDataCallback("o");
|
|
|
|
|
onDataCallback("\r");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
const writeCalls = mockTerminalInstance.write.mock.calls.map((c: unknown[]) => c[0] as string);
|
|
|
|
|
expect(
|
|
|
|
|
writeCalls.some((s) => s.toLowerCase().includes("error") || s.includes("connection refused"))
|
|
|
|
|
).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("writes stderr output to the terminal when exit_code is non-zero", async () => {
|
|
|
|
|
execPodCmdMock.mockResolvedValue({ stdout: "", stderr: "command not found", exit_code: 127 });
|
|
|
|
|
|
|
|
|
|
render(<Terminal {...withPodProps} />);
|
|
|
|
|
await waitFor(() => screen.getByRole("tab"));
|
|
|
|
|
|
|
|
|
|
const onDataCallback = getOnDataCallback();
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
onDataCallback("b");
|
|
|
|
|
onDataCallback("a");
|
|
|
|
|
onDataCallback("d");
|
|
|
|
|
onDataCallback("\r");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
const writeCalls = mockTerminalInstance.write.mock.calls.map((c: unknown[]) => c[0] as string);
|
|
|
|
|
expect(writeCalls.some((s) => s.includes("command not found"))).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("shell selector", () => {
|
|
|
|
|
it("renders a shell selector dropdown", async () => {
|
|
|
|
|
render(<Terminal {...withPodProps} />);
|
|
|
|
|
await waitFor(() => screen.getByRole("tab"));
|
|
|
|
|
|
|
|
|
|
const shellSelector = screen.getByRole("combobox");
|
|
|
|
|
expect(shellSelector).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("passes selected shell to execPodCmd", async () => {
|
|
|
|
|
render(<Terminal {...withPodProps} />);
|
|
|
|
|
await waitFor(() => screen.getByRole("tab"));
|
|
|
|
|
|
|
|
|
|
const shellSelector = screen.getByRole("combobox");
|
|
|
|
|
await userEvent.selectOptions(shellSelector, "sh");
|
|
|
|
|
|
|
|
|
|
const onDataCallback = getOnDataCallback();
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
onDataCallback("p");
|
|
|
|
|
onDataCallback("w");
|
|
|
|
|
onDataCallback("d");
|
|
|
|
|
onDataCallback("\r");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(execPodCmdMock).toHaveBeenCalledWith(
|
|
|
|
|
expect.any(String),
|
|
|
|
|
expect.any(String),
|
|
|
|
|
expect.any(String),
|
|
|
|
|
expect.any(String),
|
|
|
|
|
"pwd",
|
|
|
|
|
"sh"
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("cleanup", () => {
|
|
|
|
|
it("calls terminal.dispose() on unmount", async () => {
|
|
|
|
|
const { unmount } = render(<Terminal {...withPodProps} />);
|
|
|
|
|
await waitFor(() => screen.getByRole("tab"));
|
|
|
|
|
|
|
|
|
|
unmount();
|
|
|
|
|
|
|
|
|
|
expect(mockTerminalInstance.dispose).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-06-09 18:28:30 +00:00
|
|
|
|
|
|
|
|
describe("terminal configuration", () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
localStorage.clear();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("renders settings button in tab bar", async () => {
|
|
|
|
|
render(<Terminal {...withPodProps} />);
|
|
|
|
|
await waitFor(() => screen.getByRole("tab"));
|
|
|
|
|
|
|
|
|
|
const settingsBtn = screen.queryByRole("button", { name: /settings/i });
|
|
|
|
|
expect(settingsBtn).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("opens settings dialog when settings button is clicked", async () => {
|
|
|
|
|
render(<Terminal {...withPodProps} />);
|
|
|
|
|
await waitFor(() => screen.getByRole("tab"));
|
|
|
|
|
|
|
|
|
|
const settingsBtn = screen.getByRole("button", { name: /settings/i });
|
|
|
|
|
await userEvent.click(settingsBtn);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(screen.getByText(/terminal settings/i)).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("shows copy-on-select toggle in settings dialog", async () => {
|
|
|
|
|
render(<Terminal {...withPodProps} />);
|
|
|
|
|
await waitFor(() => screen.getByRole("tab"));
|
|
|
|
|
|
|
|
|
|
const settingsBtn = screen.getByRole("button", { name: /settings/i });
|
|
|
|
|
await userEvent.click(settingsBtn);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(screen.getByLabelText(/copy on select/i)).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("shows font family input in settings dialog", async () => {
|
|
|
|
|
render(<Terminal {...withPodProps} />);
|
|
|
|
|
await waitFor(() => screen.getByRole("tab"));
|
|
|
|
|
|
|
|
|
|
const settingsBtn = screen.getByRole("button", { name: /settings/i });
|
|
|
|
|
await userEvent.click(settingsBtn);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(screen.getByLabelText(/font family/i)).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("shows font size input in settings dialog", async () => {
|
|
|
|
|
render(<Terminal {...withPodProps} />);
|
|
|
|
|
await waitFor(() => screen.getByRole("tab"));
|
|
|
|
|
|
|
|
|
|
const settingsBtn = screen.getByRole("button", { name: /settings/i });
|
|
|
|
|
await userEvent.click(settingsBtn);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(screen.getByLabelText(/font size/i)).toBeInTheDocument();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("persists settings to localStorage when changed", async () => {
|
|
|
|
|
render(<Terminal {...withPodProps} />);
|
|
|
|
|
await waitFor(() => screen.getByRole("tab"));
|
|
|
|
|
|
|
|
|
|
const settingsBtn = screen.getByRole("button", { name: /settings/i });
|
|
|
|
|
await userEvent.click(settingsBtn);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => screen.getByLabelText(/copy on select/i));
|
|
|
|
|
|
|
|
|
|
const copyOnSelectCheckbox = screen.getByLabelText(/copy on select/i) as HTMLInputElement;
|
|
|
|
|
await userEvent.click(copyOnSelectCheckbox);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
const stored = localStorage.getItem("terminal-settings");
|
|
|
|
|
expect(stored).toBeTruthy();
|
|
|
|
|
const parsed = JSON.parse(stored || "{}");
|
|
|
|
|
expect(parsed.copyOnSelect).toBeDefined();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("loads settings from localStorage on mount", async () => {
|
|
|
|
|
localStorage.setItem(
|
|
|
|
|
"terminal-settings",
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
copyOnSelect: true,
|
|
|
|
|
fontFamily: "Courier New",
|
|
|
|
|
fontSize: 16,
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
render(<Terminal {...withPodProps} />);
|
|
|
|
|
await waitFor(() => screen.getByRole("tab"));
|
|
|
|
|
|
|
|
|
|
const settingsBtn = screen.getByRole("button", { name: /settings/i });
|
|
|
|
|
await userEvent.click(settingsBtn);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
const copyOnSelectCheckbox = screen.getByLabelText(/copy on select/i) as HTMLInputElement;
|
|
|
|
|
expect(copyOnSelectCheckbox.checked).toBe(true);
|
|
|
|
|
|
|
|
|
|
const fontFamilyInput = screen.getByLabelText(/font family/i) as HTMLInputElement;
|
|
|
|
|
expect(fontFamilyInput.value).toBe("Courier New");
|
|
|
|
|
|
|
|
|
|
const fontSizeInput = screen.getByLabelText(/font size/i) as HTMLInputElement;
|
|
|
|
|
expect(fontSizeInput.value).toBe("16");
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
feat(kubernetes): implement Lens Desktop v5 feature-parity UI
Complete overhaul of the Kubernetes management page from a basic config
panel into a full Lens-style IDE shell with 26 resource types, real-time
data, and a comprehensive test suite.
Layout & navigation:
- Rewrite KubernetesPage as a Lens v5-style shell: collapsible sidebar
(Workloads / Services & Networking / Config & Storage / Access Control /
Cluster), top hotbar with cluster+namespace selectors, Ctrl+K command
palette
- All 26 resource types now accessible via sidebar navigation (previously 5)
New resource types (Rust + TypeScript + React):
- StorageClasses, NetworkPolicies, ResourceQuotas, LimitRanges
- 4 new Tauri commands registered in generate_handler![]
Component implementations (replacing stubs with real IPC):
- Terminal: full xterm.js with multi-tab sessions and exec_pod IPC
- YamlEditor: Monaco editor with YAML syntax highlighting
- MetricsChart: recharts LineChart/BarChart
- ClusterOverview: live node/pod/deployment/namespace counts
- ClusterDetails: real kubeconfig + node data
- PodDetail, DeploymentDetail, ServiceDetail, ConfigMapDetail, SecretDetail:
all connected to real IPC data, zero hardcoded values
- CreateResourceModal, EditResourceModal: wired to createResourceCmd /
editResourceCmd
- RbacViewer: live data from 4 RBAC IPC commands
- RbacEditor: create roles/cluster-roles via YAML editor
- CommandPalette: 12 real navigation commands, keyboard nav
Dependencies added: xterm@5, xterm-addon-fit, xterm-addon-web-links,
@monaco-editor/react@4, recharts@2
Tooling:
- Replace eslint-plugin-react (incompatible with ESLint 10) with
@eslint-react/eslint-plugin; fix eslint.config.js for flat config
- Fix pre-existing hoisting lint errors in Security.tsx, PortForwardForm.tsx
- Fix eventBus.ts: replace all `any` generics with `unknown`
Tests: 251 passing across 35 test files (was 94/19)
- 16 new test files covering all new and fixed components (TDD)
- npx tsc --noEmit: 0 errors
- cargo clippy -- -D warnings: 0 warnings
- cargo fmt --check: passes
- eslint src/ --max-warnings 0: 0 issues
2026-06-07 21:41:28 +00:00
|
|
|
});
|