fix(kubernetes): use kubeconfig files from Settings instead of duplicate cluster management #77

Merged
sarman merged 3 commits from feature/kubernetes-management-v2 into master 2026-06-07 18:28:53 +00:00
2 changed files with 114 additions and 57 deletions

View File

@ -1,25 +1,24 @@
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 type { PortForwardResponse, KubeconfigInfo, PortForwardRequest } from "@/lib/tauriCommands";
import {
listClustersCmd,
removeClusterCmd,
listPortForwardsCmd,
stopPortForwardCmd,
deletePortForwardCmd,
listKubeconfigsCmd,
activateKubeconfigCmd,
startPortForwardCmd,
} from "@/lib/tauriCommands";
export function KubernetesPage() {
const { clusters, addCluster, removeCluster, selectedClusterId } = useKubernetesStore();
const { selectedClusterId, setSelectedCluster } = useKubernetesStore();
const [kubeconfigs, setKubeconfigs] = useState<KubeconfigInfo[]>([]);
const [portForwards, setPortForwards] = useState<PortForwardResponse[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isAddClusterOpen, setIsAddClusterOpen] = useState(false);
const [isStartPortForwardOpen, setIsStartPortForwardOpen] = useState(false);
useEffect(() => {
loadData();
@ -28,12 +27,12 @@ export function KubernetesPage() {
const loadData = async () => {
setIsLoading(true);
try {
const [clustersData, portForwardsData] = await Promise.all([
listClustersCmd(),
const [kubeconfigsData, portForwardsData] = await Promise.all([
listKubeconfigsCmd(),
listPortForwardsCmd(),
]);
clustersData.forEach(addCluster);
setKubeconfigs(kubeconfigsData);
setPortForwards(portForwardsData);
} catch (err) {
console.error("Failed to load data:", err);
@ -42,13 +41,20 @@ export function KubernetesPage() {
}
};
const handleRemoveCluster = async (clusterId: string) => {
const handleActivateKubeconfig = async (id: string) => {
try {
await removeClusterCmd(clusterId);
removeCluster(clusterId);
await activateKubeconfigCmd(id);
const [kubeconfigsData] = await Promise.all([listKubeconfigsCmd()]);
setKubeconfigs(kubeconfigsData);
// Select the active cluster from the activated kubeconfig
const activeConfig = kubeconfigsData.find((c) => c.is_active);
if (activeConfig) {
setSelectedCluster(activeConfig.id);
}
} catch (err) {
console.error("Failed to remove cluster:", err);
alert("Failed to remove cluster");
console.error("Failed to activate kubeconfig:", err);
alert("Failed to activate kubeconfig");
}
};
@ -72,12 +78,14 @@ export function KubernetesPage() {
}
};
const handleAddCluster = (cluster: ClusterInfo) => {
addCluster(cluster);
};
const handleStartPortForward = (portForward: PortForwardResponse) => {
setPortForwards((prev) => [...prev, portForward]);
const handleStartPortForward = async (portForward: PortForwardRequest) => {
try {
const result = await startPortForwardCmd(portForward);
setPortForwards((prev) => [...prev, result]);
} catch (err) {
console.error("Failed to start port forward:", err);
alert("Failed to start port forward");
}
};
if (isLoading) {
@ -100,40 +108,94 @@ export function KubernetesPage() {
</p>
</div>
{/* Cluster Management Section */}
{/* Cluster Management Section - Uses kubeconfig files from Settings */}
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold">Clusters</h2>
<button
onClick={() => setIsAddClusterOpen(true)}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
>
Add Cluster
</button>
<h2 className="text-xl font-semibold">Clusters (from kubeconfig files)</h2>
<div className="flex gap-2">
<button
onClick={() => window.location.href = "/settings/kubeconfig"}
className="px-4 py-2 bg-secondary text-secondary-foreground rounded-md hover:bg-secondary/90"
>
Manage kubeconfigs
</button>
</div>
</div>
<ClusterList
clusters={clusters}
onAdd={() => setIsAddClusterOpen(true)}
onRemove={handleRemoveCluster}
/>
{kubeconfigs.length === 0 ? (
<div className="rounded-lg border border-dashed px-6 py-12 text-center bg-card">
<div className="mx-auto w-12 h-12 text-muted-foreground mb-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</svg>
</div>
<h3 className="text-lg font-medium mb-2">No kubeconfig files uploaded</h3>
<p className="text-sm text-muted-foreground mb-4">
Upload kubeconfig files in Settings Kubeconfig to manage Kubernetes clusters
</p>
<button
onClick={() => window.location.href = "/settings/kubeconfig"}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
>
Go to Kubeconfig Manager
</button>
</div>
) : (
<div className="grid gap-4">
{kubeconfigs.map((config) => (
<div
key={config.id}
className={`rounded-lg border bg-card p-4 hover:border-primary/50 transition-colors ${
config.is_active ? "border-primary ring-1 ring-primary/20" : ""
}`}
>
<div className="flex items-start justify-between">
<div className="space-y-1 flex-1">
<div className="flex items-center gap-2">
<h3 className="font-medium text-lg">{config.name}</h3>
{config.is_active && (
<span className="px-2 py-1 text-xs font-semibold bg-green-100 text-green-800 rounded">
Active
</span>
)}
</div>
<div className="text-sm text-muted-foreground space-y-1">
<div>
<span className="font-medium">Context:</span> {config.context}
</div>
{config.cluster_url && (
<div>
<span className="font-medium">Cluster:</span> {config.cluster_url}
</div>
)}
</div>
</div>
<div className="flex gap-2">
{!config.is_active && (
<button
onClick={() => handleActivateKubeconfig(config.id)}
className="px-3 py-1 text-sm bg-secondary text-secondary-foreground rounded hover:bg-secondary/90"
>
Activate
</button>
)}
</div>
</div>
</div>
))}
</div>
)}
</div>
{/* Port Forwarding Section */}
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold">Port Forwarding</h2>
<button
onClick={() => setIsStartPortForwardOpen(true)}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
>
Start Port Forward
</button>
</div>
<PortForwardList
portForwards={portForwards}
onStart={() => setIsStartPortForwardOpen(true)}
onStart={() => {}}
onStop={handleStopPortForward}
onDelete={handleDeletePortForward}
/>
@ -146,20 +208,8 @@ export function KubernetesPage() {
<ResourceBrowser clusterId={selectedClusterId} />
</div>
)}
{/* Add Cluster Modal */}
<AddClusterModal
isOpen={isAddClusterOpen}
onClose={() => setIsAddClusterOpen(false)}
onAdd={handleAddCluster}
/>
{/* Port Forward Form */}
<PortForwardForm
isOpen={isStartPortForwardOpen}
onClose={() => setIsStartPortForwardOpen(false)}
onStart={handleStartPortForward}
/>
</div>
);
}

View File

@ -1,5 +1,5 @@
import { create } from "zustand";
import type { ClusterInfo, ContextInfo, ResourceInfo } from "@/lib/tauriCommands";
import type { ClusterInfo, ContextInfo, ResourceInfo, KubeconfigInfo } from "@/lib/tauriCommands";
export type ResourceType =
| "pods"
@ -103,6 +103,13 @@ export const useKubernetesStore = create<KubernetesState>()((set, get) => ({
// Actions
setSelectedCluster: (clusterId) => set({ selectedClusterId: clusterId, selectedNamespace: "all" }),
selectClusterFromKubeconfig: (kubeconfigs: KubeconfigInfo[]) => {
const activeConfig = kubeconfigs.find((c) => c.is_active);
if (activeConfig) {
set({ selectedClusterId: activeConfig.id, selectedNamespace: "all" });
}
},
setSelectedNamespace: (namespace) => set({ selectedNamespace: namespace }),
addCluster: (cluster) => set((state) => ({