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:
1 parent
f7b667b878
commit
cac1414917
27 files changed
+779
-284
No files matched your search
@@ -0,0 +1,184 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { RefreshCw, Search, Server, Loader2 } from 'lucide-react';
|
||||
import { kubernetesApi } from '@/services/api';
|
||||
|
||||
export function KubernetesNodes() {
|
||||
const [clusters, setClusters] = useState<any[]>([]);
|
||||
const [selectedCluster, setSelectedCluster] = useState('');
|
||||
const [nodes, setNodes] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
const [roleFilter, setRoleFilter] = useState('all');
|
||||
|
||||
const loadClusters = useCallback(async () => {
|
||||
try {
|
||||
const res: any = await kubernetesApi.getClusters();
|
||||
const list = res?.list || [];
|
||||
setClusters(list);
|
||||
if (list.length > 0 && !selectedCluster) setSelectedCluster(String(list[0].id));
|
||||
} catch { setClusters([]); }
|
||||
}, []);
|
||||
|
||||
useEffect(() => { loadClusters(); }, [loadClusters]);
|
||||
|
||||
const loadNodes = useCallback(async () => {
|
||||
if (!selectedCluster) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res: any = await kubernetesApi.getNodes(selectedCluster);
|
||||
setNodes(Array.isArray(res) ? res : (res?.list || []));
|
||||
} catch { setNodes([]); } finally { setLoading(false); }
|
||||
}, [selectedCluster]);
|
||||
|
||||
useEffect(() => { loadNodes(); }, [loadNodes]);
|
||||
|
||||
const filtered = nodes.filter((n) =>
|
||||
(n.name || '').toLowerCase().includes(search.toLowerCase()) &&
|
||||
(roleFilter === 'all' || n.role === roleFilter),
|
||||
);
|
||||
|
||||
const getRoleBadge = (role: string) =>
|
||||
role === 'master'
|
||||
? <Badge className="bg-blue-500/20 text-blue-400 border-blue-500/30">Master</Badge>
|
||||
: <Badge className="bg-emerald-500/20 text-emerald-400 border-emerald-500/30">Worker</Badge>;
|
||||
|
||||
const getStatusBadge = (status: string) =>
|
||||
status === 'Ready'
|
||||
? <Badge className="bg-emerald-500/20 text-emerald-400 border-emerald-500/30">{status}</Badge>
|
||||
: <Badge variant="destructive">{status || 'Unknown'}</Badge>;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<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">K8s 集群工作节点状态与资源</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={selectedCluster} onValueChange={setSelectedCluster}>
|
||||
<SelectTrigger className="w-48 h-9 bg-slate-800/50 border-slate-700 text-slate-200">
|
||||
<SelectValue placeholder="选择集群" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">
|
||||
{clusters.map((c) => (
|
||||
<SelectItem key={c.id} value={String(c.id)}>{c.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" size="icon" className="h-9 w-9 border-slate-700 text-slate-400" onClick={loadNodes} disabled={loading}>
|
||||
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCw className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{[
|
||||
{ label: '总节点', value: nodes.length, color: 'text-blue-400' },
|
||||
{ label: 'Master', value: nodes.filter((n) => n.role === 'master').length, color: 'text-purple-400' },
|
||||
{ label: 'Worker', value: nodes.filter((n) => n.role === 'worker').length, color: 'text-emerald-400' },
|
||||
{ label: '就绪', value: nodes.filter((n) => n.status === 'Ready').length, color: 'text-cyan-400' },
|
||||
].map((s) => (
|
||||
<Card key={s.label} className="bg-slate-900 border-slate-800">
|
||||
<CardContent className="p-4">
|
||||
<p className="text-sm text-slate-500">{s.label}</p>
|
||||
<p className={`text-2xl font-bold ${s.color}`}>{s.value}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative w-56">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-500" />
|
||||
<Input placeholder="搜索节点..." value={search} onChange={(e) => setSearch(e.target.value)} className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||
</div>
|
||||
<Select value={roleFilter} onValueChange={setRoleFilter}>
|
||||
<SelectTrigger className="w-32 h-9 bg-slate-800/50 border-slate-700 text-slate-200">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">
|
||||
<SelectItem value="all">全部角色</SelectItem>
|
||||
<SelectItem value="master">Master</SelectItem>
|
||||
<SelectItem value="worker">Worker</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-slate-800 hover:bg-transparent">
|
||||
<TableHead className="text-slate-400">节点名称</TableHead>
|
||||
<TableHead className="text-slate-400">IP</TableHead>
|
||||
<TableHead className="text-slate-400">角色</TableHead>
|
||||
<TableHead className="text-slate-400">状态</TableHead>
|
||||
<TableHead className="text-slate-400">配置</TableHead>
|
||||
<TableHead className="text-slate-400">使用率</TableHead>
|
||||
<TableHead className="text-slate-400">Pods</TableHead>
|
||||
<TableHead className="text-slate-400">Kubelet</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow><TableCell colSpan={8} className="text-center text-slate-500 py-12"><Loader2 className="h-5 w-5 animate-spin mx-auto mb-2" />加载中...</TableCell></TableRow>
|
||||
) : filtered.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={8} className="text-center text-slate-500 py-12"><Server className="h-8 w-8 mx-auto mb-2 opacity-50" />{selectedCluster ? '暂无节点数据' : '请选择集群'}</TableCell></TableRow>
|
||||
) : (
|
||||
filtered.map((n) => (
|
||||
<TableRow key={n.id} className="border-slate-800 hover:bg-slate-800/30">
|
||||
<TableCell className="font-medium text-slate-200">{n.name}</TableCell>
|
||||
<TableCell><code className="text-sm text-slate-300 bg-slate-800 px-1.5 py-0.5 rounded">{n.ip || '-'}</code></TableCell>
|
||||
<TableCell>{getRoleBadge(n.role)}</TableCell>
|
||||
<TableCell>{getStatusBadge(n.status)}</TableCell>
|
||||
<TableCell className="text-sm text-slate-400">
|
||||
{n.cpu ? `${n.cpu}核` : '-'} / {n.memory ? `${Math.round(n.memory / 1024)}GB` : '-'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{(() => {
|
||||
const cpu = n.cpu_usage || 0;
|
||||
const mem = n.memory_usage || 0;
|
||||
const bar = (val: number) => val >= 85 ? 'bg-red-500' : val >= 60 ? 'bg-yellow-500' : 'bg-emerald-500';
|
||||
const txt = (val: number) => val >= 85 ? 'text-red-400' : val >= 60 ? 'text-yellow-400' : 'text-emerald-400';
|
||||
return (
|
||||
<div className="flex flex-col gap-1 min-w-[120px]">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-slate-500 w-7">CPU</span>
|
||||
<div className="flex-1 h-1.5 rounded-full bg-slate-700/50 overflow-hidden">
|
||||
<div className={`h-full rounded-full ${bar(cpu)}`} style={{ width: `${Math.min(cpu, 100)}%` }} />
|
||||
</div>
|
||||
<span className={`text-xs w-9 text-right ${txt(cpu)}`}>{cpu.toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-slate-500 w-7">Mem</span>
|
||||
<div className="flex-1 h-1.5 rounded-full bg-slate-700/50 overflow-hidden">
|
||||
<div className={`h-full rounded-full ${bar(mem)}`} style={{ width: `${Math.min(mem, 100)}%` }} />
|
||||
</div>
|
||||
<span className={`text-xs w-9 text-right ${txt(mem)}`}>{mem.toFixed(0)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-slate-400">{n.pod_count || 0} / {n.max_pods || '-'}</TableCell>
|
||||
<TableCell className="text-xs text-slate-500">{n.kubelet_version || '-'}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user