refactor: 全局重命名 k8s → kubernetes (前后端+路由+表名+文件)

后端:
  - pkg/k8sclient → pkg/kubernetesclient
  - models/k8s.go → models/kubernetes.go
  - handlers/k8s.go → handlers/kubernetes.go
  - K8sCluster/K8sNode/.. → KubernetesCluster/KubernetesNode/..
  - K8sHandler → KubernetesHandler
  - 路由: /k8s-clusters → /kubernetes-clusters
  - 路由: /k8s/:code/namespace/:ns/... → /kubernetes/:code/namespace/:ns/... (Kuboard风格)
  - 表前缀: container_k8s_ → container_kubernetes_

前端:
  - components/k8s/ → components/kubernetes/
  - K8sClusters → KubernetesClusters
  - K8sWorkloads → KubernetesWorkloads
  - K8sNodes → KubernetesNodes
  - k8sApi → kubernetesApi
  - 路由前缀: /k8s/ → /kubernetes/

测试验证:
✅ /kubernetes/k8s-cluster-liren/namespace/kube-system/deployments → 3 items
✅ /kubernetes/k8s-cluster-liren/namespace/kube-system/pods → 7 items
✅ /kubernetes/k8s-cluster-liren/namespace/kube-system/services → 4 items
✅ /kubernetes/k8s-cluster-liren/namespace/kube-system/configmaps → 6 items
✅ /kubernetes/k8s-cluster-liren/namespace/kube-system/secrets → 1 item
✅ /kubernetes/k8s-cluster-liren/namespaces → 4 items
This commit is contained in:
attacker committed 2026-06-19 08:10:12 +00:00
1 parent f7b667b878
commit cac1414917
27 files changed
+779 -284

No files matched your search

@@ -0,0 +1,360 @@
import { useState, useEffect } from 'react';
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 { Badge } from '@/components/ui/badge';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
Cloud,
Plus,
MoreHorizontal,
Server,
Database,
CheckCircle,
AlertTriangle,
XCircle,
RefreshCw,
Settings,
Eye,
Trash2,
Search,
} from 'lucide-react';
import { kubernetesApi } from '@/services/api';
import type { KubernetesCluster } from '@/types';
import { KubernetesClusterDetail } from './KubernetesClusterDetail';
import { KubernetesClusterDialog } from './KubernetesClusterDialog';
export function KubernetesClusters() {
const [clusters, setClusters] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [selectedCluster, setSelectedCluster] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
// 创建/编辑集群弹窗
const [dialogOpen, setDialogOpen] = useState(false);
const [editingCluster, setEditingCluster] = useState<any>(null);
const loadData = async () => {
try {
setLoading(true);
const res: any = await kubernetesApi.getClusters();
setClusters(res?.list || []);
} catch (e) {
console.error('加载集群列表失败:', e);
setClusters([]);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadData();
}, []);
const handleDelete = async (id: string) => {
try {
await kubernetesApi.deleteCluster(id);
loadData();
} catch (e) {
console.error('删除集群失败:', e);
}
};
const filteredClusters = clusters.filter(
(cluster) =>
(cluster.name || '').toLowerCase().includes(searchQuery.toLowerCase()) ||
(cluster.version || '').includes(searchQuery)
);
const getStatusBadge = (status: KubernetesCluster['status']) => {
switch (status) {
case 'healthy':
return (
<Badge variant="outline" className="border-emerald-500/30 text-emerald-400 bg-emerald-500/10">
<CheckCircle className="h-3 w-3 mr-1" />
健康
</Badge>
);
case 'warning':
return (
<Badge variant="outline" className="border-amber-500/30 text-amber-400 bg-amber-500/10">
<AlertTriangle className="h-3 w-3 mr-1" />
警告
</Badge>
);
case 'error':
return (
<Badge variant="outline" className="border-red-500/30 text-red-400 bg-red-500/10">
<XCircle className="h-3 w-3 mr-1" />
异常
</Badge>
);
default:
return null;
}
};
const clusterDetail = selectedCluster ? clusters.find((c) => c.id === selectedCluster) : null;
return (
<div className="space-y-6">
{/* Page Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-100">集群列表</h1>
<p className="text-sm text-slate-500 mt-1">管理Kubernetes集群资源</p>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
className="border-slate-700 text-slate-300 hover:text-slate-100 hover:bg-slate-800"
onClick={loadData}
>
<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={() => { setEditingCluster(null); setDialogOpen(true); }}>
<Plus className="h-4 w-4 mr-2" />
添加集群
</Button>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">集群总数</p>
<p className="text-2xl font-bold text-slate-100">{clusters.length}</p>
</div>
<div className="p-3 rounded-lg bg-blue-500/10 text-blue-400">
<Cloud className="h-5 w-5" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">健康集群</p>
<p className="text-2xl font-bold text-emerald-400">
{clusters.filter((c) => c.status === 'healthy').length}
</p>
</div>
<div className="p-3 rounded-lg bg-emerald-500/10 text-emerald-400">
<CheckCircle className="h-5 w-5" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">总节点数</p>
<p className="text-2xl font-bold text-slate-100">
{clusters.reduce((acc, c) => acc + (c.node_count || 0), 0)}
</p>
</div>
<div className="p-3 rounded-lg bg-purple-500/10 text-purple-400">
<Server className="h-5 w-5" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">总Pod数</p>
<p className="text-2xl font-bold text-slate-100">
{clusters.reduce((acc, c) => acc + (c.pod_count || 0), 0)}
</p>
</div>
<div className="p-3 rounded-lg bg-cyan-500/10 text-cyan-400">
<Database className="h-5 w-5" />
</div>
</div>
</CardContent>
</Card>
</div>
{/* Search */}
<div className="flex items-center gap-3">
<div className="relative w-80">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
<Input
placeholder="搜索集群名称或版本..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200 placeholder:text-slate-500"
/>
</div>
</div>
{/* Clusters Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4">
{loading ? (
<Card className="bg-slate-900 border-slate-800 col-span-full">
<CardContent className="p-12 text-center text-slate-500">
<RefreshCw className="h-6 w-6 animate-spin mx-auto mb-3" />
加载中...
</CardContent>
</Card>
) : filteredClusters.length === 0 ? (
<Card className="bg-slate-900 border-slate-800 col-span-full">
<CardContent className="p-12 text-center text-slate-500">暂无集群数据</CardContent>
</Card>
) : (
filteredClusters.map((cluster) => {
const cpuUsage = Number(cluster.cpu_usage || 0);
const memoryUsage = Number(cluster.memory_usage || 0);
return (
<Card
key={cluster.id}
className={cn(
'bg-slate-900 border-slate-800 cursor-pointer transition-all',
selectedCluster === cluster.id
? 'border-blue-500 ring-1 ring-blue-500/30'
: 'hover:border-slate-700'
)}
onClick={() => setSelectedCluster(cluster.id)}
>
<CardHeader className="pb-3">
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-blue-500/10 flex items-center justify-center">
<Cloud className="h-5 w-5 text-blue-400" />
</div>
<div>
<CardTitle className="text-base text-slate-100">{cluster.name}</CardTitle>
<CardDescription className="text-slate-500">{cluster.version}</CardDescription>
</div>
</div>
<div className="flex items-center gap-1">
{getStatusBadge(cluster.status)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-slate-100"
onClick={(e) => e.stopPropagation()}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48 bg-slate-800 border-slate-700">
<DropdownMenuLabel className="text-slate-300">操作</DropdownMenuLabel>
<DropdownMenuSeparator className="bg-slate-700" />
<DropdownMenuItem className="text-slate-300 focus:bg-slate-700 focus:text-slate-100">
<Eye className="h-4 w-4 mr-2" />
查看详情
</DropdownMenuItem>
<DropdownMenuItem
className="text-slate-300 focus:bg-slate-700 focus:text-slate-100"
onClick={(e) => {
e.stopPropagation();
setEditingCluster(cluster);
setDialogOpen(true);
}}
>
<Settings className="h-4 w-4 mr-2" />
配置
</DropdownMenuItem>
<DropdownMenuSeparator className="bg-slate-700" />
<DropdownMenuItem
className="text-red-400 focus:bg-slate-700 focus:text-red-400"
onClick={(e) => {
e.stopPropagation();
handleDelete(cluster.id);
}}
>
<Trash2 className="h-4 w-4 mr-2" />
删除
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</CardHeader>
<CardContent className="pt-0">
<div className="grid grid-cols-4 gap-2 mb-4">
<div className="text-center p-2 rounded-lg bg-slate-800/50">
<div className="text-lg font-semibold text-slate-200">{cluster.node_count || 0}</div>
<div className="text-xs text-slate-500">节点</div>
</div>
<div className="text-center p-2 rounded-lg bg-slate-800/50">
<div className="text-lg font-semibold text-slate-200">{cluster.pod_count || 0}</div>
<div className="text-xs text-slate-500">Pod</div>
</div>
<div className="text-center p-2 rounded-lg bg-slate-800/50">
<div className="text-lg font-semibold text-slate-200">{cluster.service_count || 0}</div>
<div className="text-xs text-slate-500">Service</div>
</div>
<div className="text-center p-2 rounded-lg bg-slate-800/50">
<div className="text-lg font-semibold text-slate-200">{cluster.deployment_count || 0}</div>
<div className="text-xs text-slate-500">Deployment</div>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500 w-10">CPU</span>
<div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden">
<div
className={cn(
'h-full rounded-full',
cpuUsage > 80 ? 'bg-red-400' : cpuUsage > 60 ? 'bg-amber-400' : 'bg-emerald-400'
)}
style={{ width: `${cpuUsage}%` }}
/>
</div>
<span className="text-xs text-slate-400 w-10 text-right">{cpuUsage.toFixed(1)}%</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500 w-10">内存</span>
<div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden">
<div
className={cn(
'h-full rounded-full',
memoryUsage > 80 ? 'bg-red-400' : memoryUsage > 60 ? 'bg-amber-400' : 'bg-emerald-400'
)}
style={{ width: `${memoryUsage}%` }}
/>
</div>
<span className="text-xs text-slate-400 w-10 text-right">{memoryUsage.toFixed(1)}%</span>
</div>
</div>
</CardContent>
</Card>
);
})
)}
</div>
{/* Cluster Detail */}
{clusterDetail && (
<KubernetesClusterDetail cluster={clusterDetail} onClose={() => setSelectedCluster(null)} />
)}
{/* 创建/编辑集群 Dialog */}
<KubernetesClusterDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
editingCluster={editingCluster}
onSaved={loadData}
/>
</div>
);
}