diff --git a/src/components/Kubernetes/AddClusterModal.tsx b/src/components/Kubernetes/AddClusterModal.tsx
new file mode 100644
index 00000000..f09c98de
--- /dev/null
+++ b/src/components/Kubernetes/AddClusterModal.tsx
@@ -0,0 +1,130 @@
+import React, { useState } from "react";
+import { X, Loader2 } from "lucide-react";
+import { Button } from "@/components/ui";
+import type { ClusterInfo } from "@/lib/tauriCommands";
+import { addClusterCmd } from "@/lib/tauriCommands";
+
+interface AddClusterModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onAdd: (cluster: ClusterInfo) => void;
+}
+
+export function AddClusterModal({ isOpen, onClose, onAdd }: AddClusterModalProps) {
+ const [id, setId] = useState("");
+ const [name, setName] = useState("");
+ const [kubeconfig, setKubeconfig] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState("");
+
+ if (!isOpen) return null;
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError("");
+
+ if (!id.trim() || !name.trim() || !kubeconfig.trim()) {
+ setError("All fields are required");
+ return;
+ }
+
+ setIsLoading(true);
+
+ try {
+ const cluster = await addClusterCmd(id, name, kubeconfig);
+ onAdd(cluster);
+ onClose();
+ setId("");
+ setName("");
+ setKubeconfig("");
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to add cluster");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
Add Kubernetes Cluster
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/Kubernetes/ClusterList.tsx b/src/components/Kubernetes/ClusterList.tsx
new file mode 100644
index 00000000..bfdf93b1
--- /dev/null
+++ b/src/components/Kubernetes/ClusterList.tsx
@@ -0,0 +1,79 @@
+import React from "react";
+import { Trash2, Plus, Server, Activity } from "lucide-react";
+import { Button } from "@/components/ui";
+import type { ClusterInfo } from "@/lib/tauriCommands";
+import { removeClusterCmd } from "@/lib/tauriCommands";
+
+interface ClusterListProps {
+ clusters: ClusterInfo[];
+ onAdd: () => void;
+ onRemove: (clusterId: string) => Promise;
+}
+
+export function ClusterList({ clusters, onAdd, onRemove }: ClusterListProps) {
+ const handleRemove = async (clusterId: string) => {
+ if (window.confirm("Are you sure you want to remove this cluster?")) {
+ await onRemove(clusterId);
+ }
+ };
+
+ return (
+
+
+
+
+
Clusters
+
+
+
+
+ {clusters.length === 0 ? (
+
+
+
No clusters configured
+
+ Add a Kubernetes cluster to start managing it
+
+
+
+ ) : (
+
+ {clusters.map((cluster) => (
+
+
+
+
{cluster.name}
+
+ ID: {cluster.id}
+
+
+ Context: {cluster.context}
+
+
+ URL: {cluster.cluster_url}
+
+
+
+
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/components/Kubernetes/PortForwardForm.tsx b/src/components/Kubernetes/PortForwardForm.tsx
new file mode 100644
index 00000000..552810d7
--- /dev/null
+++ b/src/components/Kubernetes/PortForwardForm.tsx
@@ -0,0 +1,180 @@
+import React, { useState } from "react";
+import { X, Loader2 } from "lucide-react";
+import { Button } from "@/components/ui";
+import type { PortForwardResponse } from "@/lib/tauriCommands";
+import { startPortForwardCmd } from "@/lib/tauriCommands";
+import { listClustersCmd } from "@/lib/tauriCommands";
+
+interface PortForwardFormProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onStart: (portForward: PortForwardResponse) => void;
+}
+
+export function PortForwardForm({ isOpen, onClose, onStart }: PortForwardFormProps) {
+ const [clusterId, setClusterId] = useState("");
+ const [namespace, setNamespace] = useState("default");
+ const [pod, setPod] = useState("");
+ const [containerPort, setContainerPort] = useState("80");
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState("");
+ const [clusters, setClusters] = useState<{ id: string; name: string }[]>([]);
+
+ if (!isOpen) return null;
+
+ React.useEffect(() => {
+ if (isOpen) {
+ loadClusters();
+ }
+ }, [isOpen]);
+
+ const loadClusters = async () => {
+ try {
+ const clusters = await listClustersCmd();
+ setClusters(clusters.map((c) => ({ id: c.id, name: c.name })));
+ } catch (err) {
+ console.error("Failed to load clusters:", err);
+ }
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError("");
+
+ if (!clusterId || !pod || !containerPort) {
+ setError("All fields are required");
+ return;
+ }
+
+ const port = parseInt(containerPort, 10);
+ if (isNaN(port) || port < 1 || port > 65535) {
+ setError("Container port must be a valid port number (1-65535)");
+ return;
+ }
+
+ setIsLoading(true);
+
+ try {
+ const portForward = await startPortForwardCmd({
+ cluster_id: clusterId,
+ namespace,
+ pod,
+ container_port: port,
+ });
+ onStart(portForward);
+ onClose();
+ setClusterId("");
+ setNamespace("default");
+ setPod("");
+ setContainerPort("80");
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to start port forward");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
Start Port Forward
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/Kubernetes/PortForwardList.tsx b/src/components/Kubernetes/PortForwardList.tsx
new file mode 100644
index 00000000..a66c66a7
--- /dev/null
+++ b/src/components/Kubernetes/PortForwardList.tsx
@@ -0,0 +1,117 @@
+import React from "react";
+import { Trash2, Plus, Activity } from "lucide-react";
+import { Button } from "@/components/ui";
+import type { PortForwardResponse } from "@/lib/tauriCommands";
+import { stopPortForwardCmd } from "@/lib/tauriCommands";
+
+interface PortForwardListProps {
+ portForwards: PortForwardResponse[];
+ onStart: () => void;
+ onStop: (session_id: string) => Promise;
+}
+
+export function PortForwardList({ portForwards, onStart, onStop }: PortForwardListProps) {
+ const handleStop = async (id: string) => {
+ if (window.confirm("Are you sure you want to stop this port forward?")) {
+ await onStop(id);
+ }
+ };
+
+ const getStatusColor = (status: string) => {
+ switch (status.toLowerCase()) {
+ case "active":
+ return "bg-green-500/15 text-green-600 dark:text-green-400 border-green-500/20";
+ case "stopped":
+ return "bg-gray-500/15 text-gray-600 dark:text-gray-400 border-gray-500/20";
+ case "error":
+ return "bg-red-500/15 text-red-600 dark:text-red-400 border-red-500/20";
+ default:
+ return "bg-muted text-muted-foreground";
+ }
+ };
+
+ return (
+
+
+
+
+
+
+ {portForwards.length === 0 ? (
+
+
+
No active port forwards
+
+ Start a port forward to expose a pod locally
+
+
+
+ ) : (
+
+ {portForwards.map((pf) => (
+
+
+
+
+
Port Forward
+
+ {pf.status}
+
+
+
+ Cluster: {pf.cluster_id}
+
+
+ Namespace: {pf.namespace}
+
+
+ Pod: {pf.pod}
+
+
+ Container Port: {pf.container_port}
+ |
+ Local Port: {pf.local_port > 0 ? pf.local_port : "pending"}
+
+
+
+ {pf.status.toLowerCase() === "active" && (
+
+ )}
+
+
+
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/components/Kubernetes/index.tsx b/src/components/Kubernetes/index.tsx
new file mode 100644
index 00000000..1ad0acfc
--- /dev/null
+++ b/src/components/Kubernetes/index.tsx
@@ -0,0 +1,4 @@
+export { ClusterList } from "./ClusterList";
+export { PortForwardList } from "./PortForwardList";
+export { AddClusterModal } from "./AddClusterModal";
+export { PortForwardForm } from "./PortForwardForm";
diff --git a/src/lib/tauriCommands.ts b/src/lib/tauriCommands.ts
index c9836deb..4426a614 100644
--- a/src/lib/tauriCommands.ts
+++ b/src/lib/tauriCommands.ts
@@ -738,3 +738,49 @@ export const listCommandExecutionsCmd = (issueId?: string) =>
export const checkKubectlInstalledCmd = () =>
invoke("check_kubectl_installed");
+
+// ─── Kubernetes Management Types ──────────────────────────────────────────────
+
+export interface ClusterInfo {
+ id: string;
+ name: string;
+ context: string;
+ cluster_url: string;
+}
+
+export interface PortForwardRequest {
+ cluster_id: string;
+ namespace: string;
+ pod: string;
+ container_port: number;
+}
+
+export interface PortForwardResponse {
+ id: string;
+ cluster_id: string;
+ namespace: string;
+ pod: string;
+ container_port: number;
+ local_port: number;
+ status: string;
+}
+
+// ─── Kubernetes Management Commands ───────────────────────────────────────────
+
+export const addClusterCmd = (id: string, name: string, kubeconfigContent: string) =>
+ invoke("add_cluster", { id, name, kubeconfig_content: kubeconfigContent });
+
+export const removeClusterCmd = (id: string) =>
+ invoke("remove_cluster", { id });
+
+export const listClustersCmd = () =>
+ invoke("list_clusters");
+
+export const startPortForwardCmd = (request: PortForwardRequest) =>
+ invoke("start_port_forward", { request });
+
+export const stopPortForwardCmd = (id: string) =>
+ invoke("stop_port_forward", { id });
+
+export const listPortForwardsCmd = () =>
+ invoke("list_port_forwards");
diff --git a/src/pages/Kubernetes/KubernetesPage.tsx b/src/pages/Kubernetes/KubernetesPage.tsx
new file mode 100644
index 00000000..89223a9b
--- /dev/null
+++ b/src/pages/Kubernetes/KubernetesPage.tsx
@@ -0,0 +1,117 @@
+import React, { useState, useEffect } from "react";
+import { Server, Activity } from "lucide-react";
+import { ClusterList } from "@/components/Kubernetes/ClusterList";
+import { PortForwardList } from "@/components/Kubernetes/PortForwardList";
+import { AddClusterModal } from "@/components/Kubernetes/AddClusterModal";
+import { PortForwardForm } from "@/components/Kubernetes/PortForwardForm";
+import type { ClusterInfo, PortForwardResponse } from "@/lib/tauriCommands";
+import {
+ listClustersCmd,
+ removeClusterCmd,
+ listPortForwardsCmd,
+ stopPortForwardCmd,
+} from "@/lib/tauriCommands";
+
+export function KubernetesPage() {
+ const [clusters, setClusters] = useState([]);
+ const [portForwards, setPortForwards] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isAddClusterOpen, setIsAddClusterOpen] = useState(false);
+ const [isStartPortForwardOpen, setIsStartPortForwardOpen] = useState(false);
+
+ useEffect(() => {
+ loadData();
+ }, []);
+
+ const loadData = async () => {
+ setIsLoading(true);
+ try {
+ const [clustersData, portForwardsData] = await Promise.all([
+ listClustersCmd(),
+ listPortForwardsCmd(),
+ ]);
+ setClusters(clustersData);
+ setPortForwards(portForwardsData);
+ } catch (err) {
+ console.error("Failed to load data:", err);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleRemoveCluster = async (clusterId: string) => {
+ try {
+ await removeClusterCmd(clusterId);
+ setClusters((prev) => prev.filter((c) => c.id !== clusterId));
+ } catch (err) {
+ console.error("Failed to remove cluster:", err);
+ alert("Failed to remove cluster");
+ }
+ };
+
+ const handleStopPortForward = async (id: string) => {
+ try {
+ await stopPortForwardCmd(id);
+ setPortForwards((prev) => prev.filter((pf) => pf.id !== id));
+ } catch (err) {
+ console.error("Failed to stop port forward:", err);
+ alert("Failed to stop port forward");
+ }
+ };
+
+ const handleAddCluster = (cluster: ClusterInfo) => {
+ setClusters((prev) => [...prev, cluster]);
+ };
+
+ const handleStartPortForward = (portForward: PortForwardResponse) => {
+ setPortForwards((prev) => [...prev, portForward]);
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+
Loading Kubernetes resources...
+
+
+ );
+ }
+
+ return (
+
+
+
Kubernetes Management
+
+ Manage your Kubernetes clusters and port forwarding sessions
+
+
+
+
+
setIsAddClusterOpen(true)}
+ onRemove={handleRemoveCluster}
+ />
+
+ setIsStartPortForwardOpen(true)}
+ onStop={handleStopPortForward}
+ />
+
+
+
setIsAddClusterOpen(false)}
+ onAdd={handleAddCluster}
+ />
+
+ setIsStartPortForwardOpen(false)}
+ onStart={handleStartPortForward}
+ />
+
+ );
+}
diff --git a/tests/unit/kubernetesCommands.test.ts b/tests/unit/kubernetesCommands.test.ts
new file mode 100644
index 00000000..b606a05f
--- /dev/null
+++ b/tests/unit/kubernetesCommands.test.ts
@@ -0,0 +1,141 @@
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { invoke } from "@tauri-apps/api/core";
+import * as tauriCommands from "@/lib/tauriCommands";
+
+// Mock Tauri invoke
+vi.mock("@tauri-apps/api/core");
+
+type MockedFunction any> = T & {
+ mockResolvedValue: (value: any) => void;
+ mockRejectedValue: (error: Error) => void;
+};
+
+describe("Kubernetes Management Commands", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe("addClusterCmd", () => {
+ it("should call invoke with correct parameters", async () => {
+ (invoke as MockedFunction).mockResolvedValue({
+ id: "cluster-1",
+ name: "production",
+ context: "prod-context",
+ cluster_url: "https://prod.example.com",
+ });
+
+ const result = await tauriCommands.addClusterCmd(
+ "cluster-1",
+ "production",
+ "kubeconfig-content"
+ );
+
+ expect(invoke).toHaveBeenCalledWith("add_cluster", {
+ id: "cluster-1",
+ name: "production",
+ kubeconfig_content: "kubeconfig-content",
+ });
+ expect(result).toEqual({
+ id: "cluster-1",
+ name: "production",
+ context: "prod-context",
+ cluster_url: "https://prod.example.com",
+ });
+ });
+ });
+
+ describe("removeClusterCmd", () => {
+ it("should call invoke with cluster id", async () => {
+ (invoke as MockedFunction).mockResolvedValue(undefined);
+
+ await tauriCommands.removeClusterCmd("cluster-1");
+
+ expect(invoke).toHaveBeenCalledWith("remove_cluster", { id: "cluster-1" });
+ });
+ });
+
+ describe("listClustersCmd", () => {
+ it("should call invoke and return cluster list", async () => {
+ (invoke as MockedFunction).mockResolvedValue([
+ {
+ id: "cluster-1",
+ name: "production",
+ context: "prod-context",
+ cluster_url: "https://prod.example.com",
+ },
+ ]);
+
+ const result = await tauriCommands.listClustersCmd();
+
+ expect(invoke).toHaveBeenCalledWith("list_clusters");
+ expect(result).toHaveLength(1);
+ expect(result[0].name).toBe("production");
+ });
+ });
+
+ describe("startPortForwardCmd", () => {
+ it("should call invoke with port forward request", async () => {
+ (invoke as MockedFunction).mockResolvedValue({
+ id: "pf-1",
+ cluster_id: "cluster-1",
+ namespace: "default",
+ pod: "nginx-abc123",
+ container_port: 80,
+ local_port: 8080,
+ status: "Active",
+ });
+
+ const request = {
+ cluster_id: "cluster-1",
+ namespace: "default",
+ pod: "nginx-abc123",
+ container_port: 80,
+ };
+
+ const result = await tauriCommands.startPortForwardCmd(request);
+
+ expect(invoke).toHaveBeenCalledWith("start_port_forward", { request });
+ expect(result).toEqual({
+ id: "pf-1",
+ cluster_id: "cluster-1",
+ namespace: "default",
+ pod: "nginx-abc123",
+ container_port: 80,
+ local_port: 8080,
+ status: "Active",
+ });
+ });
+ });
+
+ describe("stopPortForwardCmd", () => {
+ it("should call invoke with session id", async () => {
+ (invoke as MockedFunction).mockResolvedValue(undefined);
+
+ await tauriCommands.stopPortForwardCmd("pf-1");
+
+ expect(invoke).toHaveBeenCalledWith("stop_port_forward", { id: "pf-1" });
+ });
+ });
+
+ describe("listPortForwardsCmd", () => {
+ it("should call invoke and return port forwards list", async () => {
+ (invoke as MockedFunction).mockResolvedValue([
+ {
+ id: "pf-1",
+ cluster_id: "cluster-1",
+ namespace: "default",
+ pod: "nginx-abc123",
+ container_port: 80,
+ local_port: 8080,
+ status: "Active",
+ },
+ ]);
+
+ const result = await tauriCommands.listPortForwardsCmd();
+
+ expect(invoke).toHaveBeenCalledWith("list_port_forwards");
+ expect(result).toHaveLength(1);
+ expect(result[0].pod).toBe("nginx-abc123");
+ });
+ });
+});