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::shutdown_port_forwards,
|
||||||
commands::kube::test_cluster_connection,
|
commands::kube::test_cluster_connection,
|
||||||
commands::kube::discover_pods,
|
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!())
|
.run(tauri::generate_context!())
|
||||||
.expect("Error running Troubleshooting and RCA Assistant application");
|
.expect("Error running Troubleshooting and RCA Assistant application");
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import {
|
|||||||
Moon,
|
Moon,
|
||||||
Terminal,
|
Terminal,
|
||||||
FileCode,
|
FileCode,
|
||||||
|
Server,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useSettingsStore } from "@/stores/settingsStore";
|
import { useSettingsStore } from "@/stores/settingsStore";
|
||||||
import { getAppVersionCmd, loadAiProvidersCmd, testProviderConnectionCmd, shutdownPortForwardsCmd } from "@/lib/tauriCommands";
|
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 Security from "@/pages/Settings/Security";
|
||||||
import ShellExecution from "@/pages/Settings/ShellExecution";
|
import ShellExecution from "@/pages/Settings/ShellExecution";
|
||||||
import KubeconfigManager from "@/pages/Settings/KubeconfigManager";
|
import KubeconfigManager from "@/pages/Settings/KubeconfigManager";
|
||||||
|
import { KubernetesPage } from "@/pages/Kubernetes/KubernetesPage";
|
||||||
import { ShellApprovalModal } from "@/components/ShellApprovalModal";
|
import { ShellApprovalModal } from "@/components/ShellApprovalModal";
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ to: "/", icon: Home, label: "Dashboard" },
|
{ to: "/", icon: Home, label: "Dashboard" },
|
||||||
{ to: "/new-issue", icon: Plus, label: "New Issue" },
|
{ to: "/new-issue", icon: Plus, label: "New Issue" },
|
||||||
|
{ to: "/kubernetes", icon: Server, label: "Kubernetes" },
|
||||||
{ to: "/history", icon: Clock, label: "History" },
|
{ to: "/history", icon: Clock, label: "History" },
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -197,6 +200,7 @@ export default function App() {
|
|||||||
<Route path="/settings/ollama" element={<Ollama />} />
|
<Route path="/settings/ollama" element={<Ollama />} />
|
||||||
<Route path="/settings/shell" element={<ShellExecution />} />
|
<Route path="/settings/shell" element={<ShellExecution />} />
|
||||||
<Route path="/settings/kubeconfig" element={<KubeconfigManager />} />
|
<Route path="/settings/kubeconfig" element={<KubeconfigManager />} />
|
||||||
|
<Route path="/kubernetes" element={<KubernetesPage />} />
|
||||||
<Route path="/settings/integrations" element={<Integrations />} />
|
<Route path="/settings/integrations" element={<Integrations />} />
|
||||||
<Route path="/settings/mcp" element={<MCPServers />} />
|
<Route path="/settings/mcp" element={<MCPServers />} />
|
||||||
<Route path="/settings/security" element={<Security />} />
|
<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 { PortForwardList } from "./PortForwardList";
|
||||||
export { AddClusterModal } from "./AddClusterModal";
|
export { AddClusterModal } from "./AddClusterModal";
|
||||||
export { PortForwardForm } from "./PortForwardForm";
|
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";
|
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 };
|
export { cn };
|
||||||
|
|||||||
@ -783,6 +783,101 @@ export interface ClusterConnectionStatus {
|
|||||||
context: string;
|
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 ───────────────────────────────────────────
|
// ─── Kubernetes Management Commands ───────────────────────────────────────────
|
||||||
|
|
||||||
export const addClusterCmd = (id: string, name: string, kubeconfigContent: string) =>
|
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) =>
|
export const discoverPodsCmd = (clusterId: string, namespace: string) =>
|
||||||
invoke<PodInfo[]>("discover_pods", { clusterId, namespace });
|
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