import React, { useState, useEffect, useCallback, useRef } from "react"; import { Layers, Network, Database, Shield, Server, ChevronDown, ChevronRight, RefreshCw, Plus, Package, } from "lucide-react"; import { useKubernetesStore } from "@/stores/kubernetesStore"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui"; import { PodList, DeploymentList, DaemonSetList, StatefulSetList, ReplicaSetList, JobList, CronJobList, ServiceList, IngressList, ConfigMapList, SecretList, HPAList, PVCList, PVList, ServiceAccountList, RoleList, ClusterRoleList, RoleBindingList, ClusterRoleBindingList, NodeList, EventList, ClusterOverview, PortForwardList, PortForwardForm, CommandPalette, Hotbar, StorageClassList, NetworkPolicyList, ResourceQuotaList, LimitRangeList, } from "@/components/Kubernetes"; import type { KubeconfigInfo, NamespaceInfo, PortForwardResponse, PodInfo, ServiceInfo, DeploymentInfo, StatefulSetInfo, DaemonSetInfo, ReplicaSetInfo, JobInfo, CronJobInfo, ConfigMapInfo, SecretInfo, NodeInfo, EventInfo, IngressInfo, PersistentVolumeClaimInfo, PersistentVolumeInfo, ServiceAccountInfo, RoleInfo, ClusterRoleInfo, RoleBindingInfo, ClusterRoleBindingInfo, HorizontalPodAutoscalerInfo, StorageClassInfo, NetworkPolicyInfo, ResourceQuotaInfo, LimitRangeInfo, } from "@/lib/tauriCommands"; import { listKubeconfigsCmd, activateKubeconfigCmd, connectClusterFromKubeconfigCmd, listNamespacesCmd, listPortForwardsCmd, startPortForwardCmd, stopPortForwardCmd, deletePortForwardCmd, listPodsCmd, listServicesCmd, listDeploymentsCmd, listStatefulsetsCmd, listDaemonsetsCmd, listReplicasetsCmd, listJobsCmd, listCronjobsCmd, listConfigmapsCmd, listSecretsCmd, listNodesCmd, listEventsCmd, listIngressesCmd, listPersistentvolumeclaimsCmd, listPersistentvolumesCmd, listServiceaccountsCmd, listRolesCmd, listClusterrolesCmd, listRolebindingsCmd, listClusterrolebindingsCmd, listHorizontalpodautoscalersCmd, listStorageclassesCmd, listNetworkpoliciesCmd, listResourcequotasCmd, listLimitrangesCmd, } from "@/lib/tauriCommands"; // ─── Types ──────────────────────────────────────────────────────────────────── type ActiveSection = | "overview" | "pods" | "deployments" | "daemonsets" | "statefulsets" | "replicasets" | "jobs" | "cronjobs" | "services" | "ingresses" | "configmaps" | "secrets" | "hpas" | "pvcs" | "pvs" | "serviceaccounts" | "roles" | "clusterroles" | "rolebindings" | "clusterrolebindings" | "nodes" | "events" | "portforwarding" | "storageclasses" | "networkpolicies" | "resourcequotas" | "limitranges"; interface NavItem { id: ActiveSection; label: string; } interface NavSection { label: string; icon: React.ElementType; items: NavItem[]; } // ─── Nav structure ──────────────────────────────────────────────────────────── const NAV_SECTIONS: NavSection[] = [ { label: "Workloads", icon: Layers, items: [ { id: "pods", label: "Pods" }, { id: "deployments", label: "Deployments" }, { id: "daemonsets", label: "Daemon Sets" }, { id: "statefulsets", label: "Stateful Sets" }, { id: "replicasets", label: "Replica Sets" }, { id: "jobs", label: "Jobs" }, { id: "cronjobs", label: "Cron Jobs" }, ], }, { label: "Services & Networking", icon: Network, items: [ { id: "services", label: "Services" }, { id: "ingresses", label: "Ingresses" }, { id: "networkpolicies", label: "Network Policies" }, ], }, { label: "Config & Storage", icon: Database, items: [ { id: "configmaps", label: "Config Maps" }, { id: "secrets", label: "Secrets" }, { id: "hpas", label: "Horizontal Pod Autoscalers" }, { id: "pvcs", label: "Persistent Volume Claims" }, { id: "pvs", label: "Persistent Volumes" }, { id: "storageclasses", label: "Storage Classes" }, { id: "resourcequotas", label: "Resource Quotas" }, { id: "limitranges", label: "Limit Ranges" }, ], }, { label: "Access Control", icon: Shield, items: [ { id: "serviceaccounts", label: "Service Accounts" }, { id: "roles", label: "Roles" }, { id: "clusterroles", label: "Cluster Roles" }, { id: "rolebindings", label: "Role Bindings" }, { id: "clusterrolebindings", label: "Cluster Role Bindings" }, ], }, { label: "Cluster", icon: Server, items: [ { id: "overview", label: "Overview" }, { id: "nodes", label: "Nodes" }, { id: "events", label: "Events" }, { id: "portforwarding", label: "Port Forwarding" }, ], }, ]; // ─── Resource data union ────────────────────────────────────────────────────── interface ResourceData { pods: PodInfo[]; services: ServiceInfo[]; deployments: DeploymentInfo[]; statefulsets: StatefulSetInfo[]; daemonsets: DaemonSetInfo[]; replicasets: ReplicaSetInfo[]; jobs: JobInfo[]; cronjobs: CronJobInfo[]; configmaps: ConfigMapInfo[]; secrets: SecretInfo[]; nodes: NodeInfo[]; events: EventInfo[]; ingresses: IngressInfo[]; pvcs: PersistentVolumeClaimInfo[]; pvs: PersistentVolumeInfo[]; serviceaccounts: ServiceAccountInfo[]; roles: RoleInfo[]; clusterroles: ClusterRoleInfo[]; rolebindings: RoleBindingInfo[]; clusterrolebindings: ClusterRoleBindingInfo[]; hpas: HorizontalPodAutoscalerInfo[]; storageclasses: StorageClassInfo[]; networkpolicies: NetworkPolicyInfo[]; resourcequotas: ResourceQuotaInfo[]; limitranges: LimitRangeInfo[]; } const EMPTY_RESOURCES: ResourceData = { pods: [], services: [], deployments: [], statefulsets: [], daemonsets: [], replicasets: [], jobs: [], cronjobs: [], configmaps: [], secrets: [], nodes: [], events: [], ingresses: [], pvcs: [], pvs: [], serviceaccounts: [], roles: [], clusterroles: [], rolebindings: [], clusterrolebindings: [], hpas: [], storageclasses: [], networkpolicies: [], resourcequotas: [], limitranges: [], }; // ─── Component ─────────────────────────────────────────────────────────────── export function KubernetesPage() { const { selectedClusterId, selectedNamespace, setSelectedCluster, setSelectedNamespace } = useKubernetesStore(); const [kubeconfigs, setKubeconfigs] = useState([]); const [namespaces, setNamespaces] = useState([]); const [portForwards, setPortForwards] = useState([]); const [resources, setResources] = useState(EMPTY_RESOURCES); const [activeSection, setActiveSection] = useState("overview"); const [expandedSections, setExpandedSections] = useState>({ Workloads: true, "Services & Networking": true, "Config & Storage": true, "Access Control": true, Cluster: true, }); const [isLoadingResources, setIsLoadingResources] = useState(false); const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false); const [isPortForwardFormOpen, setIsPortForwardFormOpen] = useState(false); const [isNotificationsOpen, setIsNotificationsOpen] = useState(false); // Track the last loaded section to avoid redundant fetches const lastLoadedRef = useRef<{ section: ActiveSection; clusterId: string; namespace: string } | null>(null); // ── Initial data load ────────────────────────────────────────────────────── const loadInitialData = useCallback(async () => { try { const [kubeconfigsData, portForwardsData] = await Promise.all([ listKubeconfigsCmd(), listPortForwardsCmd(), ]); setKubeconfigs(kubeconfigsData); setPortForwards(portForwardsData); const activeConfig = kubeconfigsData.find((c) => c.is_active); if (activeConfig && !selectedClusterId) { await connectClusterFromKubeconfigCmd(activeConfig.id).catch(() => {}); setSelectedCluster(activeConfig.id); } else if (selectedClusterId) { await connectClusterFromKubeconfigCmd(selectedClusterId).catch(() => {}); } } catch (err) { console.error("Failed to load initial Kubernetes data:", err); } }, [selectedClusterId, setSelectedCluster]); useEffect(() => { loadInitialData(); }, [loadInitialData]); // ── Load namespaces when cluster changes ────────────────────────────────── useEffect(() => { if (!selectedClusterId) return; listNamespacesCmd(selectedClusterId) .then(setNamespaces) .catch((err) => console.error("Failed to load namespaces:", err)); }, [selectedClusterId]); // ── Load resource data when section, cluster, or namespace changes ───────── const loadResourceData = useCallback( async (section: ActiveSection, clusterId: string, namespace: string) => { if (section === "overview" || section === "portforwarding") return; const ns = namespace === "all" ? "" : namespace; setIsLoadingResources(true); try { switch (section) { case "pods": setResources((r) => ({ ...r, pods: [] })); setResources((r) => ({ ...r })); await listPodsCmd(clusterId, ns).then((data) => setResources((r) => ({ ...r, pods: data })) ); break; case "deployments": await listDeploymentsCmd(clusterId, ns).then((data) => setResources((r) => ({ ...r, deployments: data })) ); break; case "daemonsets": await listDaemonsetsCmd(clusterId, ns).then((data) => setResources((r) => ({ ...r, daemonsets: data })) ); break; case "statefulsets": await listStatefulsetsCmd(clusterId, ns).then((data) => setResources((r) => ({ ...r, statefulsets: data })) ); break; case "replicasets": await listReplicasetsCmd(clusterId, ns).then((data) => setResources((r) => ({ ...r, replicasets: data })) ); break; case "jobs": await listJobsCmd(clusterId, ns).then((data) => setResources((r) => ({ ...r, jobs: data })) ); break; case "cronjobs": await listCronjobsCmd(clusterId, ns).then((data) => setResources((r) => ({ ...r, cronjobs: data })) ); break; case "services": await listServicesCmd(clusterId, ns).then((data) => setResources((r) => ({ ...r, services: data })) ); break; case "ingresses": await listIngressesCmd(clusterId, ns).then((data) => setResources((r) => ({ ...r, ingresses: data })) ); break; case "configmaps": await listConfigmapsCmd(clusterId, ns).then((data) => setResources((r) => ({ ...r, configmaps: data })) ); break; case "secrets": await listSecretsCmd(clusterId, ns).then((data) => setResources((r) => ({ ...r, secrets: data })) ); break; case "hpas": await listHorizontalpodautoscalersCmd(clusterId, ns).then((data) => setResources((r) => ({ ...r, hpas: data })) ); break; case "pvcs": await listPersistentvolumeclaimsCmd(clusterId, ns).then((data) => setResources((r) => ({ ...r, pvcs: data })) ); break; case "pvs": await listPersistentvolumesCmd(clusterId).then((data) => setResources((r) => ({ ...r, pvs: data })) ); break; case "serviceaccounts": await listServiceaccountsCmd(clusterId, ns).then((data) => setResources((r) => ({ ...r, serviceaccounts: data })) ); break; case "roles": await listRolesCmd(clusterId, ns).then((data) => setResources((r) => ({ ...r, roles: data })) ); break; case "clusterroles": await listClusterrolesCmd(clusterId).then((data) => setResources((r) => ({ ...r, clusterroles: data })) ); break; case "rolebindings": await listRolebindingsCmd(clusterId, ns).then((data) => setResources((r) => ({ ...r, rolebindings: data })) ); break; case "clusterrolebindings": await listClusterrolebindingsCmd(clusterId).then((data) => setResources((r) => ({ ...r, clusterrolebindings: data })) ); break; case "nodes": await listNodesCmd(clusterId).then((data) => setResources((r) => ({ ...r, nodes: data })) ); break; case "events": await listEventsCmd(clusterId, ns || undefined).then((data) => setResources((r) => ({ ...r, events: data })) ); break; case "storageclasses": await listStorageclassesCmd(clusterId).then((data) => setResources((r) => ({ ...r, storageclasses: data })) ); break; case "networkpolicies": await listNetworkpoliciesCmd(clusterId, ns).then((data) => setResources((r) => ({ ...r, networkpolicies: data })) ); break; case "resourcequotas": await listResourcequotasCmd(clusterId, ns).then((data) => setResources((r) => ({ ...r, resourcequotas: data })) ); break; case "limitranges": await listLimitrangesCmd(clusterId, ns).then((data) => setResources((r) => ({ ...r, limitranges: data })) ); break; } lastLoadedRef.current = { section, clusterId, namespace }; } catch (err) { console.error(`Failed to load ${section}:`, err); } finally { setIsLoadingResources(false); } }, [] ); useEffect(() => { if (!selectedClusterId) return; loadResourceData(activeSection, selectedClusterId, selectedNamespace); }, [activeSection, selectedClusterId, selectedNamespace, loadResourceData]); // ── Keyboard shortcut for CommandPalette ────────────────────────────────── useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.ctrlKey && e.key === "k") { e.preventDefault(); setIsCommandPaletteOpen((prev) => !prev); } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, []); // ── Handlers ───────────────────────────────────────────────────────────── const handleClusterChange = async (id: string) => { try { await activateKubeconfigCmd(id); await connectClusterFromKubeconfigCmd(id); const updated = await listKubeconfigsCmd(); setKubeconfigs(updated); const active = updated.find((c) => c.is_active); if (active) { setSelectedCluster(active.id); } } catch (err) { console.error("Failed to activate kubeconfig:", err); } }; const handleRefresh = () => { if (!selectedClusterId) return; lastLoadedRef.current = null; if (activeSection === "portforwarding") { listPortForwardsCmd() .then(setPortForwards) .catch((err) => console.error("Failed to refresh port forwards:", err)); return; } loadResourceData(activeSection, selectedClusterId, selectedNamespace); }; 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); } }; const handleDeletePortForward = async (id: string) => { try { await deletePortForwardCmd(id); setPortForwards((prev) => prev.filter((pf) => pf.id !== id)); } catch (err) { console.error("Failed to delete port forward:", err); } }; const handleStartPortForward = async (portForward: Parameters[0]) => { try { const result = await startPortForwardCmd(portForward); setPortForwards((prev) => [...prev, result]); } catch (err) { console.error("Failed to start port forward:", err); } }; const toggleSection = (label: string) => { setExpandedSections((prev) => ({ ...prev, [label]: !prev[label] })); }; const handleNavigate = (section: string) => { setActiveSection(section as ActiveSection); }; // ── Content renderer ────────────────────────────────────────────────────── const renderContent = () => { if (!selectedClusterId) { return (

No cluster selected

Select a cluster from the dropdown above, or upload a kubeconfig file in Settings → Kubeconfig to get started.

); } if (activeSection === "overview") { return ( ); } if (activeSection === "portforwarding") { return (
setIsPortForwardFormOpen(true)} onStop={handleStopPortForward} onDelete={handleDeletePortForward} /> setIsPortForwardFormOpen(false)} onStart={(pf) => { setPortForwards((prev) => [...prev, pf]); setIsPortForwardFormOpen(false); }} />
); } if (isLoadingResources) { return (

Loading resources...

); } const ns = selectedNamespace; const cid = selectedClusterId; switch (activeSection) { case "pods": return ; case "deployments": return ; case "daemonsets": return ; case "statefulsets": return ; case "replicasets": return ; case "jobs": return ; case "cronjobs": return ; case "services": return ; case "ingresses": return ; case "configmaps": return ; case "secrets": return ; case "hpas": return ; case "pvcs": return ; case "pvs": return ; case "serviceaccounts": return ; case "roles": return ; case "clusterroles": return ; case "rolebindings": return ; case "clusterrolebindings": return ; case "nodes": return ; case "events": return ; case "storageclasses": return ; case "networkpolicies": return ; case "resourcequotas": return ; case "limitranges": return ; default: return null; } }; // ── Render ──────────────────────────────────────────────────────────────── const selectedConfig = kubeconfigs.find((c) => c.id === selectedClusterId); return (
{/* Hotbar */} setIsCommandPaletteOpen(true)} onSettings={() => {}} onNotifications={() => setIsNotificationsOpen(true)} clusterName={selectedConfig?.name} /> {/* Top bar: cluster selector + namespace selector */}
{selectedClusterId && ( <>
Namespace:
)} {selectedConfig && (
Context: {selectedConfig.context} {selectedConfig.cluster_url && ( <> | {selectedConfig.cluster_url} )}
)}
{/* Main layout: sidebar + content */}
{/* Sidebar */} {/* Main content */}
{renderContent()}
{/* Command Palette */} setIsCommandPaletteOpen(false)} onNavigate={handleNavigate} /> {/* Notifications panel */} Notifications
{selectedConfig ? (

Active cluster

{selectedConfig.context}

{selectedConfig.cluster_url && (

{selectedConfig.cluster_url}

)}
) : (

No cluster connected.

)}

Navigate to Cluster → Events to view live cluster events.

{/* Port Forward Form (only rendered outside portforwarding section via global trigger) */} {activeSection !== "portforwarding" && ( setIsPortForwardFormOpen(false)} onStart={(pf) => { void handleStartPortForward({ cluster_id: pf.cluster_id, namespace: pf.namespace, pod: pf.pod, container_port: pf.container_ports[0] ?? 80, }); setIsPortForwardFormOpen(false); }} /> )}
); }