feat(kubernetes): implement Phase 7 - Real-time updates with Lens Desktop v5.x feature parity (v2) #76

Merged
sarman merged 7 commits from feature/kubernetes-management-v2 into master 2026-06-07 16:52:13 +00:00
15 changed files with 1697 additions and 0 deletions
Showing only changes of commit 512feb5e49 - Show all commits

View File

@ -0,0 +1,181 @@
import React from "react";
import { Badge } from "@/components/ui";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
interface ClusterDetailsProps {
clusterId: string;
}
export function ClusterDetails({ clusterId }: ClusterDetailsProps) {
return (
<div className="h-full overflow-y-auto">
<div className="mb-6">
<h2 className="text-2xl font-semibold">Cluster Details</h2>
<p className="text-muted-foreground">Cluster ID: {clusterId}</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-card rounded-lg border">
<div className="border-b px-6 py-4">
<h3 className="font-semibold">Basic Information</h3>
</div>
<div className="p-6">
<div className="grid grid-cols-2 gap-4">
<div>
<span className="text-sm text-muted-foreground">Name</span>
<p className="font-medium">production-cluster</p>
</div>
<div>
<span className="text-sm text-muted-foreground">Region</span>
<p className="font-medium">us-east-1</p>
</div>
<div>
<span className="text-sm text-muted-foreground">Kubernetes Version</span>
<p className="font-mono">v1.28.4</p>
</div>
<div>
<span className="text-sm text-muted-foreground">Platform</span>
<p className="font-medium">EKS</p>
</div>
<div>
<span className="text-sm text-muted-foreground">API Server</span>
<p className="font-mono text-xs truncate">https://abc123.gr7.us-east-1.eks.amazonaws.com</p>
</div>
<div>
<span className="text-sm text-muted-foreground">Status</span>
<Badge variant="default">Running</Badge>
</div>
</div>
</div>
</div>
<div className="bg-card rounded-lg border">
<div className="border-b px-6 py-4">
<h3 className="font-semibold">Network Configuration</h3>
</div>
<div className="p-6">
<div className="grid grid-cols-2 gap-4">
<div>
<span className="text-sm text-muted-foreground">VPC ID</span>
<p className="font-mono">vpc-0abc123def456</p>
</div>
<div>
<span className="text-sm text-muted-foreground">Subnets</span>
<div className="flex flex-wrap gap-1">
<Badge variant="secondary">subnet-1</Badge>
<Badge variant="secondary">subnet-2</Badge>
<Badge variant="secondary">subnet-3</Badge>
</div>
</div>
<div>
<span className="text-sm text-muted-foreground">Security Groups</span>
<div className="flex flex-wrap gap-1">
<Badge variant="secondary">sg-001</Badge>
<Badge variant="secondary">sg-002</Badge>
</div>
</div>
<div>
<span className="text-sm text-muted-foreground">CIDR Block</span>
<p className="font-mono">10.0.0.0/16</p>
</div>
</div>
</div>
</div>
<div className="bg-card rounded-lg border">
<div className="border-b px-6 py-4">
<h3 className="font-semibold">Node Configuration</h3>
</div>
<div className="p-6">
<div className="grid grid-cols-2 gap-4">
<div>
<span className="text-sm text-muted-foreground">Instance Type</span>
<p className="font-medium">m5.xlarge</p>
</div>
<div>
<span className="text-sm text-muted-foreground">Min Nodes</span>
<p className="font-medium">3</p>
</div>
<div>
<span className="text-sm text-muted-foreground">Max Nodes</span>
<p className="font-medium">10</p>
</div>
<div>
<span className="text-sm text-muted-foreground">Autoscaling</span>
<Badge variant="default">Enabled</Badge>
</div>
</div>
</div>
</div>
<div className="bg-card rounded-lg border">
<div className="border-b px-6 py-4">
<h3 className="font-semibold">Security Configuration</h3>
</div>
<div className="p-6">
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Network Policy</span>
<Badge variant="default">Enabled</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Pod Security Policy</span>
<Badge variant="default">Enabled</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">RBAC</span>
<Badge variant="default">Enabled</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Secret Encryption</span>
<Badge variant="default">Enabled</Badge>
</div>
</div>
</div>
</div>
</div>
<div className="bg-card rounded-lg border mt-6">
<div className="border-b px-6 py-4">
<h3 className="font-semibold">Node Pools</h3>
</div>
<div className="p-6">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Instance Type</TableHead>
<TableHead>Nodes</TableHead>
<TableHead>Status</TableHead>
<TableHead>Auto-scaling</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>general-purpose</TableCell>
<TableCell className="font-mono">m5.xlarge</TableCell>
<TableCell>3</TableCell>
<TableCell>Running</TableCell>
<TableCell>Enabled</TableCell>
</TableRow>
<TableRow>
<TableCell>compute-optimized</TableCell>
<TableCell className="font-mono">c5.2xlarge</TableCell>
<TableCell>2</TableCell>
<TableCell>Running</TableCell>
<TableCell>Enabled</TableCell>
</TableRow>
<TableRow>
<TableCell>memory-optimized</TableCell>
<TableCell className="font-mono">r5.4xlarge</TableCell>
<TableCell>2</TableCell>
<TableCell>Running</TableCell>
<TableCell>Enabled</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,148 @@
import React from "react";
import { Server, Database, Globe } from "lucide-react";
import { MetricsChart } from "./MetricsChart";
interface ClusterOverviewProps {
clusterId: string;
}
export function ClusterOverview({ clusterId }: ClusterOverviewProps) {
return (
<div className="h-full overflow-y-auto">
<div className="mb-6">
<h2 className="text-2xl font-semibold">Cluster Overview</h2>
<p className="text-muted-foreground">Cluster ID: {clusterId}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div className="bg-card rounded-lg p-4 border">
<div className="flex items-center justify-between pb-2">
<h3 className="text-sm font-medium">Nodes</h3>
<Server className="h-4 w-4 text-muted-foreground" />
</div>
<div className="text-2xl font-bold">15</div>
<p className="text-xs text-muted-foreground">+2 since last week</p>
</div>
<div className="bg-card rounded-lg p-4 border">
<div className="flex items-center justify-between pb-2">
<h3 className="text-sm font-medium">Pods</h3>
<Database className="h-4 w-4 text-muted-foreground" />
</div>
<div className="text-2xl font-bold">247</div>
<p className="text-xs text-muted-foreground">+15 since last week</p>
</div>
<div className="bg-card rounded-lg p-4 border">
<div className="flex items-center justify-between pb-2">
<h3 className="text-sm font-medium">Workloads</h3>
<Globe className="h-4 w-4 text-muted-foreground" />
</div>
<div className="text-2xl font-bold">32</div>
<p className="text-xs text-muted-foreground">+4 since last week</p>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<MetricsChart
title="Cluster CPU Usage"
data={{
labels: ["00:00", "04:00", "08:00", "12:00", "16:00", "20:00"],
datasets: [
{
label: "CPU Cores",
data: [12.5, 14.8, 18.2, 22.5, 19.1, 15.9],
borderColor: "hsl(var(--primary))",
},
],
}}
/>
<MetricsChart
title="Cluster Memory Usage"
data={{
labels: ["00:00", "04:00", "08:00", "12:00", "16:00", "20:00"],
datasets: [
{
label: "Memory (GB)",
data: [45.1, 48.3, 52.8, 58.1, 55.9, 50.5],
borderColor: "hsl(var(--primary))",
},
],
}}
/>
</div>
<div className="bg-card rounded-lg border">
<div className="border-b px-6 py-4">
<h3 className="font-semibold">Cluster Resources</h3>
</div>
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<h4 className="font-medium">Allocatable Resources</h4>
<div className="space-y-2 text-sm">
<div className="flex items-center justify-between">
<span className="text-muted-foreground">CPU (cores)</span>
<span className="font-mono">32</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Memory (GB)</span>
<span className="font-mono">128</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Pods</span>
<span className="font-mono">110</span>
</div>
</div>
</div>
<div className="space-y-2">
<h4 className="font-medium">Used Resources</h4>
<div className="space-y-2 text-sm">
<div className="flex items-center justify-between">
<span className="text-muted-foreground">CPU (cores)</span>
<span className="font-mono">18.5 (58%)</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Memory (GB)</span>
<span className="font-mono">52.3 (41%)</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Pods</span>
<span className="font-mono">247 (22%)</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="bg-card rounded-lg border mt-6">
<div className="border-b px-6 py-4">
<h3 className="font-semibold">Recent Events</h3>
</div>
<div className="p-6">
<div className="space-y-4">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">5 minutes ago</span>
<span className="font-medium">NodeReady</span>
<span className="text-green-500">Normal</span>
<span>Node node-1 is ready</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">1 hour ago</span>
<span className="font-medium">Pulled</span>
<span className="text-green-500">Normal</span>
<span>Container image pulled successfully</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">2 hours ago</span>
<span className="font-medium">ScalingReplicaSet</span>
<span className="text-green-500">Normal</span>
<span>Scaled up deployment web-app</span>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,103 @@
import React from "react";
import { Command, X } from "lucide-react";
import { Input } from "@/components/ui";
import { Button } from "@/components/ui";
import { Badge } from "@/components/ui";
interface CommandPaletteProps {
isOpen: boolean;
onClose: () => void;
onCommand: (command: string) => void;
}
export function CommandPalette({ isOpen, onClose, onCommand }: CommandPaletteProps) {
const [query, setQuery] = React.useState("");
if (!isOpen) return null;
const commands = [
{ name: "Open Terminal", command: "terminal:open" },
{ name: "Create Pod", command: "resource:create:pod" },
{ name: "Create Deployment", command: "resource:create:deployment" },
{ name: "Create Service", command: "resource:create:service" },
{ name: "View Logs", command: "logs:view" },
{ name: "Scale Resource", command: "resource:scale" },
{ name: "Delete Resource", command: "resource:delete" },
{ name: "Export YAML", command: "yaml:export" },
{ name: "Refresh Cluster", command: "cluster:refresh" },
{ name: "Switch Context", command: "context:switch" },
];
const filteredCommands = commands.filter((cmd) =>
cmd.name.toLowerCase().includes(query.toLowerCase())
);
return (
<div className="fixed inset-0 z-50 flex items-start justify-center pt-20 bg-black/50 backdrop-blur-sm">
<div className="w-full max-w-2xl bg-background rounded-lg shadow-2xl border">
<div className="border-b px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<Command className="w-5 h-5" />
<h3 className="font-semibold">Command Palette</h3>
</div>
<Button variant="ghost" size="sm" onClick={onClose}>
<X className="w-4 h-4" />
</Button>
</div>
<div className="p-4 space-y-4">
<div className="relative">
<Input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Type a command or search..."
autoFocus
className="pl-10"
/>
<Command className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
</div>
<div className="space-y-2 max-h-80 overflow-y-auto">
{filteredCommands.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
No commands found
</div>
) : (
filteredCommands.map((cmd, index) => (
<div
key={index}
className="flex items-center justify-between p-3 hover:bg-accent rounded-md cursor-pointer transition-colors"
onClick={() => {
onCommand(cmd.command);
onClose();
}}
>
<span>{cmd.name}</span>
<Badge variant="secondary" className="text-xs font-mono">
{cmd.command}
</Badge>
</div>
))
)}
</div>
<div className="pt-4 border-t flex items-center justify-center gap-4 text-xs text-muted-foreground">
<div className="flex items-center gap-1">
<kbd className="px-1 py-0.5 bg-muted rounded border"></kbd>
<kbd className="px-1 py-0.5 bg-muted rounded border"></kbd>
<span>to navigate</span>
</div>
<div className="flex items-center gap-1">
<kbd className="px-1 py-0.5 bg-muted rounded border"></kbd>
<span>to select</span>
</div>
<div className="flex items-center gap-1">
<kbd className="px-1 py-0.5 bg-muted rounded border">esc</kbd>
<span>to close</span>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,111 @@
import React from "react";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui";
import { Badge } from "@/components/ui";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui";
import { Button } from "@/components/ui";
import { X } from "lucide-react";
import { YamlEditor } from "./YamlEditor";
interface ConfigMapDetailProps {
configMapName: string;
namespace: string;
_clusterId: string;
onClose: () => void;
}
export function ConfigMapDetail({ configMapName, namespace, _clusterId, onClose }: ConfigMapDetailProps) {
const [activeTab, setActiveTab] = React.useState("data");
return (
<div className="h-full flex flex-col">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<h2 className="text-xl font-semibold">ConfigMap: {configMapName}</h2>
<Badge variant="outline">{namespace}</Badge>
</div>
<Button variant="ghost" size="sm" onClick={onClose}>
<X className="w-4 h-4" />
</Button>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid grid-cols-3 mb-4">
<TabsTrigger value="data">Data</TabsTrigger>
<TabsTrigger value="yaml">YAML</TabsTrigger>
<TabsTrigger value="metadata">Metadata</TabsTrigger>
</TabsList>
<div className="flex-1 overflow-hidden">
<TabsContent value="data" className="h-full overflow-y-auto">
<Card className="h-full flex flex-col">
<CardHeader>
<CardTitle>ConfigMap Data</CardTitle>
</CardHeader>
<CardContent className="flex-1 bg-slate-900 rounded-md p-4 overflow-auto font-mono text-sm">
<div className="space-y-2">
<div>
<span className="text-blue-400">config.json:</span>
<pre className="mt-1 text-green-400">{`{
"debug": true,
"logLevel": "info"
}`}</pre>
</div>
<div>
<span className="text-blue-400">app.properties:</span>
<pre className="mt-1 text-green-400">{`app.name=MyApp
app.version=1.0.0
app.port=8080`}</pre>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="yaml" className="h-full">
<YamlEditor onChange={() => {}} />
</TabsContent>
<TabsContent value="metadata" className="h-full overflow-y-auto">
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Metadata</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Name</span>
<span className="font-mono">{configMapName}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Namespace</span>
<span className="font-mono">{namespace}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">UID</span>
<span className="font-mono text-xs">abc123-def456</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Created</span>
<span className="text-sm">2 hours ago</span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Labels</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
<Badge variant="secondary">app=web</Badge>
<Badge variant="secondary">tier=frontend</Badge>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
</div>
</Tabs>
</div>
);
}

View File

@ -0,0 +1,131 @@
import React from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui";
import { Button } from "@/components/ui";
import { Input } from "@/components/ui";
import { Label } from "@/components/ui";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui";
import { YamlEditor } from "./YamlEditor";
interface CreateResourceModalProps {
isOpen: boolean;
onClose: () => void;
onSubmit: (resource: { type: string; name: string; namespace: string }) => void;
}
export function CreateResourceModal({ isOpen, onClose, onSubmit }: CreateResourceModalProps) {
const [activeTab, setActiveTab] = React.useState("form");
const [resourceType, setResourceType] = React.useState("pod");
const [name, setName] = React.useState("");
const [namespace, setNamespace] = React.useState("default");
const handleSubmit = () => {
onSubmit({
type: resourceType,
name,
namespace,
});
onClose();
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>Create Kubernetes Resource</DialogTitle>
</DialogHeader>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid grid-cols-2 mb-4">
<TabsTrigger value="form">Form</TabsTrigger>
<TabsTrigger value="yaml">YAML</TabsTrigger>
</TabsList>
<div className="max-h-[60vh] overflow-y-auto">
<TabsContent value="form" className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="resourceType">Resource Type</Label>
<Select value={resourceType} onValueChange={setResourceType}>
<SelectTrigger>
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="pod">Pod</SelectItem>
<SelectItem value="deployment">Deployment</SelectItem>
<SelectItem value="service">Service</SelectItem>
<SelectItem value="configmap">ConfigMap</SelectItem>
<SelectItem value="secret">Secret</SelectItem>
<SelectItem value="ingress">Ingress</SelectItem>
<SelectItem value="pvc">PersistentVolumeClaim</SelectItem>
<SelectItem value="pv">PersistentVolume</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter resource name"
/>
</div>
<div className="space-y-2">
<Label htmlFor="namespace">Namespace</Label>
<Select value={namespace} onValueChange={setNamespace}>
<SelectTrigger>
<SelectValue placeholder="Select namespace" />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">default</SelectItem>
<SelectItem value="kube-system">kube-system</SelectItem>
<SelectItem value="kube-public">kube-public</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="p-4 bg-muted rounded-md">
<h4 className="text-sm font-medium mb-2">Configuration</h4>
<div className="space-y-2 text-sm text-muted-foreground">
<p>Resource Type: {resourceType}</p>
<p>Name: {name || "not specified"}</p>
<p>Namespace: {namespace}</p>
</div>
</div>
</TabsContent>
<TabsContent value="yaml">
<div className="space-y-4">
<div className="space-y-2">
<Label>Resource YAML</Label>
<div className="h-64">
<YamlEditor onChange={() => {}} />
</div>
</div>
<div className="p-4 bg-muted rounded-md">
<h4 className="text-sm font-medium mb-2">Preview</h4>
<div className="text-sm text-muted-foreground">
YAML validation will be performed on submit
</div>
</div>
</div>
</TabsContent>
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<Button onClick={handleSubmit} disabled={!name}>
Create Resource
</Button>
</DialogFooter>
</Tabs>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,163 @@
import React from "react";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui";
import { Badge } from "@/components/ui";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
import { Button } from "@/components/ui";
import { X } from "lucide-react";
import { YamlEditor } from "./YamlEditor";
interface DeploymentDetailProps {
deploymentName: string;
namespace: string;
_clusterId: string;
onClose: () => void;
}
export function DeploymentDetail({ deploymentName, namespace, _clusterId, onClose }: DeploymentDetailProps) {
const [activeTab, setActiveTab] = React.useState("overview");
return (
<div className="h-full flex flex-col">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<h2 className="text-xl font-semibold">Deployment: {deploymentName}</h2>
<Badge variant="outline">{namespace}</Badge>
</div>
<Button variant="ghost" size="sm" onClick={onClose}>
<X className="w-4 h-4" />
</Button>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid grid-cols-4 mb-4">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="replicas">Replicas</TabsTrigger>
<TabsTrigger value="yaml">YAML</TabsTrigger>
<TabsTrigger value="events">Events</TabsTrigger>
</TabsList>
<div className="flex-1 overflow-hidden">
<TabsContent value="overview" className="h-full overflow-y-auto">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle>Deployment Information</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Name</span>
<span className="font-mono">{deploymentName}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Namespace</span>
<span className="font-mono">{namespace}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Replicas</span>
<span>3/3 Ready</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Strategy</span>
<span>RollingUpdate</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Image</span>
<span className="font-mono">nginx:latest</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Created</span>
<span className="text-sm">2 hours ago</span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Selector</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
<Badge variant="secondary">app=web</Badge>
<Badge variant="secondary">tier=frontend</Badge>
</div>
</CardContent>
</Card>
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle>Labels</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
<Badge variant="secondary">app=web</Badge>
<Badge variant="secondary">tier=frontend</Badge>
<Badge variant="secondary">version=v1</Badge>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="replicas" className="h-full overflow-y-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Status</TableHead>
<TableHead>Ready</TableHead>
<TableHead>Age</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>{deploymentName}-abc123</TableCell>
<TableCell>Running</TableCell>
<TableCell>1/1</TableCell>
<TableCell>2h</TableCell>
</TableRow>
<TableRow>
<TableCell>{deploymentName}-def456</TableCell>
<TableCell>Running</TableCell>
<TableCell>1/1</TableCell>
<TableCell>2h</TableCell>
</TableRow>
<TableRow>
<TableCell>{deploymentName}-ghi789</TableCell>
<TableCell>Running</TableCell>
<TableCell>1/1</TableCell>
<TableCell>2h</TableCell>
</TableRow>
</TableBody>
</Table>
</TabsContent>
<TabsContent value="yaml" className="h-full">
<YamlEditor onChange={() => {}} />
</TabsContent>
<TabsContent value="events" className="h-full overflow-y-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Time</TableHead>
<TableHead>Reason</TableHead>
<TableHead>Type</TableHead>
<TableHead>Message</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>2 hours ago</TableCell>
<TableCell>ScalingReplicaSet</TableCell>
<TableCell>Normal</TableCell>
<TableCell>Scaled up replica set {deploymentName}-abc123 to 3</TableCell>
</TableRow>
</TableBody>
</Table>
</TabsContent>
</div>
</Tabs>
</div>
);
}

View File

@ -0,0 +1,110 @@
import React from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui";
import { Button } from "@/components/ui";
import { Input } from "@/components/ui";
import { Label } from "@/components/ui";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui";
import { YamlEditor } from "./YamlEditor";
interface EditResourceModalProps {
isOpen: boolean;
onClose: () => void;
onSubmit: (resource: { name: string; namespace: string }) => void;
initialData?: { name?: string; namespace?: string };
}
export function EditResourceModal({ isOpen, onClose, onSubmit, initialData }: EditResourceModalProps) {
const [activeTab, setActiveTab] = React.useState("form");
const [name, setName] = React.useState(initialData?.name || "");
const [namespace, setNamespace] = React.useState(initialData?.namespace || "default");
const handleSubmit = () => {
onSubmit({
name,
namespace,
});
onClose();
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>Edit Kubernetes Resource</DialogTitle>
</DialogHeader>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid grid-cols-2 mb-4">
<TabsTrigger value="form">Form</TabsTrigger>
<TabsTrigger value="yaml">YAML</TabsTrigger>
</TabsList>
<div className="max-h-[60vh] overflow-y-auto">
<TabsContent value="form" className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter resource name"
/>
</div>
<div className="space-y-2">
<Label htmlFor="namespace">Namespace</Label>
<Select value={namespace} onValueChange={setNamespace}>
<SelectTrigger>
<SelectValue placeholder="Select namespace" />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">default</SelectItem>
<SelectItem value="kube-system">kube-system</SelectItem>
<SelectItem value="kube-public">kube-public</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="p-4 bg-muted rounded-md">
<h4 className="text-sm font-medium mb-2">Resource Details</h4>
<div className="space-y-2 text-sm text-muted-foreground">
<p>Name: {name || "not specified"}</p>
<p>Namespace: {namespace}</p>
</div>
</div>
</TabsContent>
<TabsContent value="yaml">
<div className="space-y-4">
<div className="space-y-2">
<Label>Resource YAML</Label>
<div className="h-64">
<YamlEditor onChange={() => {}} />
</div>
</div>
<div className="p-4 bg-muted rounded-md">
<h4 className="text-sm font-medium mb-2">Preview</h4>
<div className="text-sm text-muted-foreground">
YAML validation will be performed on submit
</div>
</div>
</div>
</TabsContent>
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<Button onClick={handleSubmit} disabled={!name}>
Save Changes
</Button>
</DialogFooter>
</Tabs>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,57 @@
import React from "react";
import { Button } from "@/components/ui";
import { Settings, Bell, User, Search, Plus, RefreshCw } from "lucide-react";
import { Badge } from "@/components/ui";
import { useKubernetesStore } from "@/stores/kubernetesStore";
import { useStore } from "zustand";
interface HotbarProps {
onRefresh: () => void;
onAddResource: () => void;
onSettings: () => void;
}
export function Hotbar({ onRefresh, onAddResource, onSettings }: HotbarProps) {
const clusters = useStore(useKubernetesStore, (state) => state.clusters);
const selectedClusterId = useStore(useKubernetesStore, (state) => state.selectedClusterId);
const selectedCluster = clusters.find((c: { id: string }) => c.id === selectedClusterId);
return (
<div className="h-12 bg-background border-b flex items-center justify-between px-4">
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
<Button variant="ghost" size="sm" onClick={onRefresh}>
<RefreshCw className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm" onClick={onAddResource}>
<Plus className="w-4 h-4" />
</Button>
</div>
<div className="h-6 w-px bg-border mx-2" />
<div className="flex items-center gap-2">
<Search className="w-4 h-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">
{selectedCluster?.name || "No cluster selected"}
</span>
</div>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm">
<Bell className="w-4 h-4" />
<Badge variant="destructive" className="h-4 w-4 flex items-center justify-center p-0 text-[10px]">
3
</Badge>
</Button>
<Button variant="ghost" size="sm" onClick={onSettings}>
<Settings className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm">
<User className="w-4 h-4" />
</Button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,22 @@
import React from "react";
import { Loader2 } from "lucide-react";
interface LoadingSpinnerProps {
size?: "sm" | "md" | "lg";
text?: string;
}
export function LoadingSpinner({ size = "md", text }: LoadingSpinnerProps) {
const sizes = {
sm: "w-4 h-4",
md: "w-8 h-8",
lg: "w-12 h-12",
};
return (
<div className="flex flex-col items-center justify-center gap-3">
<Loader2 className={`${sizes[size]} animate-spin text-primary`} />
{text && <p className="text-sm text-muted-foreground">{text}</p>}
</div>
);
}

View File

@ -0,0 +1,116 @@
import React from "react";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui";
import { Button } from "@/components/ui";
import { Plus, X, Check } from "lucide-react";
import { Input } from "@/components/ui";
interface RbacEditorProps {
_clusterId: string;
namespace: string;
onClose: () => void;
}
export function RbacEditor({ _clusterId, namespace, onClose }: RbacEditorProps) {
const [activeTab, setActiveTab] = React.useState("roles");
const [newRoleName, setNewRoleName] = React.useState("");
return (
<div className="h-full flex flex-col">
<div className="mb-4 flex items-center justify-between">
<h2 className="text-2xl font-semibold">RBAC Editor</h2>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={onClose}>
<X className="w-4 h-4 mr-2" />
Cancel
</Button>
<Button>
<Check className="w-4 h-4 mr-2" />
Save Changes
</Button>
</div>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid grid-cols-4 mb-4">
<TabsTrigger value="roles">Roles</TabsTrigger>
<TabsTrigger value="clusterroles">ClusterRoles</TabsTrigger>
<TabsTrigger value="rolebindings">RoleBindings</TabsTrigger>
<TabsTrigger value="clusterrolebindings">ClusterRoleBindings</TabsTrigger>
</TabsList>
<div className="flex-1 overflow-hidden">
<TabsContent value="roles" className="h-full flex flex-col">
<div className="mb-4 flex items-center gap-2">
<Input
placeholder="New role name"
value={newRoleName}
onChange={(e) => setNewRoleName(e.target.value)}
/>
<Button disabled={!newRoleName}>
<Plus className="w-4 h-4 mr-2" />
Create Role
</Button>
</div>
<div className="flex-1 overflow-hidden">
<div className="bg-card rounded-lg border flex flex-col h-full">
<div className="border-b px-6 py-4">
<h3 className="font-semibold">Role YAML Editor</h3>
</div>
<div className="flex-1 bg-slate-900 p-4 font-mono text-sm text-green-400 overflow-auto">
<div className="space-y-1">
<div>
<span className="text-blue-400">apiVersion:</span> rbac.authorization.k8s.io/v1
</div>
<div>
<span className="text-blue-400">kind:</span> Role
</div>
<div>
<span className="text-blue-400">metadata:</span>
</div>
<div className="pl-4">
<span className="text-blue-400">name:</span> {newRoleName || "role-name"}
</div>
<div className="pl-4">
<span className="text-blue-400">namespace:</span> {namespace}
</div>
<div>
<span className="text-blue-400">rules:</span>
</div>
<div className="pl-4">
<span className="text-blue-400">-</span> <span className="text-blue-400">apiGroups:</span> [""]
</div>
<div className="pl-6">
<span className="text-blue-400">resources:</span> ["pods"]
</div>
<div className="pl-6">
<span className="text-blue-400">verbs:</span> ["get", "list", "watch"]
</div>
</div>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="clusterroles" className="h-full flex flex-col">
<div className="text-center py-12 text-muted-foreground">
<p>ClusterRole editing would be displayed here</p>
</div>
</TabsContent>
<TabsContent value="rolebindings" className="h-full flex flex-col">
<div className="text-center py-12 text-muted-foreground">
<p>RoleBinding editing would be displayed here</p>
</div>
</TabsContent>
<TabsContent value="clusterrolebindings" className="h-full flex flex-col">
<div className="text-center py-12 text-muted-foreground">
<p>ClusterRoleBinding editing would be displayed here</p>
</div>
</TabsContent>
</div>
</Tabs>
</div>
);
}

View File

@ -0,0 +1,196 @@
import React from "react";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
import { Button } from "@/components/ui";
import { Plus, Shield, User } from "lucide-react";
interface RbacViewerProps {
clusterId: string;
namespace: string;
}
export function RbacViewer({ clusterId, namespace }: RbacViewerProps) {
return (
<div className="h-full overflow-y-auto">
<div className="mb-6 flex items-center justify-between">
<div>
<h2 className="text-2xl font-semibold">RBAC Management</h2>
<p className="text-muted-foreground">Cluster ID: {clusterId} | Namespace: {namespace}</p>
</div>
<Button>
<Plus className="w-4 h-4 mr-2" />
Create Role
</Button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-card rounded-lg border">
<div className="border-b px-6 py-4">
<h3 className="font-semibold flex items-center gap-2">
<Shield className="w-5 h-5" />
Roles
</h3>
</div>
<div className="p-6">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Namespace</TableHead>
<TableHead>Rules</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>pod-reader</TableCell>
<TableCell className="font-mono">{namespace}</TableCell>
<TableCell>get, list, watch pods</TableCell>
<TableCell>
<Button variant="ghost" size="sm">Edit</Button>
</TableCell>
</TableRow>
<TableRow>
<TableCell>secret-viewer</TableCell>
<TableCell className="font-mono">{namespace}</TableCell>
<TableCell>get, list secrets</TableCell>
<TableCell>
<Button variant="ghost" size="sm">Edit</Button>
</TableCell>
</TableRow>
<TableRow>
<TableCell>deployment-manager</TableCell>
<TableCell className="font-mono">{namespace}</TableCell>
<TableCell>get, list, create, update deployments</TableCell>
<TableCell>
<Button variant="ghost" size="sm">Edit</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</div>
<div className="bg-card rounded-lg border">
<div className="border-b px-6 py-4">
<h3 className="font-semibold flex items-center gap-2">
<Shield className="w-5 h-5" />
ClusterRoles
</h3>
</div>
<div className="p-6">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Rules</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>admin</TableCell>
<TableCell>Full access to all resources</TableCell>
<TableCell>
<Button variant="ghost" size="sm">Edit</Button>
</TableCell>
</TableRow>
<TableRow>
<TableCell>edit</TableCell>
<TableCell>Modify resources in namespace</TableCell>
<TableCell>
<Button variant="ghost" size="sm">Edit</Button>
</TableCell>
</TableRow>
<TableRow>
<TableCell>view</TableCell>
<TableCell>Read-only access to resources</TableCell>
<TableCell>
<Button variant="ghost" size="sm">Edit</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</div>
<div className="bg-card rounded-lg border">
<div className="border-b px-6 py-4">
<h3 className="font-semibold flex items-center gap-2">
<User className="w-5 h-5" />
RoleBindings
</h3>
</div>
<div className="p-6">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Role</TableHead>
<TableHead>Subjects</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>pod-reader-binding</TableCell>
<TableCell>pod-reader</TableCell>
<TableCell>user:alice</TableCell>
<TableCell>
<Button variant="ghost" size="sm">Edit</Button>
</TableCell>
</TableRow>
<TableRow>
<TableCell>deployment-manager-binding</TableCell>
<TableCell>deployment-manager</TableCell>
<TableCell>group:devs</TableCell>
<TableCell>
<Button variant="ghost" size="sm">Edit</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</div>
<div className="bg-card rounded-lg border">
<div className="border-b px-6 py-4">
<h3 className="font-semibold flex items-center gap-2">
<User className="w-5 h-5" />
ClusterRoleBindings
</h3>
</div>
<div className="p-6">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>ClusterRole</TableHead>
<TableHead>Subjects</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>admin-binding</TableCell>
<TableCell>admin</TableCell>
<TableCell>group:admins</TableCell>
<TableCell>
<Button variant="ghost" size="sm">Edit</Button>
</TableCell>
</TableRow>
<TableRow>
<TableCell>view-binding</TableCell>
<TableCell>view</TableCell>
<TableCell>group:auditors</TableCell>
<TableCell>
<Button variant="ghost" size="sm">Edit</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,122 @@
import React from "react";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui";
import { Badge } from "@/components/ui";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui";
import { Button } from "@/components/ui";
import { X } from "lucide-react";
import { YamlEditor } from "./YamlEditor";
interface SecretDetailProps {
secretName: string;
namespace: string;
_clusterId: string;
onClose: () => void;
}
export function SecretDetail({ secretName, namespace, _clusterId, onClose }: SecretDetailProps) {
const [activeTab, setActiveTab] = React.useState("data");
const [showValues, setShowValues] = React.useState(false);
return (
<div className="h-full flex flex-col">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<h2 className="text-xl font-semibold">Secret: {secretName}</h2>
<Badge variant="destructive">Secret</Badge>
</div>
<Button variant="ghost" size="sm" onClick={onClose}>
<X className="w-4 h-4" />
</Button>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid grid-cols-3 mb-4">
<TabsTrigger value="data">Data</TabsTrigger>
<TabsTrigger value="yaml">YAML</TabsTrigger>
<TabsTrigger value="metadata">Metadata</TabsTrigger>
</TabsList>
<div className="flex-1 overflow-hidden">
<TabsContent value="data" className="h-full overflow-y-auto">
<Card className="h-full flex flex-col">
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Secret Data</CardTitle>
<Button variant="outline" size="sm" onClick={() => setShowValues(!showValues)}>
{showValues ? "Hide Values" : "Show Values"}
</Button>
</div>
</CardHeader>
<CardContent className="flex-1 bg-slate-900 rounded-md p-4 overflow-auto font-mono text-sm">
<div className="space-y-2">
<div>
<span className="text-blue-400">username:</span>
<span className="text-green-400 ml-2">
{showValues ? "admin" : "****"}
</span>
</div>
<div>
<span className="text-blue-400">password:</span>
<span className="text-green-400 ml-2">
{showValues ? "secret123" : "****"}
</span>
</div>
<div>
<span className="text-blue-400">api-key:</span>
<span className="text-green-400 ml-2">
{showValues ? "sk-abc123xyz" : "****"}
</span>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="yaml" className="h-full">
<YamlEditor onChange={() => {}} />
</TabsContent>
<TabsContent value="metadata" className="h-full overflow-y-auto">
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Metadata</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Name</span>
<span className="font-mono">{secretName}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Namespace</span>
<span className="font-mono">{namespace}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Type</span>
<Badge variant="secondary">Opaque</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Created</span>
<span className="text-sm">2 hours ago</span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Labels</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
<Badge variant="secondary">app=web</Badge>
<Badge variant="secondary">tier=frontend</Badge>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
</div>
</Tabs>
</div>
);
}

View File

@ -0,0 +1,157 @@
import React from "react";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui";
import { Badge } from "@/components/ui";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
import { Button } from "@/components/ui";
import { X } from "lucide-react";
import { YamlEditor } from "./YamlEditor";
interface ServiceDetailProps {
serviceName: string;
namespace: string;
_clusterId: string;
onClose: () => void;
}
export function ServiceDetail({ serviceName, namespace, _clusterId, onClose }: ServiceDetailProps) {
const [activeTab, setActiveTab] = React.useState("overview");
return (
<div className="h-full flex flex-col">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<h2 className="text-xl font-semibold">Service: {serviceName}</h2>
<Badge variant="outline">{namespace}</Badge>
</div>
<Button variant="ghost" size="sm" onClick={onClose}>
<X className="w-4 h-4" />
</Button>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid grid-cols-4 mb-4">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="endpoints">Endpoints</TabsTrigger>
<TabsTrigger value="yaml">YAML</TabsTrigger>
<TabsTrigger value="events">Events</TabsTrigger>
</TabsList>
<div className="flex-1 overflow-hidden">
<TabsContent value="overview" className="h-full overflow-y-auto">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle>Service Information</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Name</span>
<span className="font-mono">{serviceName}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Namespace</span>
<span className="font-mono">{namespace}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Type</span>
<Badge variant="secondary">ClusterIP</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Cluster IP</span>
<span className="font-mono">10.96.0.1</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">External IP</span>
<span className="text-muted-foreground">none</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Port</span>
<span>80/TCP</span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Selector</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
<Badge variant="secondary">app=web</Badge>
</div>
</CardContent>
</Card>
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle>Labels</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
<Badge variant="secondary">app=web</Badge>
<Badge variant="secondary">tier=frontend</Badge>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="endpoints" className="h-full overflow-y-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>IP</TableHead>
<TableHead>Port</TableHead>
<TableHead>Node</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>10.0.0.1</TableCell>
<TableCell>80</TableCell>
<TableCell>node-1</TableCell>
</TableRow>
<TableRow>
<TableCell>10.0.0.2</TableCell>
<TableCell>80</TableCell>
<TableCell>node-2</TableCell>
</TableRow>
<TableRow>
<TableCell>10.0.0.3</TableCell>
<TableCell>80</TableCell>
<TableCell>node-3</TableCell>
</TableRow>
</TableBody>
</Table>
</TabsContent>
<TabsContent value="yaml" className="h-full">
<YamlEditor onChange={() => {}} />
</TabsContent>
<TabsContent value="events" className="h-full overflow-y-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Time</TableHead>
<TableHead>Reason</TableHead>
<TableHead>Type</TableHead>
<TableHead>Message</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>2 hours ago</TableCell>
<TableCell>SettingClusterIP</TableCell>
<TableCell>Normal</TableCell>
<TableCell>Assigned cluster IP 10.96.0.1</TableCell>
</TableRow>
</TableBody>
</Table>
</TabsContent>
</div>
</Tabs>
</div>
);
}

View File

@ -0,0 +1,66 @@
import React from "react";
import { X, AlertCircle, CheckCircle, Info, AlertTriangle } from "lucide-react";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui";
import { Button } from "@/components/ui";
interface ToastProps {
message: string;
type?: "success" | "error" | "info" | "warning";
duration?: number;
onClose: () => void;
}
export function Toast({ message, type = "info", duration = 5000, onClose }: ToastProps) {
const [visible, setVisible] = React.useState(true);
React.useEffect(() => {
if (duration > 0) {
const timer = setTimeout(() => {
setVisible(false);
setTimeout(onClose, 300);
}, duration);
return () => clearTimeout(timer);
}
}, [duration, onClose]);
const icons = {
success: <CheckCircle className="w-5 h-5 text-green-500" />,
error: <AlertCircle className="w-5 h-5 text-red-500" />,
info: <Info className="w-5 h-5 text-blue-500" />,
warning: <AlertTriangle className="w-5 h-5 text-yellow-500" />,
};
const bgColors = {
success: "bg-green-50 border-green-200",
error: "bg-red-50 border-red-200",
info: "bg-blue-50 border-blue-200",
warning: "bg-yellow-50 border-yellow-200",
};
return (
<div
className={`fixed top-4 right-4 z-50 w-full max-w-sm transition-all duration-300 transform ${
visible ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"
}`}
>
<Card className={`border-l-4 ${bgColors[type]} shadow-lg`}>
<CardHeader className="pb-2">
<div className="flex items-start justify-between">
<div className="flex items-center gap-2">
{icons[type]}
<CardTitle className="text-sm font-medium">
{type.charAt(0).toUpperCase() + type.slice(1)}
</CardTitle>
</div>
<Button variant="ghost" size="sm" onClick={() => setVisible(false)}>
<X className="w-4 h-4" />
</Button>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">{message}</p>
</CardContent>
</Card>
</div>
);
}

View File

@ -31,3 +31,17 @@ export { SearchBar } from "./SearchBar";
export { ContextSwitcher } from "./ContextSwitcher";
export { ApplicationView } from "./ApplicationView";
export { PodDetail } from "./PodDetail";
export { DeploymentDetail } from "./DeploymentDetail";
export { ServiceDetail } from "./ServiceDetail";
export { ConfigMapDetail } from "./ConfigMapDetail";
export { SecretDetail } from "./SecretDetail";
export { ClusterOverview } from "./ClusterOverview";
export { ClusterDetails } from "./ClusterDetails";
export { Hotbar } from "./Hotbar";
export { CommandPalette } from "./CommandPalette";
export { Toast } from "./Toast";
export { LoadingSpinner } from "./LoadingSpinner";
export { CreateResourceModal } from "./CreateResourceModal";
export { EditResourceModal } from "./EditResourceModal";
export { RbacViewer } from "./RbacViewer";
export { RbacEditor } from "./RbacEditor";