feat: 新增日志中心、功能优化与代码重构

- 新增 log_datasource.go / log_proxy.go / user_api_token.go 后端控制器
- 新增 log_datasource.go model
- 新增 monitor/log-center.ts API 模块
- 新增 LogCenter 前端组件及页面
- 重构 deployment 相关组件 (HPDA/UpdateImageModal/bundle-yaml 等)
- 新增 ResetPasswordDialog / code-snippet-block / copy-button 等 UI 组件
- 重构多个 k8s 相关组件 (TopologyView/ResourceVisualEditor/PodFileBrowser 等)
- 更新多个 API 和工具函数 (auth.ts/monitor/index.ts/request.ts 等)
- 更新静态资源 (index.html + 新 JS/CSS bundle)
- 删除废弃静态资源文件
This commit is contained in:
Logan committed 2026-06-26 23:18:05 +08:00
1 parent 5ca0e6d83a
commit 8d46e8f352
197 files changed
+5014 -815

No files matched your search

@@ -9,6 +9,7 @@ export const NODE_TYPE_LABELS: Record<string, string> = {
loadbalancer: 'Ingress',
service: 'Service',
app: 'Deployment',
pod: 'Pod',
server: 'Node',
database: '数据库',
external: '外部服务',
@@ -33,6 +34,7 @@ export function getNodeIcon(type: string, className = 'h-5 w-5') {
case 'cluster': return <Cloud className={className} />;
case 'app': return <Boxes className={className} />;
case 'service': return <Network className={className} />;
case 'pod': return <Box className={className} />;
case 'database': return <Database className={className} />;
case 'external': return <Zap className={className} />;
case 'custom': return <Box className={className} />;
@@ -48,6 +50,7 @@ export function getNodeColor(type: string) {
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 'pod': return 'bg-teal-500/20 text-teal-400 border-teal-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';
@@ -56,18 +59,19 @@ export function getNodeColor(type: string) {
}
/**
* buildTopology — 从真实 K8s 资源构建分层拓扑图
* 层级: Cluster → Ingress → Service → Deployment → Node
* buildTopology — 从真实 K8s 资源构建分层拓扑图(自动关联)
* 推荐层级: Cluster → Ingress(域名+路径) → Service → Deployment → Pod
* 支持基于 Ingress rules、Service selector、Pod 标签的自动连线
*/
export function buildTopology( cluster: any, ingresses: any[], services: any[], deployments: any[], nodes: any[],
export function buildTopology( cluster: any, ingresses: any[], services: any[], deployments: any[], nodes: any[], pods: 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 maxLayerCount = Math.max(1, ingresses.length, services.length, deployments.length, pods.length || nodes.length);
const canvasW = Math.max(900, Math.min(maxLayerCount * 140, 2600));
const layerY = [50, 160, 280, 410, 540, 680]; // cluster, ingress, svc, deploy, pod, node (可选)
const distributeX = (count: number): number[] => {
const cap = Math.min(count, MAX_PER_LAYER);
@@ -85,12 +89,24 @@ export function buildTopology( cluster: any, ingresses: any[], services: any[],
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
// 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 } });
const hosts = Array.isArray(ing.hosts) ? ing.hosts : (ing.hosts ? JSON.parse(ing.hosts) : []);
const hostLabel = hosts.length ? hosts.join(',') : (ing.name || '');
// 尝试显示路径 (如果数据中有 paths 或 backend)
const pathLabel = ing.paths ? ing.paths : (ing.backend ? `→${ing.backend}` : '');
const displayName = pathLabel ? `${hostLabel}\n${pathLabel}` : hostLabel;
tNodes.push({
id,
name: displayName,
type: 'loadbalancer',
x: ingXs[i],
y: layerY[1],
data: { kind: 'ingress', namespace: ing.namespace, backend: ing.backend, tls: ing.tls, hosts }
});
tEdges.push({ id: `e-${clusterId}-${id}`, source: clusterId, target: id, label: 'route' });
});
@@ -124,7 +140,41 @@ export function buildTopology( cluster: any, ingresses: any[], services: any[],
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
// Layer 4: Pod (自动关联 Deployment)
const podCap = (pods || []).slice(0, MAX_PER_LAYER);
const podXs = distributeX(podCap.length);
podCap.forEach((pod, i) => {
const id = `pod-${pod.id || pod.name}`;
tNodes.push({
id,
name: pod.name || 'pod',
type: 'pod', // 需要在类型中支持
x: podXs[i],
y: layerY[4],
data: { kind: 'pod', namespace: pod.namespace, node: pod.node_name || pod.node, status: pod.status, ip: pod.ip }
});
});
// Deployment → Pod 连线 (基于 deployment 名称匹配或 namespace)
if (depCap.length > 0 && podCap.length > 0) {
depCap.forEach((dep) => {
const depNodeId = `deploy-${dep.id}`;
let matchedPods = podCap.filter((p) => p.namespace === dep.namespace);
// 简单匹配:pod 名称包含 deploy 名称 或 labels
matchedPods = matchedPods.filter((p) =>
(p.name || '').includes(dep.name) ||
(dep.name && (p.name || '').startsWith(dep.name))
);
if (matchedPods.length === 0) matchedPods = podCap.filter((p) => p.namespace === dep.namespace).slice(0, 3);
if (matchedPods.length === 0) matchedPods = podCap.slice(0, 2);
matchedPods.slice(0, 3).forEach((p) => {
const pid = `pod-${p.id || p.name}`;
tEdges.push({ id: `e-${depNodeId}-${pid}`, source: depNodeId, target: pid, label: 'owns' });
});
});
}
// Service → Deployment edges (基于 selector 自动匹配)
if (svcCap.length > 0 && depCap.length > 0) {
svcCap.forEach((svc) => {
const svcNodeId = `service-${svc.id}`;
@@ -147,18 +197,23 @@ export function buildTopology( cluster: any, ingresses: any[], services: any[],
});
}
// Layer 4: Node
// Layer 5: 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 } });
tNodes.push({ id, name: n.name, type: 'server', x: nodeXs[i], y: layerY[5], 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
// Deployment / Pod → Node (宿主机) 连线
const workerNodes = nodeCap.filter((n) => (n.role || '').toLowerCase() === 'worker');
const schedulableNodes = workerNodes.length > 0 ? workerNodes : nodeCap;
if (depCap.length > 0 && schedulableNodes.length > 0) {
if (podCap.length > 0 && schedulableNodes.length > 0) {
podCap.forEach((p) => {
const pid = `pod-${p.id || p.name}`;
schedulableNodes.slice(0, 2).forEach((n) => tEdges.push({ id: `e-${pid}-k8snode-${n.id}`, source: pid, target: `k8snode-${n.id}`, label: 'on' }));
});
} else 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' }));