feat: 定时任务编辑入口全栈打通 + 修复ApprovalFlow编译错误 + 完成大文件拆分重构

- CronJobManager: 编辑按钮接通 onClick → openEdit(job),弹出编辑对话框预填数据,
  复用新建/编辑共用 Dialog(editingId 区分模式),标题/按钮文案动态切换,
  保存时根据 editingId 调用 createJob 或 updateJob — 消除占位按钮
- ApprovalFlow: 修复 API 方法名不匹配导致 tsc -b 编译失败
  (getInstances→getProcesses, getMyApprovals→getProcesses({status:'pending'}),
   approveInstance→approveProcess, rejectInstance→rejectProcess)
  + 添加 ApprovalFlowManager 导出别名兼容 App.tsx 导入
- 合并已完成的大文件拆分重构: automation.go→automation_cron/script/playbook,
  ldap.go→ldap_ou/ldap_users, server.go→cloud_accounts/domain/platform/region,
  user.go→user_admin; 前端 Dashboard/K8s/Server/Approval/Monitor 子组件拆分
  // Logan.Li
This commit is contained in:
attacker committed 2026-06-15 01:09:49 +08:00
1 parent 8be63ac168
commit a7e2351ad2
37 files changed
+7374 -4891

No files matched your search

@@ -0,0 +1,489 @@
import { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Cloud, XCircle } from 'lucide-react';
import { k8sApi } from '@/services/api';
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from 'recharts';
// 模拟资源使用趋势数据
const resourceData = [
{ time: '00:00', cpu: 45, memory: 52 },
{ time: '04:00', cpu: 38, memory: 48 },
{ time: '08:00', cpu: 62, memory: 68 },
{ time: '12:00', cpu: 78, memory: 82 },
{ time: '16:00', cpu: 72, memory: 76 },
{ time: '20:00', cpu: 58, memory: 64 },
{ time: '24:00', cpu: 48, memory: 56 },
];
export function K8sClusterDetail({ cluster, onClose }: { cluster: any; onClose: () => void }) {
const [nodes, setNodes] = useState<any[]>([]);
const [pods, setPods] = useState<any[]>([]);
const [deployments, setDeployments] = useState<any[]>([]);
const [services, setServices] = useState<any[]>([]);
const [ingresses, setIngresses] = useState<any[]>([]);
const loadClusterDetail = async (clusterId: string) => {
try {
// 后端 ListNodes/ListPods/etc 返回 data 直接为数组(非 {list,total})
const [nodesRes, podsRes, depRes, svcRes, ingRes] = await Promise.all([
k8sApi.getNodes(clusterId).catch(() => []),
k8sApi.getPods(clusterId).catch(() => []),
k8sApi.getDeployments(clusterId).catch(() => []),
k8sApi.getServices(clusterId).catch(() => []),
k8sApi.getIngresses(clusterId).catch(() => []),
]);
const toArray = (v: any) => (Array.isArray(v) ? v : (v?.list || []));
setNodes(toArray(nodesRes));
setPods(toArray(podsRes));
setDeployments(toArray(depRes));
setServices(toArray(svcRes));
setIngresses(toArray(ingRes));
} catch (e) {
console.error('加载集群详情失败:', e);
}
};
useEffect(() => {
if (cluster?.id) {
loadClusterDetail(cluster.id);
}
}, [cluster?.id]);
return (
<Card className="bg-slate-900 border-slate-800">
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Cloud className="h-6 w-6 text-blue-400" />
<div>
<CardTitle className="text-slate-100">{cluster.name}</CardTitle>
<CardDescription className="text-slate-500">
{cluster.version} · {cluster.node_count || 0} 节点 · 创建于{' '}
{cluster.created_at ? new Date(cluster.created_at).toLocaleDateString() : '-'}
</CardDescription>
</div>
</div>
<Button
variant="ghost"
size="sm"
className="text-slate-400 hover:text-slate-100"
onClick={onClose}
>
<XCircle className="h-4 w-4 mr-2" />
关闭
</Button>
</div>
</CardHeader>
<CardContent>
<Tabs defaultValue="overview" className="w-full">
<TabsList className="bg-slate-800 border border-slate-700 flex-wrap h-auto">
<TabsTrigger value="overview" className="data-[state=active]:bg-slate-700">
概览
</TabsTrigger>
<TabsTrigger value="nodes" className="data-[state=active]:bg-slate-700">
节点 ({nodes.length})
</TabsTrigger>
<TabsTrigger value="pods" className="data-[state=active]:bg-slate-700">
Pod ({pods.length})
</TabsTrigger>
<TabsTrigger value="deployments" className="data-[state=active]:bg-slate-700">
Deployment ({deployments.length})
</TabsTrigger>
<TabsTrigger value="services" className="data-[state=active]:bg-slate-700">
Service ({services.length})
</TabsTrigger>
<TabsTrigger value="ingresses" className="data-[state=active]:bg-slate-700">
Ingress ({ingresses.length})
</TabsTrigger>
<TabsTrigger value="events" className="data-[state=active]:bg-slate-700">
事件
</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="mt-4 space-y-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader className="pb-2">
<CardTitle className="text-sm text-slate-400">资源使用趋势</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[200px]">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={resourceData}>
<defs>
<linearGradient id="colorCpu" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.3} />
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
</linearGradient>
<linearGradient id="colorMemory" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#10b981" stopOpacity={0.3} />
<stop offset="95%" stopColor="#10b981" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
<XAxis dataKey="time" stroke="#475569" fontSize={12} />
<YAxis stroke="#475569" fontSize={12} />
<Tooltip
contentStyle={{
backgroundColor: '#0f172a',
border: '1px solid #1e293b',
borderRadius: '6px',
color: '#f8fafc',
}}
/>
<Area
type="monotone"
dataKey="cpu"
stroke="#3b82f6"
fillOpacity={1}
fill="url(#colorCpu)"
name="CPU"
/>
<Area
type="monotone"
dataKey="memory"
stroke="#10b981"
fillOpacity={1}
fill="url(#colorMemory)"
name="内存"
/>
</AreaChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader className="pb-2">
<CardTitle className="text-sm text-slate-400">资源分布</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div className="text-center p-4 rounded-lg bg-slate-800">
<div className="text-3xl font-bold text-blue-400">{cluster.pod_count || 0}</div>
<div className="text-sm text-slate-500 mt-1">Pod</div>
</div>
<div className="text-center p-4 rounded-lg bg-slate-800">
<div className="text-3xl font-bold text-emerald-400">{cluster.service_count || 0}</div>
<div className="text-sm text-slate-500 mt-1">Service</div>
</div>
<div className="text-center p-4 rounded-lg bg-slate-800">
<div className="text-3xl font-bold text-purple-400">{cluster.deployment_count || 0}</div>
<div className="text-sm text-slate-500 mt-1">Deployment</div>
</div>
<div className="text-center p-4 rounded-lg bg-slate-800">
<div className="text-3xl font-bold text-amber-400">{cluster.node_count || 0}</div>
<div className="text-sm text-slate-500 mt-1">节点</div>
</div>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="nodes" className="mt-4">
<div className="border rounded-lg border-slate-700 overflow-hidden">
<table className="w-full">
<thead className="bg-slate-800">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">名称</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">角色</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">状态</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">CPU</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">内存</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">Pod</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">操作系统</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{nodes.length === 0 ? (
<tr>
<td colSpan={7} className="px-4 py-8 text-center text-slate-500">暂无节点数据</td>
</tr>
) : (
nodes.map((node) => (
<tr key={node.id || node.name} className="hover:bg-slate-800/50">
<td className="px-4 py-3 text-sm text-slate-200">{node.name}</td>
<td className="px-4 py-3">
<Badge
variant="outline"
className={
node.role === 'master'
? 'border-purple-500/30 text-purple-400 bg-purple-500/10'
: 'border-blue-500/30 text-blue-400 bg-blue-500/10'
}
>
{node.role === 'master' ? 'Master' : 'Worker'}
</Badge>
</td>
<td className="px-4 py-3">
<Badge
variant="outline"
className={
(node.status || '').toLowerCase() === 'ready'
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
: 'border-red-500/30 text-red-400 bg-red-500/10'
}
>
{(node.status || '').toLowerCase() === 'ready' ? 'Ready' : 'NotReady'}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-slate-300">
{Number(node.cpu_usage || 0).toFixed(1)}% / {node.cpu}核
</td>
<td className="px-4 py-3 text-sm text-slate-300">
{Number(node.memory_usage || 0).toFixed(1)}% / {node.memory}GB
</td>
<td className="px-4 py-3 text-sm text-slate-300">{node.pods ?? node.pod_count ?? 0}</td>
<td className="px-4 py-3 text-sm text-slate-400">{node.os}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</TabsContent>
<TabsContent value="pods" className="mt-4">
<div className="border rounded-lg border-slate-700 overflow-hidden">
<table className="w-full">
<thead className="bg-slate-800">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">名称</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">命名空间</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">状态</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">重启次数</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">CPU</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">内存</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">节点</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{pods.length === 0 ? (
<tr>
<td colSpan={7} className="px-4 py-8 text-center text-slate-500">暂无Pod数据</td>
</tr>
) : (
pods.map((pod) => (
<tr key={pod.id || pod.name} className="hover:bg-slate-800/50">
<td className="px-4 py-3 text-sm text-slate-200">{pod.name}</td>
<td className="px-4 py-3">
<Badge variant="outline" className="border-slate-700 text-slate-400">
{pod.namespace}
</Badge>
</td>
<td className="px-4 py-3">
<Badge
variant="outline"
className={
(pod.status || '').toLowerCase() === 'running'
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
: (pod.status || '').toLowerCase() === 'pending'
? 'border-amber-500/30 text-amber-400 bg-amber-500/10'
: 'border-red-500/30 text-red-400 bg-red-500/10'
}
>
{pod.status}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-slate-300">{pod.restart_count ?? 0}</td>
<td className="px-4 py-3 text-sm text-slate-300">{Number(pod.cpu_usage || 0).toFixed(1)}m</td>
<td className="px-4 py-3 text-sm text-slate-300">{Number(pod.memory_usage || 0).toFixed(0)}Mi</td>
<td className="px-4 py-3 text-sm text-slate-400">{pod.node_name}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</TabsContent>
<TabsContent value="deployments" className="mt-4">
<div className="border rounded-lg border-slate-700 overflow-hidden">
<table className="w-full">
<thead className="bg-slate-800">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">名称</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">命名空间</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">副本数</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">可用副本</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">镜像</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">创建时间</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{deployments.length === 0 ? (
<tr>
<td colSpan={6} className="px-4 py-8 text-center text-slate-500">暂无Deployment数据</td>
</tr>
) : (
deployments.map((deployment) => (
<tr key={deployment.id || deployment.name} className="hover:bg-slate-800/50">
<td className="px-4 py-3 text-sm text-slate-200">{deployment.name}</td>
<td className="px-4 py-3">
<Badge variant="outline" className="border-slate-700 text-slate-400">
{deployment.namespace}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-slate-300">{deployment.replicas}</td>
<td className="px-4 py-3">
<span className={`text-sm ${(deployment.available_replicas ?? deployment.availableReplicas) === deployment.replicas ? 'text-emerald-400' : 'text-amber-400'}`}>
{deployment.available_replicas ?? deployment.availableReplicas} / {deployment.replicas}
</span>
</td>
<td className="px-4 py-3 text-sm text-slate-400 truncate max-w-[200px]">{deployment.image}</td>
<td className="px-4 py-3 text-sm text-slate-400">{deployment.created_at || deployment.createdAt}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</TabsContent>
<TabsContent value="services" className="mt-4">
<div className="border rounded-lg border-slate-700 overflow-hidden">
<table className="w-full">
<thead className="bg-slate-800">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">名称</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">命名空间</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">类型</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">ClusterIP</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">端口</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">Selector</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{services.length === 0 ? (
<tr>
<td colSpan={6} className="px-4 py-8 text-center text-slate-500">暂无Service数据</td>
</tr>
) : (
services.map((service) => (
<tr key={service.id || service.name} className="hover:bg-slate-800/50">
<td className="px-4 py-3 text-sm text-slate-200">{service.name}</td>
<td className="px-4 py-3">
<Badge variant="outline" className="border-slate-700 text-slate-400">
{service.namespace}
</Badge>
</td>
<td className="px-4 py-3">
<Badge
variant="outline"
className={
service.type === 'ClusterIP'
? 'border-blue-500/30 text-blue-400 bg-blue-500/10'
: service.type === 'NodePort'
? 'border-purple-500/30 text-purple-400 bg-purple-500/10'
: 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
}
>
{service.type}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-slate-300">{service.cluster_ip ?? service.clusterIP}</td>
<td className="px-4 py-3 text-sm text-slate-300">{service.ports}</td>
<td className="px-4 py-3 text-sm text-slate-400">{service.selector}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</TabsContent>
<TabsContent value="ingresses" className="mt-4">
<div className="border rounded-lg border-slate-700 overflow-hidden">
<table className="w-full">
<thead className="bg-slate-800">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">名称</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">命名空间</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">Hosts</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">后端服务</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">TLS</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{ingresses.length === 0 ? (
<tr>
<td colSpan={5} className="px-4 py-8 text-center text-slate-500">暂无Ingress数据</td>
</tr>
) : (
ingresses.map((ingress) => (
<tr key={ingress.id || ingress.name} className="hover:bg-slate-800/50">
<td className="px-4 py-3 text-sm text-slate-200">{ingress.name}</td>
<td className="px-4 py-3">
<Badge variant="outline" className="border-slate-700 text-slate-400">
{ingress.namespace}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-slate-300">{ingress.hosts}</td>
<td className="px-4 py-3 text-sm text-slate-300">{ingress.backend}</td>
<td className="px-4 py-3">
<Badge
variant="outline"
className={
ingress.tls
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
: 'border-slate-700 text-slate-400'
}
>
{ingress.tls ? '已启用' : '未启用'}
</Badge>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</TabsContent>
<TabsContent value="events" className="mt-4">
<div className="space-y-2">
{[
{ type: 'Normal', reason: 'Started', message: 'Started container nginx', time: '2分钟前' },
{ type: 'Normal', reason: 'Created', message: 'Created pod nginx-deployment-xxx', time: '5分钟前' },
{ type: 'Warning', reason: 'FailedScheduling', message: '0/3 nodes are available', time: '10分钟前' },
{ type: 'Normal', reason: 'Pulling', message: 'Pulling image "nginx:latest"', time: '15分钟前' },
{ type: 'Normal', reason: 'Pulled', message: 'Successfully pulled image', time: '12分钟前' },
].map((event, index) => (
<div
key={index}
className="flex items-center gap-3 p-3 rounded-lg bg-slate-800/50"
>
<Badge
variant="outline"
className={
event.type === 'Normal'
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
: 'border-amber-500/30 text-amber-400 bg-amber-500/10'
}
>
{event.type}
</Badge>
<span className="text-sm font-medium text-slate-300 min-w-[120px]">{event.reason}</span>
<span className="text-sm text-slate-400 flex-1">{event.message}</span>
<span className="text-xs text-slate-500">{event.time}</span>
</div>
))}
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
);
}
@@ -0,0 +1,218 @@
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { k8sApi } from '@/services/api';
const emptyForm = {
name: '',
version: '',
description: '',
config_type: 1, // 0=Token, 1=Kubeconfig
api_server: '',
token: '',
kube_config: '',
};
export function K8sClusterDialog({
open,
onOpenChange,
editingCluster,
onSaved,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
editingCluster: any;
onSaved: () => void;
}) {
const [formData, setFormData] = useState({ ...emptyForm });
const [submitting, setSubmitting] = useState(false);
// 打开时同步表单(编辑时填充,新建时重置)
useEffect(() => {
if (!open) return;
if (editingCluster) {
setFormData({
name: editingCluster.name || '',
version: editingCluster.version || '',
description: editingCluster.description || '',
config_type: Number(editingCluster.config_type ?? 1),
api_server: editingCluster.api_server || '',
token: editingCluster.token || '',
kube_config: editingCluster.kube_config || '',
});
} else {
setFormData({ ...emptyForm });
}
}, [open, editingCluster]);
// 从 kubeconfig YAML 中解析 server 地址
const parseApiServer = (yaml: string): string => {
const match = yaml.match(/^\s*server:\s*(\S+)\s*$/m);
return match ? match[1] : '';
};
const handleKubeConfigChange = (value: string) => {
const server = parseApiServer(value);
setFormData((prev) => ({
...prev,
kube_config: value,
api_server: server || prev.api_server,
}));
};
const handleSubmit = async () => {
if (!formData.name.trim()) return;
try {
setSubmitting(true);
const payload = {
name: formData.name,
version: formData.version,
description: formData.description,
config_type: formData.config_type,
api_server: formData.api_server,
token: formData.config_type === 0 ? formData.token : '',
kube_config: formData.config_type === 1 ? formData.kube_config : '',
};
if (editingCluster) {
await k8sApi.updateCluster(editingCluster.id, payload);
} else {
await k8sApi.createCluster(payload);
}
onOpenChange(false);
onSaved();
} catch (e) {
console.error('保存集群失败:', e);
} finally {
setSubmitting(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-w-2xl">
<DialogHeader>
<DialogTitle className="text-[var(--dg-text-primary)]">
{editingCluster ? '编辑集群' : '添加集群'}
</DialogTitle>
<DialogDescription className="text-[var(--dg-text-secondary)]">
{editingCluster ? '修改集群接入配置' : '配置新的 Kubernetes 集群接入信息'}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-2 max-h-[60vh] overflow-y-auto pr-1">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-[var(--dg-text-secondary)]">集群名称</Label>
<Input
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="例如:prod-cluster"
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
/>
</div>
<div className="space-y-2">
<Label className="text-[var(--dg-text-secondary)]">版本(可选)</Label>
<Input
value={formData.version}
onChange={(e) => setFormData({ ...formData, version: e.target.value })}
placeholder="例如:1.28.0"
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
/>
</div>
</div>
<div className="space-y-2">
<Label className="text-[var(--dg-text-secondary)]">描述</Label>
<Input
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="集群用途描述"
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
/>
</div>
<div className="space-y-2">
<Label className="text-[var(--dg-text-secondary)]">接入方式</Label>
<RadioGroup
value={String(formData.config_type)}
onValueChange={(v) => setFormData({ ...formData, config_type: Number(v) })}
className="flex gap-4"
>
<div className="flex items-center gap-2 px-3 py-2 rounded-md border border-[var(--dg-border)] cursor-pointer hover:bg-[var(--dg-bg-hover)]">
<RadioGroupItem value="0" id="ct-0" />
<Label htmlFor="ct-0" className="cursor-pointer text-[var(--dg-text-primary)] font-normal">Token</Label>
</div>
<div className="flex items-center gap-2 px-3 py-2 rounded-md border border-[var(--dg-border)] cursor-pointer hover:bg-[var(--dg-bg-hover)]">
<RadioGroupItem value="1" id="ct-1" />
<Label htmlFor="ct-1" className="cursor-pointer text-[var(--dg-text-primary)] font-normal">Kubeconfig</Label>
</div>
</RadioGroup>
</div>
<div className="space-y-2">
<Label className="text-[var(--dg-text-secondary)]">API Server</Label>
<Input
value={formData.api_server}
onChange={(e) => setFormData({ ...formData, api_server: e.target.value })}
placeholder="https://10.0.0.1:6443"
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
/>
</div>
{formData.config_type === 0 ? (
<div className="space-y-2">
<Label className="text-[var(--dg-text-secondary)]">Token</Label>
<Textarea
value={formData.token}
onChange={(e) => setFormData({ ...formData, token: e.target.value })}
placeholder="粘贴 Service Account Token..."
rows={4}
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)] font-mono text-xs"
/>
</div>
) : (
<div className="space-y-2">
<Label className="text-[var(--dg-text-secondary)]">Kubeconfig</Label>
<Textarea
value={formData.kube_config}
onChange={(e) => handleKubeConfigChange(e.target.value)}
placeholder={'apiVersion: v1\nkind: Config\nclusters:\n- cluster:\n server: https://10.0.0.1:6443\n ...'}
rows={10}
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)] font-mono text-xs"
/>
<p className="text-xs text-[var(--dg-text-muted)]">
粘贴 kubeconfig 内容后将自动解析 API Server 地址
</p>
</div>
)}
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => { onOpenChange(false); }}
className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]"
>
取消
</Button>
<Button
onClick={handleSubmit}
disabled={submitting || !formData.name.trim()}
className="bg-[var(--dg-blue)] hover:opacity-90 text-white"
>
{submitting ? '保存中...' : editingCluster ? '保存' : '添加'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+13 -710
View File
@@ -3,12 +3,7 @@ import { cn } from '@/lib/utils';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
DropdownMenu,
DropdownMenuContent,
@@ -17,23 +12,12 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import {
Cloud,
Plus,
MoreHorizontal,
Server,
Cpu,
Database,
Activity,
CheckCircle,
AlertTriangle,
XCircle,
@@ -41,27 +25,13 @@ import {
Settings,
Eye,
Trash2,
Edit,
Terminal,
Search,
Filter,
} from 'lucide-react';
import { k8sApi } from '@/services/api';
import type { K8sCluster } from '@/types';
import {
LineChart,
Line,
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
PieChart,
Pie,
Cell,
} from 'recharts';
import { K8sClusterDetail } from './K8sClusterDetail';
import { K8sClusterDialog } from './K8sClusterDialog';
export function K8sClusters() {
const [clusters, setClusters] = useState<any[]>([]);
@@ -69,26 +39,9 @@ export function K8sClusters() {
const [selectedCluster, setSelectedCluster] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
// 创建/编辑集群表单
// 创建/编辑集群弹窗
const [dialogOpen, setDialogOpen] = useState(false);
const [editingCluster, setEditingCluster] = useState<any>(null);
const [submitting, setSubmitting] = useState(false);
const [formData, setFormData] = useState({
name: '',
version: '',
description: '',
config_type: 1, // 0=Token, 1=Kubeconfig
api_server: '',
token: '',
kube_config: '',
});
// 集群详情关联资源
const [nodes, setNodes] = useState<any[]>([]);
const [pods, setPods] = useState<any[]>([]);
const [deployments, setDeployments] = useState<any[]>([]);
const [services, setServices] = useState<any[]>([]);
const [ingresses, setIngresses] = useState<any[]>([]);
const loadData = async () => {
try {
@@ -107,39 +60,6 @@ export function K8sClusters() {
loadData();
}, []);
const loadClusterDetail = async (clusterId: string) => {
try {
// 后端 ListNodes/ListPods/etc 返回 data 直接为数组(非 {list,total})
const [nodesRes, podsRes, depRes, svcRes, ingRes] = await Promise.all([
k8sApi.getNodes(clusterId).catch(() => []),
k8sApi.getPods(clusterId).catch(() => []),
k8sApi.getDeployments(clusterId).catch(() => []),
k8sApi.getServices(clusterId).catch(() => []),
k8sApi.getIngresses(clusterId).catch(() => []),
]);
const toArray = (v: any) => (Array.isArray(v) ? v : (v?.list || []));
setNodes(toArray(nodesRes));
setPods(toArray(podsRes));
setDeployments(toArray(depRes));
setServices(toArray(svcRes));
setIngresses(toArray(ingRes));
} catch (e) {
console.error('加载集群详情失败:', e);
}
};
useEffect(() => {
if (selectedCluster) {
loadClusterDetail(selectedCluster);
} else {
setNodes([]);
setPods([]);
setDeployments([]);
setServices([]);
setIngresses([]);
}
}, [selectedCluster]);
const handleDelete = async (id: string) => {
try {
await k8sApi.deleteCluster(id);
@@ -149,81 +69,6 @@ export function K8sClusters() {
}
};
const resetForm = () => {
setFormData({
name: '',
version: '',
description: '',
config_type: 1,
api_server: '',
token: '',
kube_config: '',
});
setEditingCluster(null);
};
const openCreateDialog = () => {
resetForm();
setDialogOpen(true);
};
const openEditDialog = (cluster: any) => {
setEditingCluster(cluster);
setFormData({
name: cluster.name || '',
version: cluster.version || '',
description: cluster.description || '',
config_type: Number(cluster.config_type ?? 1),
api_server: cluster.api_server || '',
token: cluster.token || '',
kube_config: cluster.kube_config || '',
});
setDialogOpen(true);
};
// 从 kubeconfig YAML 中解析 server 地址
const parseApiServer = (yaml: string): string => {
const match = yaml.match(/^\s*server:\s*(\S+)\s*$/m);
return match ? match[1] : '';
};
const handleKubeConfigChange = (value: string) => {
const server = parseApiServer(value);
setFormData((prev) => ({
...prev,
kube_config: value,
api_server: server || prev.api_server,
}));
};
const handleSubmit = async () => {
if (!formData.name.trim()) return;
try {
setSubmitting(true);
const payload = {
name: formData.name,
version: formData.version,
description: formData.description,
config_type: formData.config_type,
api_server: formData.api_server,
token: formData.config_type === 0 ? formData.token : '',
kube_config: formData.config_type === 1 ? formData.kube_config : '',
};
if (editingCluster) {
await k8sApi.updateCluster(editingCluster.id, payload);
} else {
await k8sApi.createCluster(payload);
}
setDialogOpen(false);
resetForm();
loadData();
} catch (e) {
console.error('保存集群失败:', e);
} finally {
setSubmitting(false);
}
};
const filteredClusters = clusters.filter(
(cluster) =>
(cluster.name || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
@@ -260,17 +105,6 @@ export function K8sClusters() {
const clusterDetail = selectedCluster ? clusters.find((c) => c.id === selectedCluster) : null;
// 模拟资源使用趋势数据
const resourceData = [
{ time: '00:00', cpu: 45, memory: 52 },
{ time: '04:00', cpu: 38, memory: 48 },
{ time: '08:00', cpu: 62, memory: 68 },
{ time: '12:00', cpu: 78, memory: 82 },
{ time: '16:00', cpu: 72, memory: 76 },
{ time: '20:00', cpu: 58, memory: 64 },
{ time: '24:00', cpu: 48, memory: 56 },
];
return (
<div className="space-y-6">
{/* Page Header */}
@@ -288,7 +122,7 @@ export function K8sClusters() {
<RefreshCw className={cn('h-4 w-4 mr-2', loading && 'animate-spin')} />
刷新
</Button>
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={openCreateDialog}>
<Button className="bg-blue-600 hover:bg-blue-700 text-white" onClick={() => { setEditingCluster(null); setDialogOpen(true); }}>
<Plus className="h-4 w-4 mr-2" />
添加集群
</Button>
@@ -437,7 +271,8 @@ export function K8sClusters() {
className="text-slate-300 focus:bg-slate-700 focus:text-slate-100"
onClick={(e) => {
e.stopPropagation();
openEditDialog(cluster);
setEditingCluster(cluster);
setDialogOpen(true);
}}
>
<Settings className="h-4 w-4 mr-2" />
@@ -515,548 +350,16 @@ export function K8sClusters() {
{/* Cluster Detail */}
{clusterDetail && (
<Card className="bg-slate-900 border-slate-800">
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Cloud className="h-6 w-6 text-blue-400" />
<div>
<CardTitle className="text-slate-100">{clusterDetail.name}</CardTitle>
<CardDescription className="text-slate-500">
{clusterDetail.version} · {clusterDetail.node_count || 0} 节点 · 创建于{' '}
{clusterDetail.created_at ? new Date(clusterDetail.created_at).toLocaleDateString() : '-'}
</CardDescription>
</div>
</div>
<Button
variant="ghost"
size="sm"
className="text-slate-400 hover:text-slate-100"
onClick={() => setSelectedCluster(null)}
>
<XCircle className="h-4 w-4 mr-2" />
关闭
</Button>
</div>
</CardHeader>
<CardContent>
<Tabs defaultValue="overview" className="w-full">
<TabsList className="bg-slate-800 border border-slate-700 flex-wrap h-auto">
<TabsTrigger value="overview" className="data-[state=active]:bg-slate-700">
概览
</TabsTrigger>
<TabsTrigger value="nodes" className="data-[state=active]:bg-slate-700">
节点 ({nodes.length})
</TabsTrigger>
<TabsTrigger value="pods" className="data-[state=active]:bg-slate-700">
Pod ({pods.length})
</TabsTrigger>
<TabsTrigger value="deployments" className="data-[state=active]:bg-slate-700">
Deployment ({deployments.length})
</TabsTrigger>
<TabsTrigger value="services" className="data-[state=active]:bg-slate-700">
Service ({services.length})
</TabsTrigger>
<TabsTrigger value="ingresses" className="data-[state=active]:bg-slate-700">
Ingress ({ingresses.length})
</TabsTrigger>
<TabsTrigger value="events" className="data-[state=active]:bg-slate-700">
事件
</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="mt-4 space-y-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader className="pb-2">
<CardTitle className="text-sm text-slate-400">资源使用趋势</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[200px]">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={resourceData}>
<defs>
<linearGradient id="colorCpu" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.3} />
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
</linearGradient>
<linearGradient id="colorMemory" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#10b981" stopOpacity={0.3} />
<stop offset="95%" stopColor="#10b981" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
<XAxis dataKey="time" stroke="#475569" fontSize={12} />
<YAxis stroke="#475569" fontSize={12} />
<Tooltip
contentStyle={{
backgroundColor: '#0f172a',
border: '1px solid #1e293b',
borderRadius: '6px',
color: '#f8fafc',
}}
/>
<Area
type="monotone"
dataKey="cpu"
stroke="#3b82f6"
fillOpacity={1}
fill="url(#colorCpu)"
name="CPU"
/>
<Area
type="monotone"
dataKey="memory"
stroke="#10b981"
fillOpacity={1}
fill="url(#colorMemory)"
name="内存"
/>
</AreaChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader className="pb-2">
<CardTitle className="text-sm text-slate-400">资源分布</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div className="text-center p-4 rounded-lg bg-slate-800">
<div className="text-3xl font-bold text-blue-400">{clusterDetail.pod_count || 0}</div>
<div className="text-sm text-slate-500 mt-1">Pod</div>
</div>
<div className="text-center p-4 rounded-lg bg-slate-800">
<div className="text-3xl font-bold text-emerald-400">{clusterDetail.service_count || 0}</div>
<div className="text-sm text-slate-500 mt-1">Service</div>
</div>
<div className="text-center p-4 rounded-lg bg-slate-800">
<div className="text-3xl font-bold text-purple-400">{clusterDetail.deployment_count || 0}</div>
<div className="text-sm text-slate-500 mt-1">Deployment</div>
</div>
<div className="text-center p-4 rounded-lg bg-slate-800">
<div className="text-3xl font-bold text-amber-400">{clusterDetail.node_count || 0}</div>
<div className="text-sm text-slate-500 mt-1">节点</div>
</div>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="nodes" className="mt-4">
<div className="border rounded-lg border-slate-700 overflow-hidden">
<table className="w-full">
<thead className="bg-slate-800">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">名称</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">角色</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">状态</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">CPU</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">内存</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">Pod</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">操作系统</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{nodes.length === 0 ? (
<tr>
<td colSpan={7} className="px-4 py-8 text-center text-slate-500">暂无节点数据</td>
</tr>
) : (
nodes.map((node) => (
<tr key={node.id || node.name} className="hover:bg-slate-800/50">
<td className="px-4 py-3 text-sm text-slate-200">{node.name}</td>
<td className="px-4 py-3">
<Badge
variant="outline"
className={
node.role === 'master'
? 'border-purple-500/30 text-purple-400 bg-purple-500/10'
: 'border-blue-500/30 text-blue-400 bg-blue-500/10'
}
>
{node.role === 'master' ? 'Master' : 'Worker'}
</Badge>
</td>
<td className="px-4 py-3">
<Badge
variant="outline"
className={
(node.status || '').toLowerCase() === 'ready'
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
: 'border-red-500/30 text-red-400 bg-red-500/10'
}
>
{(node.status || '').toLowerCase() === 'ready' ? 'Ready' : 'NotReady'}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-slate-300">
{Number(node.cpu_usage || 0).toFixed(1)}% / {node.cpu}核
</td>
<td className="px-4 py-3 text-sm text-slate-300">
{Number(node.memory_usage || 0).toFixed(1)}% / {node.memory}GB
</td>
<td className="px-4 py-3 text-sm text-slate-300">{node.pods ?? node.pod_count ?? 0}</td>
<td className="px-4 py-3 text-sm text-slate-400">{node.os}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</TabsContent>
<TabsContent value="pods" className="mt-4">
<div className="border rounded-lg border-slate-700 overflow-hidden">
<table className="w-full">
<thead className="bg-slate-800">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">名称</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">命名空间</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">状态</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">重启次数</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">CPU</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">内存</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">节点</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{pods.length === 0 ? (
<tr>
<td colSpan={7} className="px-4 py-8 text-center text-slate-500">暂无Pod数据</td>
</tr>
) : (
pods.map((pod) => (
<tr key={pod.id || pod.name} className="hover:bg-slate-800/50">
<td className="px-4 py-3 text-sm text-slate-200">{pod.name}</td>
<td className="px-4 py-3">
<Badge variant="outline" className="border-slate-700 text-slate-400">
{pod.namespace}
</Badge>
</td>
<td className="px-4 py-3">
<Badge
variant="outline"
className={
(pod.status || '').toLowerCase() === 'running'
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
: (pod.status || '').toLowerCase() === 'pending'
? 'border-amber-500/30 text-amber-400 bg-amber-500/10'
: 'border-red-500/30 text-red-400 bg-red-500/10'
}
>
{pod.status}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-slate-300">{pod.restart_count ?? 0}</td>
<td className="px-4 py-3 text-sm text-slate-300">{Number(pod.cpu_usage || 0).toFixed(1)}m</td>
<td className="px-4 py-3 text-sm text-slate-300">{Number(pod.memory_usage || 0).toFixed(0)}Mi</td>
<td className="px-4 py-3 text-sm text-slate-400">{pod.node_name}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</TabsContent>
<TabsContent value="deployments" className="mt-4">
<div className="border rounded-lg border-slate-700 overflow-hidden">
<table className="w-full">
<thead className="bg-slate-800">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">名称</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">命名空间</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">副本数</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">可用副本</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">镜像</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">创建时间</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{deployments.length === 0 ? (
<tr>
<td colSpan={6} className="px-4 py-8 text-center text-slate-500">暂无Deployment数据</td>
</tr>
) : (
deployments.map((deployment) => (
<tr key={deployment.id || deployment.name} className="hover:bg-slate-800/50">
<td className="px-4 py-3 text-sm text-slate-200">{deployment.name}</td>
<td className="px-4 py-3">
<Badge variant="outline" className="border-slate-700 text-slate-400">
{deployment.namespace}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-slate-300">{deployment.replicas}</td>
<td className="px-4 py-3">
<span className={`text-sm ${(deployment.available_replicas ?? deployment.availableReplicas) === deployment.replicas ? 'text-emerald-400' : 'text-amber-400'}`}>
{deployment.available_replicas ?? deployment.availableReplicas} / {deployment.replicas}
</span>
</td>
<td className="px-4 py-3 text-sm text-slate-400 truncate max-w-[200px]">{deployment.image}</td>
<td className="px-4 py-3 text-sm text-slate-400">{deployment.created_at || deployment.createdAt}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</TabsContent>
<TabsContent value="services" className="mt-4">
<div className="border rounded-lg border-slate-700 overflow-hidden">
<table className="w-full">
<thead className="bg-slate-800">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">名称</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">命名空间</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">类型</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">ClusterIP</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">端口</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">Selector</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{services.length === 0 ? (
<tr>
<td colSpan={6} className="px-4 py-8 text-center text-slate-500">暂无Service数据</td>
</tr>
) : (
services.map((service) => (
<tr key={service.id || service.name} className="hover:bg-slate-800/50">
<td className="px-4 py-3 text-sm text-slate-200">{service.name}</td>
<td className="px-4 py-3">
<Badge variant="outline" className="border-slate-700 text-slate-400">
{service.namespace}
</Badge>
</td>
<td className="px-4 py-3">
<Badge
variant="outline"
className={
service.type === 'ClusterIP'
? 'border-blue-500/30 text-blue-400 bg-blue-500/10'
: service.type === 'NodePort'
? 'border-purple-500/30 text-purple-400 bg-purple-500/10'
: 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
}
>
{service.type}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-slate-300">{service.cluster_ip ?? service.clusterIP}</td>
<td className="px-4 py-3 text-sm text-slate-300">{service.ports}</td>
<td className="px-4 py-3 text-sm text-slate-400">{service.selector}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</TabsContent>
<TabsContent value="ingresses" className="mt-4">
<div className="border rounded-lg border-slate-700 overflow-hidden">
<table className="w-full">
<thead className="bg-slate-800">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">名称</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">命名空间</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">Hosts</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">后端服务</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">TLS</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800">
{ingresses.length === 0 ? (
<tr>
<td colSpan={5} className="px-4 py-8 text-center text-slate-500">暂无Ingress数据</td>
</tr>
) : (
ingresses.map((ingress) => (
<tr key={ingress.id || ingress.name} className="hover:bg-slate-800/50">
<td className="px-4 py-3 text-sm text-slate-200">{ingress.name}</td>
<td className="px-4 py-3">
<Badge variant="outline" className="border-slate-700 text-slate-400">
{ingress.namespace}
</Badge>
</td>
<td className="px-4 py-3 text-sm text-slate-300">{ingress.hosts}</td>
<td className="px-4 py-3 text-sm text-slate-300">{ingress.backend}</td>
<td className="px-4 py-3">
<Badge
variant="outline"
className={
ingress.tls
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
: 'border-slate-700 text-slate-400'
}
>
{ingress.tls ? '已启用' : '未启用'}
</Badge>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</TabsContent>
<TabsContent value="events" className="mt-4">
<div className="space-y-2">
{[
{ type: 'Normal', reason: 'Started', message: 'Started container nginx', time: '2分钟前' },
{ type: 'Normal', reason: 'Created', message: 'Created pod nginx-deployment-xxx', time: '5分钟前' },
{ type: 'Warning', reason: 'FailedScheduling', message: '0/3 nodes are available', time: '10分钟前' },
{ type: 'Normal', reason: 'Pulling', message: 'Pulling image "nginx:latest"', time: '15分钟前' },
{ type: 'Normal', reason: 'Pulled', message: 'Successfully pulled image', time: '12分钟前' },
].map((event, index) => (
<div
key={index}
className="flex items-center gap-3 p-3 rounded-lg bg-slate-800/50"
>
<Badge
variant="outline"
className={
event.type === 'Normal'
? 'border-emerald-500/30 text-emerald-400 bg-emerald-500/10'
: 'border-amber-500/30 text-amber-400 bg-amber-500/10'
}
>
{event.type}
</Badge>
<span className="text-sm font-medium text-slate-300 min-w-[120px]">{event.reason}</span>
<span className="text-sm text-slate-400 flex-1">{event.message}</span>
<span className="text-xs text-slate-500">{event.time}</span>
</div>
))}
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
<K8sClusterDetail cluster={clusterDetail} onClose={() => setSelectedCluster(null)} />
)}
{/* 创建/编辑集群 Dialog */}
<Dialog open={dialogOpen} onOpenChange={(o) => { setDialogOpen(o); if (!o) resetForm(); }}>
<DialogContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] max-w-2xl">
<DialogHeader>
<DialogTitle className="text-[var(--dg-text-primary)]">
{editingCluster ? '编辑集群' : '添加集群'}
</DialogTitle>
<DialogDescription className="text-[var(--dg-text-secondary)]">
{editingCluster ? '修改集群接入配置' : '配置新的 Kubernetes 集群接入信息'}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-2 max-h-[60vh] overflow-y-auto pr-1">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-[var(--dg-text-secondary)]">集群名称</Label>
<Input
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="例如:prod-cluster"
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
/>
</div>
<div className="space-y-2">
<Label className="text-[var(--dg-text-secondary)]">版本(可选)</Label>
<Input
value={formData.version}
onChange={(e) => setFormData({ ...formData, version: e.target.value })}
placeholder="例如:1.28.0"
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
/>
</div>
</div>
<div className="space-y-2">
<Label className="text-[var(--dg-text-secondary)]">描述</Label>
<Input
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="集群用途描述"
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
/>
</div>
<div className="space-y-2">
<Label className="text-[var(--dg-text-secondary)]">接入方式</Label>
<RadioGroup
value={String(formData.config_type)}
onValueChange={(v) => setFormData({ ...formData, config_type: Number(v) })}
className="flex gap-4"
>
<div className="flex items-center gap-2 px-3 py-2 rounded-md border border-[var(--dg-border)] cursor-pointer hover:bg-[var(--dg-bg-hover)]">
<RadioGroupItem value="0" id="ct-0" />
<Label htmlFor="ct-0" className="cursor-pointer text-[var(--dg-text-primary)] font-normal">Token</Label>
</div>
<div className="flex items-center gap-2 px-3 py-2 rounded-md border border-[var(--dg-border)] cursor-pointer hover:bg-[var(--dg-bg-hover)]">
<RadioGroupItem value="1" id="ct-1" />
<Label htmlFor="ct-1" className="cursor-pointer text-[var(--dg-text-primary)] font-normal">Kubeconfig</Label>
</div>
</RadioGroup>
</div>
<div className="space-y-2">
<Label className="text-[var(--dg-text-secondary)]">API Server</Label>
<Input
value={formData.api_server}
onChange={(e) => setFormData({ ...formData, api_server: e.target.value })}
placeholder="https://10.0.0.1:6443"
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]"
/>
</div>
{formData.config_type === 0 ? (
<div className="space-y-2">
<Label className="text-[var(--dg-text-secondary)]">Token</Label>
<Textarea
value={formData.token}
onChange={(e) => setFormData({ ...formData, token: e.target.value })}
placeholder="粘贴 Service Account Token..."
rows={4}
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)] font-mono text-xs"
/>
</div>
) : (
<div className="space-y-2">
<Label className="text-[var(--dg-text-secondary)]">Kubeconfig</Label>
<Textarea
value={formData.kube_config}
onChange={(e) => handleKubeConfigChange(e.target.value)}
placeholder={'apiVersion: v1\nkind: Config\nclusters:\n- cluster:\n server: https://10.0.0.1:6443\n ...'}
rows={10}
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)] font-mono text-xs"
/>
<p className="text-xs text-[var(--dg-text-muted)]">
粘贴 kubeconfig 内容后将自动解析 API Server 地址
</p>
</div>
)}
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => { setDialogOpen(false); resetForm(); }}
className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]"
>
取消
</Button>
<Button
onClick={handleSubmit}
disabled={submitting || !formData.name.trim()}
className="bg-[var(--dg-blue)] hover:opacity-90 text-white"
>
{submitting ? '保存中...' : editingCluster ? '保存' : '添加'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<K8sClusterDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
editingCluster={editingCluster}
onSaved={loadData}
/>
</div>
);
}