+
+
+
+
Terminal
+
+
+
+ New Terminal
+
+
+
+ {sessions.length === 0 ? (
+
+
+
+
No terminals open
+
+
+ Open Terminal
+
+
+
+ ) : (
+
+
+
+ {sessions.map((session) => (
+
+
+ {session.pod || "new"} / {session.container || "bash"}
+
+ {
+ e.stopPropagation();
+ removeSession(session.id);
+ }}
+ className="hover:text-destructive"
+ >
+
+
+
+ ))}
+
+
+ {sessions.map((session) => (
+
+ initTerminal(session.id, el)}
+ className="w-full h-full bg-slate-900 rounded-md overflow-hidden"
+ />
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/src/components/Kubernetes/YamlEditor.tsx b/src/components/Kubernetes/YamlEditor.tsx
new file mode 100644
index 00000000..4cbd168e
--- /dev/null
+++ b/src/components/Kubernetes/YamlEditor.tsx
@@ -0,0 +1,35 @@
+import React from "react";
+import { Button } from "@/components/ui";
+import { Badge } from "@/components/ui";
+
+interface YamlEditorProps {
+ onChange: (value: string) => void;
+}
+
+export function YamlEditor({ onChange }: YamlEditorProps) {
+ return (
+
+
+
+
YAML Editor
+ Ready
+
+
+ onChange("")}>
+ Clear
+
+
+ Apply
+
+
+
+
+
+
+
YAML Editor would be displayed here
+
Requires @monaco-editor/react dependency
+
+
+
+ );
+}
diff --git a/src/components/Kubernetes/index.tsx b/src/components/Kubernetes/index.tsx
index 34159051..3def9745 100644
--- a/src/components/Kubernetes/index.tsx
+++ b/src/components/Kubernetes/index.tsx
@@ -8,3 +8,26 @@ export { ServiceList } from "./ServiceList";
export { DeploymentList } from "./DeploymentList";
export { StatefulSetList } from "./StatefulSetList";
export { DaemonSetList } from "./DaemonSetList";
+export { NodeList } from "./NodeList";
+export { EventList } from "./EventList";
+export { ConfigMapList } from "./ConfigMapList";
+export { SecretList } from "./SecretList";
+export { ReplicaSetList } from "./ReplicaSetList";
+export { JobList } from "./JobList";
+export { CronJobList } from "./CronJobList";
+export { IngressList } from "./IngressList";
+export { PVCList } from "./PVCList";
+export { PVList } from "./PVList";
+export { ServiceAccountList } from "./ServiceAccountList";
+export { RoleList } from "./RoleList";
+export { ClusterRoleList } from "./ClusterRoleList";
+export { RoleBindingList } from "./RoleBindingList";
+export { ClusterRoleBindingList } from "./ClusterRoleBindingList";
+export { HPAList } from "./HPAList";
+export { Terminal } from "./Terminal";
+export { YamlEditor } from "./YamlEditor";
+export { MetricsChart } from "./MetricsChart";
+export { SearchBar } from "./SearchBar";
+export { ContextSwitcher } from "./ContextSwitcher";
+export { ApplicationView } from "./ApplicationView";
+export { PodDetail } from "./PodDetail";
diff --git a/src/lib/tauriCommands.ts b/src/lib/tauriCommands.ts
index 1bbb53c4..054c9523 100644
--- a/src/lib/tauriCommands.ts
+++ b/src/lib/tauriCommands.ts
@@ -748,6 +748,18 @@ export interface ClusterInfo {
cluster_url: string;
}
+export interface ContextInfo {
+ name: string;
+ cluster: string;
+ user: string;
+}
+
+export interface ResourceInfo {
+ name: string;
+ namespace: string;
+ [key: string]: unknown;
+}
+
export interface PortForwardRequest {
cluster_id: string;
namespace: string;
diff --git a/src/pages/Kubernetes/KubernetesPage.tsx b/src/pages/Kubernetes/KubernetesPage.tsx
index 7bb2b5b6..7cfcac60 100644
--- a/src/pages/Kubernetes/KubernetesPage.tsx
+++ b/src/pages/Kubernetes/KubernetesPage.tsx
@@ -1,8 +1,10 @@
import React, { useState, useEffect } from "react";
+import { useKubernetesStore } from "@/stores/kubernetesStore";
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 { ResourceBrowser } from "@/components/Kubernetes/ResourceBrowser";
import type { ClusterInfo, PortForwardResponse } from "@/lib/tauriCommands";
import {
listClustersCmd,
@@ -13,7 +15,7 @@ import {
} from "@/lib/tauriCommands";
export function KubernetesPage() {
- const [clusters, setClusters] = useState
([]);
+ const { clusters, addCluster, removeCluster, selectedClusterId } = useKubernetesStore();
const [portForwards, setPortForwards] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [isAddClusterOpen, setIsAddClusterOpen] = useState(false);
@@ -30,7 +32,8 @@ export function KubernetesPage() {
listClustersCmd(),
listPortForwardsCmd(),
]);
- setClusters(clustersData);
+
+ clustersData.forEach(addCluster);
setPortForwards(portForwardsData);
} catch (err) {
console.error("Failed to load data:", err);
@@ -42,7 +45,7 @@ export function KubernetesPage() {
const handleRemoveCluster = async (clusterId: string) => {
try {
await removeClusterCmd(clusterId);
- setClusters((prev) => prev.filter((c) => c.id !== clusterId));
+ removeCluster(clusterId);
} catch (err) {
console.error("Failed to remove cluster:", err);
alert("Failed to remove cluster");
@@ -70,7 +73,7 @@ export function KubernetesPage() {
};
const handleAddCluster = (cluster: ClusterInfo) => {
- setClusters((prev) => [...prev, cluster]);
+ addCluster(cluster);
};
const handleStartPortForward = (portForward: PortForwardResponse) => {
@@ -93,17 +96,41 @@ export function KubernetesPage() {
Kubernetes Management
- Manage your Kubernetes clusters and port forwarding sessions
+ Manage your Kubernetes clusters and resources
-
+ {/* Cluster Management Section */}
+
+
+
Clusters
+ setIsAddClusterOpen(true)}
+ className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
+ >
+ Add Cluster
+
+
+
setIsAddClusterOpen(true)}
onRemove={handleRemoveCluster}
/>
+
+ {/* Port Forwarding Section */}
+
+
+
Port Forwarding
+ setIsStartPortForwardOpen(true)}
+ className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
+ >
+ Start Port Forward
+
+
+
setIsStartPortForwardOpen(true)}
@@ -112,12 +139,22 @@ export function KubernetesPage() {
/>
+ {/* Resource Browser Section */}
+ {selectedClusterId && (
+
+
Resource Browser
+
+
+ )}
+
+ {/* Add Cluster Modal */}
setIsAddClusterOpen(false)}
onAdd={handleAddCluster}
/>
+ {/* Port Forward Form */}
setIsStartPortForwardOpen(false)}
diff --git a/src/stores/kubernetesStore.ts b/src/stores/kubernetesStore.ts
new file mode 100644
index 00000000..90e823d0
--- /dev/null
+++ b/src/stores/kubernetesStore.ts
@@ -0,0 +1,185 @@
+import { create } from "zustand";
+import type { ClusterInfo, ContextInfo, ResourceInfo } from "@/lib/tauriCommands";
+
+export type ResourceType =
+ | "pods"
+ | "services"
+ | "deployments"
+ | "statefulsets"
+ | "daemonsets"
+ | "replicasets"
+ | "jobs"
+ | "cronjobs"
+ | "ingresses"
+ | "persistentvolumes"
+ | "persistentvolumeclaims"
+ | "configmaps"
+ | "secrets"
+ | "serviceaccounts"
+ | "roles"
+ | "clusterroles"
+ | "rolebindings"
+ | "clusterrolebindings"
+ | "nodes"
+ | "events"
+ | "hpas";
+
+interface KubernetesState {
+ // Selection state
+ selectedClusterId: string | null;
+ selectedNamespace: string;
+
+ // Data state
+ clusters: ClusterInfo[];
+ contexts: ContextInfo[];
+ namespaces: Record; // clusterId -> [namespaces]
+
+ // Loaded resources tracking
+ loadedResources: Set;
+
+ // Terminal sessions
+ terminalSessions: Record;
+ nextTerminalId: number;
+
+ // Search state
+ globalSearchQuery: string;
+ searchResults: Record;
+
+ // Bulk selection
+ bulkSelection: Record; // resourceType -> [resourceNames]
+
+ // Actions
+ setSelectedCluster: (clusterId: string) => void;
+ setSelectedNamespace: (namespace: string) => void;
+ addCluster: (cluster: ClusterInfo) => void;
+ removeCluster: (clusterId: string) => void;
+ updateCluster: (clusterId: string, updates: Partial) => void;
+ addContext: (context: ContextInfo) => void;
+ setNamespaces: (clusterId: string, namespaces: string[]) => void;
+ markResourceLoaded: (type: ResourceType) => void;
+ markResourceUnloaded: (type: ResourceType) => void;
+ isResourceLoaded: (type: ResourceType) => boolean;
+ addTerminalSession: (session: { clusterId: string; namespace: string; pod: string; container: string; command: string }) => string;
+ removeTerminalSession: (sessionId: string) => void;
+ setGlobalSearchQuery: (query: string) => void;
+ setSearchResults: (type: ResourceType, results: ResourceInfo[]) => void;
+ addToBulkSelection: (type: ResourceType, resourceName: string) => void;
+ removeFromBulkSelection: (type: ResourceType, resourceName: string) => void;
+ clearBulkSelection: (type: ResourceType) => void;
+ getBulkSelectionCount: (type: ResourceType) => number;
+}
+
+export const useKubernetesStore = create()((set, get) => ({
+ // Selection state
+ selectedClusterId: null,
+ selectedNamespace: "all",
+
+ // Data state
+ clusters: [],
+ contexts: [],
+ namespaces: {},
+
+ // Loaded resources tracking
+ loadedResources: new Set() as Set,
+
+ // Terminal sessions
+ terminalSessions: {},
+ nextTerminalId: 1,
+
+ // Search state
+ globalSearchQuery: "",
+ searchResults: {} as Record,
+
+ // Bulk selection
+ bulkSelection: {} as Record,
+
+ // Actions
+ setSelectedCluster: (clusterId) => set({ selectedClusterId: clusterId, selectedNamespace: "all" }),
+
+ setSelectedNamespace: (namespace) => set({ selectedNamespace: namespace }),
+
+ addCluster: (cluster) => set((state) => ({
+ clusters: [...state.clusters, cluster],
+ })),
+
+ removeCluster: (clusterId) => set((state) => ({
+ clusters: state.clusters.filter((c) => c.id !== clusterId),
+ selectedClusterId: state.selectedClusterId === clusterId ? null : state.selectedClusterId,
+ })),
+
+ updateCluster: (clusterId, updates) => set((state) => ({
+ clusters: state.clusters.map((c) =>
+ c.id === clusterId ? { ...c, ...updates } : c
+ ),
+ })),
+
+ addContext: (context) => set((state) => ({
+ contexts: [...state.contexts, context],
+ })),
+
+ setNamespaces: (clusterId, namespaces) => set((state) => ({
+ namespaces: { ...state.namespaces, [clusterId]: namespaces },
+ })),
+
+ markResourceLoaded: (type) => set((state) => {
+ const newSet = new Set(state.loadedResources);
+ newSet.add(type);
+ return { loadedResources: newSet };
+ }),
+
+ markResourceUnloaded: (type) => set((state) => {
+ const newSet = new Set(state.loadedResources);
+ newSet.delete(type);
+ return { loadedResources: newSet };
+ }),
+
+ isResourceLoaded: (type) => get().loadedResources.has(type),
+
+ addTerminalSession: (session) => {
+ const sessionId = `terminal-${get().nextTerminalId}`;
+ set((state) => ({
+ terminalSessions: { ...state.terminalSessions, [sessionId]: { id: sessionId, ...session } },
+ nextTerminalId: state.nextTerminalId + 1,
+ }));
+ return sessionId;
+ },
+
+ removeTerminalSession: (sessionId) => set((state) => ({
+ terminalSessions: Object.fromEntries(
+ Object.entries(state.terminalSessions).filter(([id]) => id !== sessionId)
+ ),
+ })),
+
+ setGlobalSearchQuery: (query) => set({ globalSearchQuery: query }),
+
+ setSearchResults: (type, results) => set((state) => ({
+ searchResults: { ...state.searchResults, [type]: results },
+ })),
+
+ addToBulkSelection: (type, resourceName) => set((state) => ({
+ bulkSelection: {
+ ...state.bulkSelection,
+ [type]: [...(state.bulkSelection[type] || []), resourceName],
+ },
+ })),
+
+ removeFromBulkSelection: (type, resourceName) => set((state) => ({
+ bulkSelection: {
+ ...state.bulkSelection,
+ [type]: (state.bulkSelection[type] || []).filter((name) => name !== resourceName),
+ },
+ })),
+
+ clearBulkSelection: (type) => set((state) => ({
+ bulkSelection: { ...state.bulkSelection, [type]: [] },
+ })),
+
+ getBulkSelectionCount: (type) => (get().bulkSelection[type] || []).length,
+}));
diff --git a/tests/unit/kubernetesStore.test.ts b/tests/unit/kubernetesStore.test.ts
new file mode 100644
index 00000000..1a125206
--- /dev/null
+++ b/tests/unit/kubernetesStore.test.ts
@@ -0,0 +1,161 @@
+import { describe, it, expect, beforeEach } from "vitest";
+import { useKubernetesStore } from "@/stores/kubernetesStore";
+import type { ResourceInfo } from "@/lib/tauriCommands";
+
+describe("Kubernetes Store", () => {
+ beforeEach(() => {
+ useKubernetesStore.getState().clusters.forEach((c) =>
+ useKubernetesStore.getState().removeCluster(c.id)
+ );
+ });
+
+ describe("Cluster Management", () => {
+ it("should add a cluster", () => {
+ const cluster = {
+ id: "cluster-1",
+ name: "Production",
+ context: "prod-context",
+ cluster_url: "https://k8s.example.com",
+ };
+
+ useKubernetesStore.getState().addCluster(cluster);
+
+ expect(useKubernetesStore.getState().clusters).toHaveLength(1);
+ expect(useKubernetesStore.getState().clusters[0].name).toBe("Production");
+ });
+
+ it("should remove a cluster", () => {
+ const cluster = {
+ id: "cluster-1",
+ name: "Production",
+ context: "prod-context",
+ cluster_url: "https://k8s.example.com",
+ };
+
+ useKubernetesStore.getState().addCluster(cluster);
+ useKubernetesStore.getState().removeCluster("cluster-1");
+
+ expect(useKubernetesStore.getState().clusters).toHaveLength(0);
+ });
+
+ it("should update a cluster", () => {
+ const cluster = {
+ id: "cluster-1",
+ name: "Production",
+ context: "prod-context",
+ cluster_url: "https://k8s.example.com",
+ };
+
+ useKubernetesStore.getState().addCluster(cluster);
+ useKubernetesStore.getState().updateCluster("cluster-1", { name: "Production New" });
+
+ expect(useKubernetesStore.getState().clusters[0].name).toBe("Production New");
+ });
+
+ it("should set selected cluster", () => {
+ const cluster = {
+ id: "cluster-1",
+ name: "Production",
+ context: "prod-context",
+ cluster_url: "https://k8s.example.com",
+ };
+
+ useKubernetesStore.getState().addCluster(cluster);
+ useKubernetesStore.getState().setSelectedCluster("cluster-1");
+
+ expect(useKubernetesStore.getState().selectedClusterId).toBe("cluster-1");
+ });
+ });
+
+ describe("Namespace Management", () => {
+ it("should set selected namespace", () => {
+ useKubernetesStore.getState().setSelectedNamespace("default");
+ expect(useKubernetesStore.getState().selectedNamespace).toBe("default");
+ });
+
+ it("should set namespaces for a cluster", () => {
+ useKubernetesStore.getState().setNamespaces("cluster-1", ["default", "kube-system", "production"]);
+ expect(useKubernetesStore.getState().namespaces["cluster-1"]).toEqual(["default", "kube-system", "production"]);
+ });
+ });
+
+ describe("Resource Loading", () => {
+ it("should mark resource as loaded", () => {
+ useKubernetesStore.getState().markResourceLoaded("pods");
+ expect(useKubernetesStore.getState().isResourceLoaded("pods")).toBe(true);
+ });
+
+ it("should mark resource as unloaded", () => {
+ useKubernetesStore.getState().markResourceLoaded("pods");
+ useKubernetesStore.getState().markResourceUnloaded("pods");
+ expect(useKubernetesStore.getState().isResourceLoaded("pods")).toBe(false);
+ });
+ });
+
+ describe("Terminal Sessions", () => {
+ it("should add a terminal session", () => {
+ const sessionId = useKubernetesStore.getState().addTerminalSession({
+ clusterId: "cluster-1",
+ namespace: "default",
+ pod: "nginx",
+ container: "nginx",
+ command: "bash",
+ });
+
+ expect(sessionId).toBe("terminal-1");
+ expect(useKubernetesStore.getState().terminalSessions[sessionId]).toBeDefined();
+ });
+
+ it("should remove a terminal session", () => {
+ const sessionId = useKubernetesStore.getState().addTerminalSession({
+ clusterId: "cluster-1",
+ namespace: "default",
+ pod: "nginx",
+ container: "nginx",
+ command: "bash",
+ });
+
+ useKubernetesStore.getState().removeTerminalSession(sessionId);
+ expect(useKubernetesStore.getState().terminalSessions[sessionId]).toBeUndefined();
+ });
+ });
+
+ describe("Search", () => {
+ it("should set global search query", () => {
+ useKubernetesStore.getState().setGlobalSearchQuery("nginx");
+ expect(useKubernetesStore.getState().globalSearchQuery).toBe("nginx");
+ });
+
+ it("should set search results", () => {
+ const results = [{ name: "nginx-1", namespace: "default" }];
+ useKubernetesStore.getState().setSearchResults("pods", results as ResourceInfo[]);
+
+ expect(useKubernetesStore.getState().searchResults.pods).toEqual(results);
+ });
+ });
+
+ describe("Bulk Selection", () => {
+ it("should add to bulk selection", () => {
+ useKubernetesStore.getState().addToBulkSelection("pods", "nginx-1");
+ expect(useKubernetesStore.getState().bulkSelection.pods).toContain("nginx-1");
+ });
+
+ it("should remove from bulk selection", () => {
+ useKubernetesStore.getState().addToBulkSelection("pods", "nginx-1");
+ useKubernetesStore.getState().removeFromBulkSelection("pods", "nginx-1");
+ expect(useKubernetesStore.getState().bulkSelection.pods).not.toContain("nginx-1");
+ });
+
+ it("should clear bulk selection", () => {
+ useKubernetesStore.getState().addToBulkSelection("pods", "nginx-1");
+ useKubernetesStore.getState().clearBulkSelection("pods");
+ expect(useKubernetesStore.getState().bulkSelection.pods).toEqual([]);
+ });
+
+ it("should get bulk selection count", () => {
+ useKubernetesStore.getState().addToBulkSelection("pods", "nginx-1");
+ useKubernetesStore.getState().addToBulkSelection("pods", "nginx-2");
+ expect(useKubernetesStore.getState().getBulkSelectionCount("pods")).toBe(2);
+ });
+ });
+});