feat: implement full Lens-like Kubernetes UI with resource discovery and management #75
File diff suppressed because it is too large
Load Diff
@ -187,6 +187,19 @@ pub fn run() {
|
||||
commands::kube::shutdown_port_forwards,
|
||||
commands::kube::test_cluster_connection,
|
||||
commands::kube::discover_pods,
|
||||
// Kubernetes Resource Discovery
|
||||
commands::kube::list_namespaces,
|
||||
commands::kube::list_pods,
|
||||
commands::kube::list_services,
|
||||
commands::kube::list_deployments,
|
||||
commands::kube::list_statefulsets,
|
||||
commands::kube::list_daemonsets,
|
||||
// Kubernetes Resource Management
|
||||
commands::kube::get_pod_logs,
|
||||
commands::kube::scale_deployment,
|
||||
commands::kube::restart_deployment,
|
||||
commands::kube::delete_resource,
|
||||
commands::kube::exec_pod,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("Error running Troubleshooting and RCA Assistant application");
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
Moon,
|
||||
Terminal,
|
||||
FileCode,
|
||||
Server,
|
||||
} from "lucide-react";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { getAppVersionCmd, loadAiProvidersCmd, testProviderConnectionCmd, shutdownPortForwardsCmd } from "@/lib/tauriCommands";
|
||||
@ -34,11 +35,13 @@ import MCPServers from "@/pages/Settings/MCPServers";
|
||||
import Security from "@/pages/Settings/Security";
|
||||
import ShellExecution from "@/pages/Settings/ShellExecution";
|
||||
import KubeconfigManager from "@/pages/Settings/KubeconfigManager";
|
||||
import { KubernetesPage } from "@/pages/Kubernetes/KubernetesPage";
|
||||
import { ShellApprovalModal } from "@/components/ShellApprovalModal";
|
||||
|
||||
const navItems = [
|
||||
{ to: "/", icon: Home, label: "Dashboard" },
|
||||
{ to: "/new-issue", icon: Plus, label: "New Issue" },
|
||||
{ to: "/kubernetes", icon: Server, label: "Kubernetes" },
|
||||
{ to: "/history", icon: Clock, label: "History" },
|
||||
];
|
||||
|
||||
@ -197,6 +200,7 @@ export default function App() {
|
||||
<Route path="/settings/ollama" element={<Ollama />} />
|
||||
<Route path="/settings/shell" element={<ShellExecution />} />
|
||||
<Route path="/settings/kubeconfig" element={<KubeconfigManager />} />
|
||||
<Route path="/kubernetes" element={<KubernetesPage />} />
|
||||
<Route path="/settings/integrations" element={<Integrations />} />
|
||||
<Route path="/settings/mcp" element={<MCPServers />} />
|
||||
<Route path="/settings/security" element={<Security />} />
|
||||
|
||||
51
src/components/Kubernetes/DaemonSetList.tsx
Normal file
51
src/components/Kubernetes/DaemonSetList.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import React from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
|
||||
import type { DaemonSetInfo } from "@/lib/tauriCommands";
|
||||
|
||||
interface DaemonSetListProps {
|
||||
daemonsets: DaemonSetInfo[];
|
||||
clusterId: string;
|
||||
namespace: string;
|
||||
}
|
||||
|
||||
export function DaemonSetList({ daemonsets, clusterId, namespace }: DaemonSetListProps) {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Desired</TableHead>
|
||||
<TableHead>Current</TableHead>
|
||||
<TableHead>Ready</TableHead>
|
||||
<TableHead>Up-to-date</TableHead>
|
||||
<TableHead>Available</TableHead>
|
||||
<TableHead>Age</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{daemonsets.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="text-center text-muted-foreground">
|
||||
No daemonsets found
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
daemonsets.map((ds) => (
|
||||
<TableRow key={ds.name}>
|
||||
<TableCell className="font-medium">{ds.name}</TableCell>
|
||||
<TableCell>{ds.desired}</TableCell>
|
||||
<TableCell>{ds.current}</TableCell>
|
||||
<TableCell>{ds.ready}</TableCell>
|
||||
<TableCell>{ds.up_to_date}</TableCell>
|
||||
<TableCell>{ds.available}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{ds.age}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
209
src/components/Kubernetes/DeploymentList.tsx
Normal file
209
src/components/Kubernetes/DeploymentList.tsx
Normal file
@ -0,0 +1,209 @@
|
||||
import React, { useState } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
|
||||
import { Button } from "@/components/ui";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui";
|
||||
import { Input } from "@/components/ui";
|
||||
import { Label } from "@/components/ui";
|
||||
import { Alert, AlertDescription } from "@/components/ui";
|
||||
import { AlertCircle, RotateCcw, Scale } from "lucide-react";
|
||||
import type { DeploymentInfo } from "@/lib/tauriCommands";
|
||||
|
||||
interface DeploymentListProps {
|
||||
deployments: DeploymentInfo[];
|
||||
clusterId: string;
|
||||
namespace: string;
|
||||
}
|
||||
|
||||
export function DeploymentList({ deployments, clusterId, namespace }: DeploymentListProps) {
|
||||
const [scalingDeployment, setScalingDeployment] = useState<DeploymentInfo | null>(null);
|
||||
const [replicas, setReplicas] = useState<string>("");
|
||||
const [isScaling, setIsScaling] = useState(false);
|
||||
const [scaleError, setScaleError] = useState<string | null>(null);
|
||||
|
||||
const [restartingDeployment, setRestartingDeployment] = useState<DeploymentInfo | null>(null);
|
||||
const [isRestarting, setIsRestarting] = useState(false);
|
||||
const [restartError, setRestartError] = useState<string | null>(null);
|
||||
|
||||
const handleScaleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setReplicas(e.target.value);
|
||||
setScaleError(null);
|
||||
};
|
||||
|
||||
const handleScaleSubmit = async () => {
|
||||
if (!scalingDeployment) return;
|
||||
|
||||
const newReplicas = parseInt(replicas, 10);
|
||||
if (isNaN(newReplicas) || newReplicas < 0) {
|
||||
setScaleError("Invalid replica count");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsScaling(true);
|
||||
setScaleError(null);
|
||||
|
||||
try {
|
||||
await invoke<void>("scale_deployment", {
|
||||
clusterId,
|
||||
namespace,
|
||||
deploymentName: scalingDeployment.name,
|
||||
replicas: newReplicas,
|
||||
});
|
||||
|
||||
setScalingDeployment(null);
|
||||
setReplicas("");
|
||||
} catch (err) {
|
||||
console.error("Failed to scale deployment:", err);
|
||||
setScaleError(err instanceof Error ? err.message : "Failed to scale deployment");
|
||||
} finally {
|
||||
setIsScaling(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestartSubmit = async () => {
|
||||
if (!restartingDeployment) return;
|
||||
|
||||
setIsRestarting(true);
|
||||
setRestartError(null);
|
||||
|
||||
try {
|
||||
await invoke<void>("restart_deployment", {
|
||||
clusterId,
|
||||
namespace,
|
||||
deploymentName: restartingDeployment.name,
|
||||
});
|
||||
|
||||
setRestartingDeployment(null);
|
||||
} catch (err) {
|
||||
console.error("Failed to restart deployment:", err);
|
||||
setRestartError(err instanceof Error ? err.message : "Failed to restart deployment");
|
||||
} finally {
|
||||
setIsRestarting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Ready</TableHead>
|
||||
<TableHead>Up-to-date</TableHead>
|
||||
<TableHead>Available</TableHead>
|
||||
<TableHead>Replicas</TableHead>
|
||||
<TableHead>Age</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{deployments.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="text-center text-muted-foreground">
|
||||
No deployments found
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
deployments.map((deployment) => (
|
||||
<TableRow key={deployment.name}>
|
||||
<TableCell className="font-medium">{deployment.name}</TableCell>
|
||||
<TableCell>{deployment.ready}</TableCell>
|
||||
<TableCell>{deployment.up_to_date}</TableCell>
|
||||
<TableCell>{deployment.available}</TableCell>
|
||||
<TableCell>{deployment.replicas}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{deployment.age}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setScalingDeployment(deployment)}
|
||||
>
|
||||
<Scale className="w-4 h-4" />
|
||||
Scale
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setRestartingDeployment(deployment)}
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
Restart
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Scale Dialog */}
|
||||
<Dialog open={!!scalingDeployment} onOpenChange={() => setScalingDeployment(null)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Scale Deployment</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="replicas">Replica Count</Label>
|
||||
<Input
|
||||
id="replicas"
|
||||
type="number"
|
||||
value={replicas}
|
||||
onChange={handleScaleChange}
|
||||
placeholder="Enter replica count"
|
||||
min="0"
|
||||
/>
|
||||
{scaleError && (
|
||||
<Alert variant="destructive" className="mt-2">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{scaleError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setScalingDeployment(null)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleScaleSubmit} disabled={isScaling}>
|
||||
{isScaling ? "Scaling..." : "Scale"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Restart Dialog */}
|
||||
<Dialog open={!!restartingDeployment} onOpenChange={() => setRestartingDeployment(null)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Restart Deployment</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This will trigger a rolling restart of the deployment.
|
||||
</p>
|
||||
{restartError && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{restartError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setRestartingDeployment(null)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleRestartSubmit} disabled={isRestarting}>
|
||||
{isRestarting ? "Restarting..." : "Restart"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
200
src/components/Kubernetes/PodList.tsx
Normal file
200
src/components/Kubernetes/PodList.tsx
Normal file
@ -0,0 +1,200 @@
|
||||
import React, { useState } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
|
||||
import { Badge } from "@/components/ui";
|
||||
import { Button } from "@/components/ui";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui";
|
||||
import { Textarea } from "@/components/ui";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui";
|
||||
import { Terminal, FileText, RotateCcw } from "lucide-react";
|
||||
import { Alert, AlertDescription } from "@/components/ui";
|
||||
import type { PodInfo, LogResponse } from "@/lib/tauriCommands";
|
||||
|
||||
interface PodListProps {
|
||||
pods: PodInfo[];
|
||||
clusterId: string;
|
||||
namespace: string;
|
||||
}
|
||||
|
||||
export function PodList({ pods, clusterId, namespace }: PodListProps) {
|
||||
const [selectedPod, setSelectedPod] = useState<PodInfo | null>(null);
|
||||
const [selectedContainer, setSelectedContainer] = useState<string>("");
|
||||
const [logs, setLogs] = useState<string>("");
|
||||
const [isFetchingLogs, setIsFetchingLogs] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
|
||||
const getPodStatusColor = (status: string) => {
|
||||
switch (status.toLowerCase()) {
|
||||
case "running":
|
||||
return "bg-green-500";
|
||||
case "pending":
|
||||
return "bg-yellow-500";
|
||||
case "succeeded":
|
||||
case "completed":
|
||||
return "bg-blue-500";
|
||||
case "failed":
|
||||
case "error":
|
||||
return "bg-red-500";
|
||||
default:
|
||||
return "bg-gray-500";
|
||||
}
|
||||
};
|
||||
|
||||
const fetchLogs = async () => {
|
||||
if (!selectedPod || !selectedContainer) return;
|
||||
|
||||
setIsFetchingLogs(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await invoke<LogResponse>("get_pod_logs", {
|
||||
clusterId,
|
||||
namespace,
|
||||
podName: selectedPod.name,
|
||||
containerName: selectedContainer,
|
||||
});
|
||||
setLogs(response.logs);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch logs:", err);
|
||||
setError(err instanceof Error ? err.message : "Failed to fetch logs");
|
||||
} finally {
|
||||
setIsFetchingLogs(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleContainerChange = (container: string) => {
|
||||
setSelectedContainer(container);
|
||||
setLogs("");
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const containers = selectedPod ? [selectedPod.name] : [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Ready</TableHead>
|
||||
<TableHead>Age</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{pods.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center text-muted-foreground">
|
||||
No pods found
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
pods.map((pod) => (
|
||||
<TableRow key={pod.name}>
|
||||
<TableCell className="font-medium">{pod.name}</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={`${getPodStatusColor(pod.status)} text-white`}>
|
||||
{pod.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{pod.ready}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{pod.age}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<Button variant="ghost" size="sm" onClick={() => { setSelectedPod(pod); setIsDialogOpen(true); }}>
|
||||
<Terminal className="w-4 h-4" />
|
||||
</Button>
|
||||
<DialogContent className="max-w-4xl max-h-[80vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{pod.name} - {namespace} namespace</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-hidden flex flex-col">
|
||||
{selectedPod && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">Container:</span>
|
||||
<select
|
||||
value={selectedContainer}
|
||||
onChange={(e) => handleContainerChange(e.target.value)}
|
||||
className="flex h-9 w-32 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
||||
>
|
||||
<option value="">Select container...</option>
|
||||
{containers.map((container) => (
|
||||
<option key={container} value={container}>
|
||||
{container}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Button
|
||||
onClick={fetchLogs}
|
||||
disabled={!selectedContainer || isFetchingLogs}
|
||||
size="sm"
|
||||
>
|
||||
{isFetchingLogs ? (
|
||||
<>
|
||||
<RotateCcw className="w-4 h-4 animate-spin" />
|
||||
Loading...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FileText className="w-4 h-4" />
|
||||
Fetch Logs
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Tabs value="logs" onValueChange={() => {}}>
|
||||
<TabsList className="grid grid-cols-2">
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="details">Details</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="flex-1 overflow-auto">
|
||||
<TabsContent value="logs" className="h-full">
|
||||
<Textarea
|
||||
value={logs}
|
||||
readOnly
|
||||
className="font-mono text-xs h-64"
|
||||
placeholder="No logs available. Click 'Fetch Logs' to retrieve."
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="details" className="h-full">
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="text-muted-foreground">Name:</div>
|
||||
<div>{selectedPod.name}</div>
|
||||
<div className="text-muted-foreground">Status:</div>
|
||||
<div>{selectedPod.status}</div>
|
||||
<div className="text-muted-foreground">Ready:</div>
|
||||
<div>{selectedPod.ready}</div>
|
||||
<div className="text-muted-foreground">Age:</div>
|
||||
<div>{selectedPod.age}</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
169
src/components/Kubernetes/ResourceBrowser.tsx
Normal file
169
src/components/Kubernetes/ResourceBrowser.tsx
Normal file
@ -0,0 +1,169 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui";
|
||||
import { Button } from "@/components/ui";
|
||||
import { Loader2, AlertCircle } from "lucide-react";
|
||||
import type { NamespaceInfo, PodInfo, ServiceInfo, DeploymentInfo, StatefulSetInfo, DaemonSetInfo } from "@/lib/tauriCommands";
|
||||
import { listNamespacesCmd, listPodsCmd, listServicesCmd, listDeploymentsCmd, listStatefulsetsCmd, listDaemonsetsCmd } from "@/lib/tauriCommands";
|
||||
import { PodList } from "./PodList";
|
||||
import { ServiceList } from "./ServiceList";
|
||||
import { DeploymentList } from "./DeploymentList";
|
||||
import { StatefulSetList } from "./StatefulSetList";
|
||||
import { DaemonSetList } from "./DaemonSetList";
|
||||
|
||||
type ResourceType = "pods" | "services" | "deployments" | "statefulsets" | "daemonsets";
|
||||
|
||||
interface ResourceBrowserProps {
|
||||
clusterId: string;
|
||||
}
|
||||
|
||||
export function ResourceBrowser({ clusterId }: ResourceBrowserProps) {
|
||||
const [namespaces, setNamespaces] = useState<NamespaceInfo[]>([]);
|
||||
const [selectedNamespace, setSelectedNamespace] = useState<string>("all");
|
||||
const [resourceType, setResourceType] = useState<ResourceType>("pods");
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [pods, setPods] = useState<PodInfo[]>([]);
|
||||
const [services, setServices] = useState<ServiceInfo[]>([]);
|
||||
const [deployments, setDeployments] = useState<DeploymentInfo[]>([]);
|
||||
const [statefulsets, setStatefulsets] = useState<StatefulSetInfo[]>([]);
|
||||
const [daemonsets, setDaemonsets] = useState<DaemonSetInfo[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [clusterId, selectedNamespace, resourceType]);
|
||||
|
||||
const loadData = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const [namespacesData, podsData, servicesData, deploymentsData, statefulsetsData, daemonsetsData] = await Promise.all([
|
||||
listNamespacesCmd(clusterId),
|
||||
selectedNamespace === "all" ? listPodsCmd(clusterId, "") : listPodsCmd(clusterId, selectedNamespace),
|
||||
selectedNamespace === "all" ? listServicesCmd(clusterId, "") : listServicesCmd(clusterId, selectedNamespace),
|
||||
selectedNamespace === "all" ? listDeploymentsCmd(clusterId, "") : listDeploymentsCmd(clusterId, selectedNamespace),
|
||||
selectedNamespace === "all" ? listStatefulsetsCmd(clusterId, "") : listStatefulsetsCmd(clusterId, selectedNamespace),
|
||||
selectedNamespace === "all" ? listDaemonsetsCmd(clusterId, "") : listDaemonsetsCmd(clusterId, selectedNamespace),
|
||||
]);
|
||||
|
||||
setNamespaces(namespacesData);
|
||||
setPods(podsData);
|
||||
setServices(servicesData);
|
||||
setDeployments(deploymentsData);
|
||||
setStatefulsets(statefulsetsData);
|
||||
setDaemonsets(daemonsetsData);
|
||||
} catch (err) {
|
||||
console.error("Failed to load resources:", err);
|
||||
setError(err instanceof Error ? err.message : "Failed to load resources");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getNamespaceOptions = () => {
|
||||
const options = [{ name: "All Namespaces", value: "all" }];
|
||||
namespaces.forEach(ns => {
|
||||
options.push({ name: ns.name, value: ns.name });
|
||||
});
|
||||
return options;
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
||||
<p className="text-muted-foreground">Loading Kubernetes resources...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<AlertCircle className="w-12 h-12 text-destructive" />
|
||||
<p className="text-center text-muted-foreground">{error}</p>
|
||||
<Button onClick={loadData}>Retry</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const renderResourceList = () => {
|
||||
switch (resourceType) {
|
||||
case "pods":
|
||||
return <PodList pods={pods} clusterId={clusterId} namespace={selectedNamespace} />;
|
||||
case "services":
|
||||
return <ServiceList services={services} clusterId={clusterId} namespace={selectedNamespace} />;
|
||||
case "deployments":
|
||||
return <DeploymentList deployments={deployments} clusterId={clusterId} namespace={selectedNamespace} />;
|
||||
case "statefulsets":
|
||||
return <StatefulSetList statefulsets={statefulsets} clusterId={clusterId} namespace={selectedNamespace} />;
|
||||
case "daemonsets":
|
||||
return <DaemonSetList daemonsets={daemonsets} clusterId={clusterId} namespace={selectedNamespace} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-y-auto p-6 space-y-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-3xl font-bold tracking-tight">Kubernetes Resources</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Browse and manage your Kubernetes resources
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Select value={selectedNamespace} onValueChange={setSelectedNamespace}>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue placeholder="Select namespace" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{getNamespaceOptions().map((ns) => (
|
||||
<SelectItem key={ns.value} value={ns.value}>
|
||||
{ns.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<Card className="flex-1 flex flex-col">
|
||||
<CardHeader>
|
||||
<Tabs value={resourceType} onValueChange={(v) => setResourceType(v as ResourceType)}>
|
||||
<TabsList className="grid grid-cols-5">
|
||||
<TabsTrigger value="pods">Pods</TabsTrigger>
|
||||
<TabsTrigger value="services">Services</TabsTrigger>
|
||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||
<TabsTrigger value="statefulsets">StatefulSets</TabsTrigger>
|
||||
<TabsTrigger value="daemonsets">DaemonSets</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 overflow-hidden">
|
||||
<div className="h-full overflow-y-auto">
|
||||
<TabsContent value={resourceType} className="h-full">
|
||||
{renderResourceList()}
|
||||
</TabsContent>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
81
src/components/Kubernetes/ServiceList.tsx
Normal file
81
src/components/Kubernetes/ServiceList.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import React from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
|
||||
import { Badge } from "@/components/ui";
|
||||
import type { ServiceInfo } from "@/lib/tauriCommands";
|
||||
|
||||
interface ServiceListProps {
|
||||
services: ServiceInfo[];
|
||||
clusterId: string;
|
||||
namespace: string;
|
||||
}
|
||||
|
||||
export function ServiceList({ services, clusterId, namespace }: ServiceListProps) {
|
||||
const getServiceTypeColor = (type: string) => {
|
||||
switch (type.toLowerCase()) {
|
||||
case "clusterip":
|
||||
return "bg-blue-500";
|
||||
case "nodeport":
|
||||
return "bg-purple-500";
|
||||
case "loadbalancer":
|
||||
return "bg-green-500";
|
||||
case "externalname":
|
||||
return "bg-gray-500";
|
||||
default:
|
||||
return "bg-gray-500";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Cluster IP</TableHead>
|
||||
<TableHead>External IP</TableHead>
|
||||
<TableHead>Ports</TableHead>
|
||||
<TableHead>Age</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{services.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center text-muted-foreground">
|
||||
No services found
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
services.map((service) => (
|
||||
<TableRow key={`${service.name}-${service.namespace}`}>
|
||||
<TableCell className="font-medium">{service.name}</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={`${getServiceTypeColor(service.type)} text-white`}>
|
||||
{service.type}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-sm">{service.cluster_ip}</TableCell>
|
||||
<TableCell className="font-mono text-sm">
|
||||
{service.external_ip || "N/A"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-1">
|
||||
{service.ports.map((port) => (
|
||||
<div key={`${port.port}-${port.protocol}`} className="text-sm">
|
||||
{port.name ? `${port.name}: ` : ""}
|
||||
{port.port}/{port.protocol}
|
||||
{port.target_port && ` → ${port.target_port}`}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">{service.age}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
45
src/components/Kubernetes/StatefulSetList.tsx
Normal file
45
src/components/Kubernetes/StatefulSetList.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import React from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
|
||||
import type { StatefulSetInfo } from "@/lib/tauriCommands";
|
||||
|
||||
interface StatefulSetListProps {
|
||||
statefulsets: StatefulSetInfo[];
|
||||
clusterId: string;
|
||||
namespace: string;
|
||||
}
|
||||
|
||||
export function StatefulSetList({ statefulsets, clusterId, namespace }: StatefulSetListProps) {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Ready</TableHead>
|
||||
<TableHead>Replicas</TableHead>
|
||||
<TableHead>Age</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{statefulsets.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-center text-muted-foreground">
|
||||
No statefulsets found
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
statefulsets.map((ss) => (
|
||||
<TableRow key={ss.name}>
|
||||
<TableCell className="font-medium">{ss.name}</TableCell>
|
||||
<TableCell>{ss.ready}</TableCell>
|
||||
<TableCell>{ss.replicas}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{ss.age}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -2,3 +2,9 @@ export { ClusterList } from "./ClusterList";
|
||||
export { PortForwardList } from "./PortForwardList";
|
||||
export { AddClusterModal } from "./AddClusterModal";
|
||||
export { PortForwardForm } from "./PortForwardForm";
|
||||
export { ResourceBrowser } from "./ResourceBrowser";
|
||||
export { PodList } from "./PodList";
|
||||
export { ServiceList } from "./ServiceList";
|
||||
export { DeploymentList } from "./DeploymentList";
|
||||
export { StatefulSetList } from "./StatefulSetList";
|
||||
export { DaemonSetList } from "./DaemonSetList";
|
||||
|
||||
@ -412,4 +412,295 @@ export const RadioGroupItem = React.forwardRef<HTMLInputElement, RadioGroupItemP
|
||||
);
|
||||
RadioGroupItem.displayName = "RadioGroupItem";
|
||||
|
||||
// ─── Table ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
Table.displayName = "Table";
|
||||
|
||||
export const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
));
|
||||
TableHeader.displayName = "TableHeader";
|
||||
|
||||
export const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableBody.displayName = "TableBody";
|
||||
|
||||
export const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableFooter.displayName = "TableFooter";
|
||||
|
||||
export const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement> & { hover?: boolean }
|
||||
>(({ className, hover, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableRow.displayName = "TableRow";
|
||||
|
||||
export const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableHead.displayName = "TableHead";
|
||||
|
||||
export const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableCell.displayName = "TableCell";
|
||||
|
||||
export const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableCaption.displayName = "TableCaption";
|
||||
|
||||
// ─── Tabs ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
const TabsContext = React.createContext<{
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
} | null>(null);
|
||||
|
||||
export function Tabs({ value, onValueChange, children }: { value: string; onValueChange: (value: string) => void; children: React.ReactNode }) {
|
||||
return (
|
||||
<TabsContext.Provider value={{ value, onValueChange }}>
|
||||
<div className="w-full">{children}</div>
|
||||
</TabsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function TabsList({ className, children }: { className?: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TabsTrigger({ className, children, value }: { className?: string; children: React.ReactNode; value: string }) {
|
||||
const ctx = React.useContext(TabsContext);
|
||||
if (!ctx) throw new Error("TabsTrigger must be used within Tabs");
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
ctx.value === value
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "hover:bg-background hover:text-foreground",
|
||||
className
|
||||
)}
|
||||
onClick={() => ctx.onValueChange(value)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function TabsContent({ className, children, value }: { className?: string; children: React.ReactNode; value: string }) {
|
||||
const ctx = React.useContext(TabsContext);
|
||||
if (!ctx) throw new Error("TabsContent must be used within Tabs");
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
ctx.value === value ? "block" : "hidden",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Dialog ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const DialogContext = React.createContext<{
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
} | null>(null);
|
||||
|
||||
export function Dialog({ open, onOpenChange, children }: { open: boolean; onOpenChange: (open: boolean) => void; children: React.ReactNode }) {
|
||||
return (
|
||||
<DialogContext.Provider value={{ open, onOpenChange }}>
|
||||
{children}
|
||||
</DialogContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function DialogTrigger({ children }: { children: React.ReactNode }) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
export function DialogContent({ className, children }: { className?: string; children: React.ReactNode }) {
|
||||
const ctx = React.useContext(DialogContext);
|
||||
if (!ctx) throw new Error("DialogContent must be used within Dialog");
|
||||
|
||||
if (!ctx.open) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||
<div
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DialogHeader({ className, children }: { className?: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DialogFooter({ className, children }: { className?: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DialogTitle({ className, children }: { className?: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<h2 className={cn("text-lg font-semibold leading-none tracking-tight", className)}>
|
||||
{children}
|
||||
</h2>
|
||||
);
|
||||
}
|
||||
|
||||
export function DialogDescription({ className, children }: { className?: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<p className={cn("text-sm text-muted-foreground", className)}>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Alert ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border p-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface AlertProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof alertVariants> {}
|
||||
|
||||
export function Alert({ className, variant, children, ...props }: AlertProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
role="alert"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AlertTitle({ className, children, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
|
||||
return (
|
||||
<h5
|
||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</h5>
|
||||
);
|
||||
}
|
||||
|
||||
export function AlertDescription({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { cn };
|
||||
|
||||
@ -783,6 +783,101 @@ export interface ClusterConnectionStatus {
|
||||
context: string;
|
||||
}
|
||||
|
||||
// ─── Kubernetes Resource Discovery Types ──────────────────────────────────────
|
||||
|
||||
export interface NamespaceInfo {
|
||||
name: string;
|
||||
status: string;
|
||||
age: string;
|
||||
}
|
||||
|
||||
export interface ServicePort {
|
||||
name?: string;
|
||||
port: number;
|
||||
target_port?: string;
|
||||
protocol: string;
|
||||
}
|
||||
|
||||
export interface ServiceInfo {
|
||||
name: string;
|
||||
namespace: string;
|
||||
type: string;
|
||||
cluster_ip: string;
|
||||
external_ip?: string;
|
||||
ports: ServicePort[];
|
||||
age: string;
|
||||
selector: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface DeploymentInfo {
|
||||
name: string;
|
||||
namespace: string;
|
||||
ready: string;
|
||||
up_to_date: string;
|
||||
available: string;
|
||||
age: string;
|
||||
replicas: number;
|
||||
labels: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface StatefulSetInfo {
|
||||
name: string;
|
||||
namespace: string;
|
||||
ready: string;
|
||||
age: string;
|
||||
replicas: number;
|
||||
labels: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface DaemonSetInfo {
|
||||
name: string;
|
||||
namespace: string;
|
||||
desired: number;
|
||||
current: number;
|
||||
ready: number;
|
||||
up_to_date: number;
|
||||
available: number;
|
||||
age: string;
|
||||
labels: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface NodeMetrics {
|
||||
name: string;
|
||||
cpu_usage: string;
|
||||
memory_usage: string;
|
||||
cpu_percentage: number;
|
||||
memory_percentage: number;
|
||||
age: string;
|
||||
}
|
||||
|
||||
export interface PodMetrics {
|
||||
name: string;
|
||||
namespace: string;
|
||||
cpu_usage: string;
|
||||
memory_usage: string;
|
||||
cpu_percentage: number;
|
||||
memory_percentage: number;
|
||||
}
|
||||
|
||||
export interface LogResponse {
|
||||
logs: string;
|
||||
}
|
||||
|
||||
export interface ExecResponse {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
exit_code: number | null;
|
||||
}
|
||||
|
||||
export interface ExecSessionResponse {
|
||||
session_id: string;
|
||||
cluster_id: string;
|
||||
namespace: string;
|
||||
pod: string;
|
||||
container?: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
// ─── Kubernetes Management Commands ───────────────────────────────────────────
|
||||
|
||||
export const addClusterCmd = (id: string, name: string, kubeconfigContent: string) =>
|
||||
@ -814,3 +909,40 @@ export const testClusterConnectionCmd = (clusterId: string) =>
|
||||
|
||||
export const discoverPodsCmd = (clusterId: string, namespace: string) =>
|
||||
invoke<PodInfo[]>("discover_pods", { clusterId, namespace });
|
||||
|
||||
// ─── Kubernetes Resource Discovery Commands ───────────────────────────────────
|
||||
|
||||
export const listNamespacesCmd = (clusterId: string) =>
|
||||
invoke<NamespaceInfo[]>("list_namespaces", { clusterId });
|
||||
|
||||
export const listPodsCmd = (clusterId: string, namespace: string) =>
|
||||
invoke<PodInfo[]>("list_pods", { clusterId, namespace });
|
||||
|
||||
export const listServicesCmd = (clusterId: string, namespace: string) =>
|
||||
invoke<ServiceInfo[]>("list_services", { clusterId, namespace });
|
||||
|
||||
export const listDeploymentsCmd = (clusterId: string, namespace: string) =>
|
||||
invoke<DeploymentInfo[]>("list_deployments", { clusterId, namespace });
|
||||
|
||||
export const listStatefulsetsCmd = (clusterId: string, namespace: string) =>
|
||||
invoke<StatefulSetInfo[]>("list_statefulsets", { clusterId, namespace });
|
||||
|
||||
export const listDaemonsetsCmd = (clusterId: string, namespace: string) =>
|
||||
invoke<DaemonSetInfo[]>("list_daemonsets", { clusterId, namespace });
|
||||
|
||||
// ─── Kubernetes Resource Management Commands ──────────────────────────────────
|
||||
|
||||
export const getPodLogsCmd = (clusterId: string, namespace: string, podName: string, containerName: string) =>
|
||||
invoke<LogResponse>("get_pod_logs", { clusterId, namespace, podName, containerName });
|
||||
|
||||
export const scaleDeploymentCmd = (clusterId: string, namespace: string, deploymentName: string, replicas: number) =>
|
||||
invoke<void>("scale_deployment", { clusterId, namespace, deploymentName, replicas });
|
||||
|
||||
export const restartDeploymentCmd = (clusterId: string, namespace: string, deploymentName: string) =>
|
||||
invoke<void>("restart_deployment", { clusterId, namespace, deploymentName });
|
||||
|
||||
export const deleteResourceCmd = (clusterId: string, resourceType: string, namespace: string, resourceName: string) =>
|
||||
invoke<void>("delete_resource", { clusterId, resourceType, namespace, resourceName });
|
||||
|
||||
export const execPodCmd = (clusterId: string, namespace: string, podName: string, containerName: string, command: string) =>
|
||||
invoke<ExecResponse>("exec_pod", { clusterId, namespace, podName, containerName, command });
|
||||
|
||||
Loading…
Reference in New Issue
Block a user