import { Network, Server, Globe, Cloud, Boxes, Database, Zap, Box, } from 'lucide-react'; import type { TopologyNode, TopologyEdge } from '@/types'; // K8s 资源类型 → 拓扑节点类型标签 // Logan.Li export const NODE_TYPE_LABELS: Record = { cluster: '集群', loadbalancer: 'Ingress', service: 'Service', app: 'Deployment', server: 'Node', database: '数据库', external: '外部服务', custom: '自定义', }; // 自定义节点类型选项(编辑模式用) export const CUSTOM_NODE_TYPES = [ { value: 'database', label: '数据库', desc: 'MySQL / Redis / PostgreSQL 等' }, { value: 'external', label: '外部服务', desc: '外部 API / CDN / DNS 等' }, { value: 'custom', label: '自定义', desc: '其他自定义节点' }, ]; // 判断是否为自定义节点(非 K8s 原生资源) export const isCustomNode = (id: string) => id.startsWith('custom-'); // 获取节点图标 export function getNodeIcon(type: string, className = 'h-5 w-5') { switch (type) { case 'server': return ; case 'loadbalancer': return ; case 'cluster': return ; case 'app': return ; case 'service': return ; case 'database': return ; case 'external': return ; case 'custom': return ; default: return ; } } // 获取节点颜色样式 export function getNodeColor(type: string) { switch (type) { case 'server': return 'bg-blue-500/20 text-blue-400 border-blue-500/30'; case 'loadbalancer': return 'bg-purple-500/20 text-purple-400 border-purple-500/30'; case 'cluster': return 'bg-cyan-500/20 text-cyan-400 border-cyan-500/30'; case 'app': return 'bg-rose-500/20 text-rose-400 border-rose-500/30'; case 'service': return 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30'; case 'database': return 'bg-amber-500/20 text-amber-400 border-amber-500/30'; case 'external': return 'bg-orange-500/20 text-orange-400 border-orange-500/30'; case 'custom': return 'bg-slate-500/20 text-slate-400 border-slate-500/30'; default: return 'bg-slate-500/20 text-slate-400 border-slate-500/30'; } } /** * buildTopology — 从真实 K8s 资源构建分层拓扑图 * 层级: Cluster → Ingress → Service → Deployment → Node */ export function buildTopology( cluster: any, ingresses: any[], services: any[], deployments: any[], nodes: any[], ): { nodes: TopologyNode[]; edges: TopologyEdge[] } { const tNodes: TopologyNode[] = []; const tEdges: TopologyEdge[] = []; const MAX_PER_LAYER = 12; const maxLayerCount = Math.max(1, ingresses.length, services.length, deployments.length, nodes.length); const canvasW = Math.max(900, Math.min(maxLayerCount * 130, 2400)); const layerY = [60, 180, 320, 480, 640]; const distributeX = (count: number): number[] => { const cap = Math.min(count, MAX_PER_LAYER); if (cap === 0) return []; if (cap === 1) return [canvasW / 2]; const step = canvasW / (cap + 1); return Array.from({ length: cap }, (_, i) => step * (i + 1)); }; // Layer 0: Cluster const clusterId = `cluster-${cluster.id}`; tNodes.push({ id: clusterId, name: cluster.name || '集群', type: 'cluster', x: canvasW / 2, y: layerY[0], data: { kind: 'cluster', version: cluster.version, status: cluster.status, api_server: cluster.api_server, node_count: cluster.node_count, pod_count: cluster.pod_count }, }); // Layer 1: Ingress const ingCap = ingresses.slice(0, MAX_PER_LAYER); const ingXs = distributeX(ingCap.length); ingCap.forEach((ing, i) => { const id = `ingress-${ing.id}`; tNodes.push({ id, name: ing.name, type: 'loadbalancer', x: ingXs[i], y: layerY[1], data: { kind: 'ingress', namespace: ing.namespace, backend: ing.backend, tls: ing.tls, hosts: ing.hosts } }); tEdges.push({ id: `e-${clusterId}-${id}`, source: clusterId, target: id, label: 'route' }); }); // Layer 2: Service const svcCap = services.slice(0, MAX_PER_LAYER); const svcXs = distributeX(svcCap.length); svcCap.forEach((svc, i) => { const id = `service-${svc.id}`; tNodes.push({ id, name: svc.name, type: 'service', x: svcXs[i], y: layerY[2], data: { kind: 'service', namespace: svc.namespace, type: svc.type, cluster_ip: svc.cluster_ip, external_ip: svc.external_ip, selector: svc.selector } }); }); // Ingress → Service edges if (ingCap.length > 0 && svcCap.length > 0) { ingCap.forEach((ing) => { const ingNodeId = `ingress-${ing.id}`; const backendSvc = (ing.backend || '').split(':')[0].trim(); let matched = svcCap.filter((s) => s.name === backendSvc); if (matched.length === 0) matched = svcCap.filter((s) => s.namespace === ing.namespace); if (matched.length === 0) matched = [svcCap[0]]; matched.forEach((svc) => tEdges.push({ id: `e-${ingNodeId}-service-${svc.id}`, source: ingNodeId, target: `service-${svc.id}`, label: '→' })); }); } else if (svcCap.length > 0) { svcCap.forEach((svc) => tEdges.push({ id: `e-${clusterId}-service-${svc.id}`, source: clusterId, target: `service-${svc.id}`, label: 'expose' })); } // Layer 3: Deployment const depCap = deployments.slice(0, MAX_PER_LAYER); const depXs = distributeX(depCap.length); depCap.forEach((dep, i) => { const id = `deploy-${dep.id}`; tNodes.push({ id, name: dep.name, type: 'app', x: depXs[i], y: layerY[3], data: { kind: 'deployment', namespace: dep.namespace, replicas: dep.replicas, ready_replicas: dep.ready_replicas, available_replicas: dep.available_replicas, image: dep.image } }); }); // Service → Deployment edges if (svcCap.length > 0 && depCap.length > 0) { svcCap.forEach((svc) => { const svcNodeId = `service-${svc.id}`; let matched: any[] = []; try { const selector = typeof svc.selector === 'string' ? JSON.parse(svc.selector || '{}') : (svc.selector || {}); const selectorEntries = Object.entries(selector); if (selectorEntries.length > 0) { matched = depCap.filter((d) => { let labels: Record = {}; try { labels = typeof d.labels === 'string' ? JSON.parse(d.labels || '{}') : (d.labels || {}); } catch { /* */ } return selectorEntries.every(([k, v]) => labels[k] === v); }); } } catch { /* */ } if (matched.length === 0) matched = depCap.filter((d) => d.name === svc.name); if (matched.length === 0) matched = depCap.filter((d) => d.namespace === svc.namespace); if (matched.length === 0) matched = [depCap[0]]; matched.slice(0, 4).forEach((dep) => tEdges.push({ id: `e-${svcNodeId}-deploy-${dep.id}`, source: svcNodeId, target: `deploy-${dep.id}`, label: 'select' })); }); } // Layer 4: Node const nodeCap = nodes.slice(0, MAX_PER_LAYER); const nodeXs = distributeX(nodeCap.length); nodeCap.forEach((n, i) => { const id = `k8snode-${n.id}`; tNodes.push({ id, name: n.name, type: 'server', x: nodeXs[i], y: layerY[4], data: { kind: 'node', ip: n.ip, role: n.role, status: n.status, os: n.os, cpu: n.cpu, memory: n.memory, cpu_usage: n.cpu_usage, memory_usage: n.memory_usage, pod_count: n.pod_count } }); }); // Deployment → Node edges const workerNodes = nodeCap.filter((n) => (n.role || '').toLowerCase() === 'worker'); const schedulableNodes = workerNodes.length > 0 ? workerNodes : nodeCap; if (depCap.length > 0 && schedulableNodes.length > 0) { depCap.forEach((dep) => { const depNodeId = `deploy-${dep.id}`; schedulableNodes.slice(0, 3).forEach((n) => tEdges.push({ id: `e-${depNodeId}-k8snode-${n.id}`, source: depNodeId, target: `k8snode-${n.id}`, label: 'schedule' })); }); } return { nodes: tNodes, edges: tEdges }; } // 布局数据结构 export interface TopologyLayoutData { positions: Record; customNodes: TopologyNode[]; customEdges: TopologyEdge[]; }