feat(kubernetes): implement Phase 3 - detail views and cluster management
- Add detail views: PodDetail, DeploymentDetail, ServiceDetail, ConfigMapDetail, SecretDetail - Add cluster management views: ClusterOverview, ClusterDetails - Add UX components: Hotbar, CommandPalette, Toast, LoadingSpinner - Add resource management: CreateResourceModal, EditResourceModal - Add RBAC management: RbacViewer, RbacEditor - Update index.tsx exports for all new components - All components pass ESLint, TypeScript, and pass 114 tests - Build successful
This commit is contained in:
parent
a3da4f5ce7
commit
512feb5e49
181
src/components/Kubernetes/ClusterDetails.tsx
Normal file
181
src/components/Kubernetes/ClusterDetails.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
148
src/components/Kubernetes/ClusterOverview.tsx
Normal file
148
src/components/Kubernetes/ClusterOverview.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
103
src/components/Kubernetes/CommandPalette.tsx
Normal file
103
src/components/Kubernetes/CommandPalette.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
111
src/components/Kubernetes/ConfigMapDetail.tsx
Normal file
111
src/components/Kubernetes/ConfigMapDetail.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
131
src/components/Kubernetes/CreateResourceModal.tsx
Normal file
131
src/components/Kubernetes/CreateResourceModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
163
src/components/Kubernetes/DeploymentDetail.tsx
Normal file
163
src/components/Kubernetes/DeploymentDetail.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
110
src/components/Kubernetes/EditResourceModal.tsx
Normal file
110
src/components/Kubernetes/EditResourceModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
57
src/components/Kubernetes/Hotbar.tsx
Normal file
57
src/components/Kubernetes/Hotbar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
22
src/components/Kubernetes/LoadingSpinner.tsx
Normal file
22
src/components/Kubernetes/LoadingSpinner.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
116
src/components/Kubernetes/RbacEditor.tsx
Normal file
116
src/components/Kubernetes/RbacEditor.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
196
src/components/Kubernetes/RbacViewer.tsx
Normal file
196
src/components/Kubernetes/RbacViewer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
122
src/components/Kubernetes/SecretDetail.tsx
Normal file
122
src/components/Kubernetes/SecretDetail.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
157
src/components/Kubernetes/ServiceDetail.tsx
Normal file
157
src/components/Kubernetes/ServiceDetail.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
66
src/components/Kubernetes/Toast.tsx
Normal file
66
src/components/Kubernetes/Toast.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user