feat(i18n): U5 Kubernetes 模块国际化 — 新增 k8s 命名空间(184键) + KubernetesClusterDetail/TopologyView 全量迁移硬编码中文改翻译键
- 新增 k8s 翻译命名空间: cluster/tab/column/empty/label/topology/resource/workload 8 子命名空间
- KubernetesClusterDetail.tsx: 7 Tab(概览/节点/Pod/Deployment/Service/Ingress/事件) 表头+空状态+趋势图标签全量国际化
- TopologyView.tsx: 标题/编辑模式/连线模式/节点编辑弹框/Toast/搜索栏全量国际化
- 新增 common.duration.minAgo 插值键
- 翻译键 1019→1204(+185), 11 命名空间, zh-CN/en-US 完全对齐
- tsc --noEmit=0, npm build✅(17.81s), 17/17 测试通过
This commit is contained in:
1 parent
3b19ae6e98
commit
6db6acc6bd
7 files changed
+611
-129
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -21,6 +22,7 @@ import type { JsonData } from '@/types/common';
|
||||
// 真正历史数据需要后端记录指标快照后才能实现 // Logan.Li
|
||||
|
||||
export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonData; onClose: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const [nodes, setNodes] = useState<JsonData[]>([]);
|
||||
const [pods, setPods] = useState<JsonData[]>([]);
|
||||
const [deployments, setDeployments] = useState<JsonData[]>([]);
|
||||
@@ -59,13 +61,13 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
|
||||
// 模拟最近 5 个时间点的趋势(基于当前值轻微回溯)
|
||||
return [
|
||||
{ time: '10m前', cpu: Math.max(0, cpu * 0.72), memory: Math.max(0, mem * 0.78) },
|
||||
{ time: '7m前', cpu: Math.max(0, cpu * 0.82), memory: Math.max(0, mem * 0.85) },
|
||||
{ time: '4m前', cpu: Math.max(0, cpu * 0.90), memory: Math.max(0, mem * 0.93) },
|
||||
{ time: '2m前', cpu: Math.max(0, cpu * 0.96), memory: Math.max(0, mem * 0.97) },
|
||||
{ time: '现在', cpu, memory: mem },
|
||||
{ time: t('common.duration.min', { count: 10 }), cpu: Math.max(0, cpu * 0.72), memory: Math.max(0, mem * 0.78) },
|
||||
{ time: t('common.duration.min', { count: 7 }), cpu: Math.max(0, cpu * 0.82), memory: Math.max(0, mem * 0.85) },
|
||||
{ time: t('common.duration.min', { count: 4 }), cpu: Math.max(0, cpu * 0.90), memory: Math.max(0, mem * 0.93) },
|
||||
{ time: t('common.duration.min', { count: 2 }), cpu: Math.max(0, cpu * 0.96), memory: Math.max(0, mem * 0.97) },
|
||||
{ time: t('k8s.cluster.now'), cpu, memory: mem },
|
||||
];
|
||||
}, [clusterMetrics]);
|
||||
}, [clusterMetrics, t]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const loadClusterDetail = async (clusterCode: string) => {
|
||||
try {
|
||||
@@ -84,8 +86,8 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
setDeployments(toArray(depRes));
|
||||
setServices(toArray(svcRes));
|
||||
setIngresses(toArray(ingRes));
|
||||
} catch (e) {
|
||||
console.error('加载集群详情失败:', e);
|
||||
} catch {
|
||||
// load failed
|
||||
}
|
||||
};
|
||||
|
||||
@@ -104,7 +106,7 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
<div>
|
||||
<CardTitle className="text-slate-100">{cluster.name}</CardTitle>
|
||||
<CardDescription className="text-slate-500">
|
||||
{cluster.version} · {cluster.node_count || 0} 节点 · 创建于{' '}
|
||||
{cluster.version} · {cluster.node_count || 0} {t('k8s.cluster.nodeCount')} · {t('k8s.cluster.created')}{' '}
|
||||
{cluster.created_at ? new Date(cluster.created_at).toLocaleDateString() : '-'}
|
||||
</CardDescription>
|
||||
</div>
|
||||
@@ -116,7 +118,7 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
onClick={onClose}
|
||||
>
|
||||
<XCircle className="h-4 w-4 mr-2" />
|
||||
关闭
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -124,25 +126,25 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
<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">
|
||||
概览
|
||||
{t('k8s.tab.overview')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="nodes" className="data-[state=active]:bg-slate-700">
|
||||
节点 ({nodes.length})
|
||||
{t('k8s.tab.nodes')} ({nodes.length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="pods" className="data-[state=active]:bg-slate-700">
|
||||
Pod ({pods.length})
|
||||
{t('k8s.tab.pods')} ({pods.length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="deployments" className="data-[state=active]:bg-slate-700">
|
||||
Deployment ({deployments.length})
|
||||
{t('k8s.tab.deployments')} ({deployments.length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="services" className="data-[state=active]:bg-slate-700">
|
||||
Service ({services.length})
|
||||
{t('k8s.tab.services')} ({services.length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="ingresses" className="data-[state=active]:bg-slate-700">
|
||||
Ingress ({ingresses.length})
|
||||
{t('k8s.tab.ingresses')} ({ingresses.length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="events" className="data-[state=active]:bg-slate-700">
|
||||
事件
|
||||
{t('k8s.tab.events')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
@@ -151,13 +153,13 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
<Card className="bg-slate-800/50 border-slate-700">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<CardTitle className="text-sm text-slate-400">资源使用趋势(模拟最近变化)</CardTitle>
|
||||
<CardTitle className="text-sm text-slate-400">{t('k8s.cluster.resourceTrend')}</CardTitle>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 text-slate-400 hover:text-slate-200"
|
||||
onClick={() => cluster?.id && loadClusterDetail(cluster.id)}
|
||||
title="刷新节点指标"
|
||||
title={t('k8s.cluster.refreshMetrics')}
|
||||
>
|
||||
<RefreshCw className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
@@ -197,7 +199,7 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
fill="url(#colorCpu)"
|
||||
dot={false}
|
||||
activeDot={{ r: 3, fill: '#3b82f6' }}
|
||||
name="CPU(%)"
|
||||
name={t('k8s.label.cpuPercent')}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
@@ -207,39 +209,39 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
fill="url(#colorMemory)"
|
||||
dot={false}
|
||||
activeDot={{ r: 3, fill: '#10b981' }}
|
||||
name="内存(%)"
|
||||
name={t('k8s.label.memoryPercent')}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="flex justify-center gap-6 mt-2 text-xs text-slate-400">
|
||||
<span>CPU: <span className="text-blue-400 font-bold">{clusterMetrics.cpuPct.toFixed(1)}%</span></span>
|
||||
<span>内存: <span className="text-emerald-400 font-bold">{clusterMetrics.memPct.toFixed(1)}%</span></span>
|
||||
<span>{t('k8s.label.cpu')}: <span className="text-blue-400 font-bold">{clusterMetrics.cpuPct.toFixed(1)}%</span></span>
|
||||
<span>{t('k8s.label.memory')}: <span className="text-emerald-400 font-bold">{clusterMetrics.memPct.toFixed(1)}%</span></span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-slate-800/50 border-slate-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm text-slate-400">资源分布</CardTitle>
|
||||
<CardTitle className="text-sm text-slate-400">{t('k8s.cluster.resourceDistribution')}</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 className="text-sm text-slate-500 mt-1">{t('k8s.tab.pods')}</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 className="text-sm text-slate-500 mt-1">{t('k8s.tab.services')}</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 className="text-sm text-slate-500 mt-1">{t('k8s.tab.deployments')}</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 className="text-sm text-slate-500 mt-1">{t('k8s.tab.nodes')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -252,19 +254,19 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
<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>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.name')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.role')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('common.status')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.cpu')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.memory')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.tab.pods')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.os')}</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>
|
||||
<td colSpan={7} className="px-4 py-8 text-center text-slate-500">{t('k8s.empty.nodes')}</td>
|
||||
</tr>
|
||||
) : (
|
||||
nodes.map((node) => (
|
||||
@@ -295,7 +297,7 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-300">
|
||||
{node.cpu_usage_m || '0m'} / {node.cpu}核
|
||||
{node.cpu_usage_m || '0m'} / {node.cpu}{t('k8s.label.cores')}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-300">
|
||||
{node.mem_usage_mi || '0Mi'} / {node.memory_mb ? (node.memory_mb / 1024).toFixed(0) + 'GB' : '-'}
|
||||
@@ -315,19 +317,19 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
<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>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.name')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.namespace')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('common.status')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.restartCount')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.cpu')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.memory')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.node')}</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>
|
||||
<td colSpan={7} className="px-4 py-8 text-center text-slate-500">{t('k8s.empty.pods')}</td>
|
||||
</tr>
|
||||
) : (
|
||||
pods.map((pod) => (
|
||||
@@ -369,18 +371,18 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
<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>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.name')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.namespace')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.replicas')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.availableReplicas')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.image')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.createdAt')}</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>
|
||||
<td colSpan={6} className="px-4 py-8 text-center text-slate-500">{t('k8s.empty.deployments')}</td>
|
||||
</tr>
|
||||
) : (
|
||||
deployments.map((deployment) => (
|
||||
@@ -412,18 +414,18 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
<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">{t('k8s.column.name')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.namespace')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('common.type')}</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>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.ports')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.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>
|
||||
<td colSpan={6} className="px-4 py-8 text-center text-slate-500">{t('k8s.empty.services')}</td>
|
||||
</tr>
|
||||
) : (
|
||||
services.map((service) => (
|
||||
@@ -464,17 +466,17 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
<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>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.name')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.namespace')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.hosts')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.backend')}</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-400">{t('k8s.column.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>
|
||||
<td colSpan={5} className="px-4 py-8 text-center text-slate-500">{t('k8s.empty.ingresses')}</td>
|
||||
</tr>
|
||||
) : (
|
||||
ingresses.map((ingress) => (
|
||||
@@ -496,7 +498,7 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
: 'border-slate-700 text-slate-400'
|
||||
}
|
||||
>
|
||||
{ingress.tls ? '已启用' : '未启用'}
|
||||
{ingress.tls ? t('k8s.label.enabled') : t('k8s.label.disabled')}
|
||||
</Badge>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -510,11 +512,11 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonDa
|
||||
<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分钟前' },
|
||||
{ type: 'Normal', reason: 'Started', message: 'Started container nginx', time: t('common.duration.minAgo', { count: 2 }) },
|
||||
{ type: 'Normal', reason: 'Created', message: 'Created pod nginx-deployment-xxx', time: t('common.duration.minAgo', { count: 5 }) },
|
||||
{ type: 'Warning', reason: 'FailedScheduling', message: '0/3 nodes are available', time: t('common.duration.minAgo', { count: 10 }) },
|
||||
{ type: 'Normal', reason: 'Pulling', message: 'Pulling image "nginx:latest"', time: t('common.duration.minAgo', { count: 15 }) },
|
||||
{ type: 'Normal', reason: 'Pulled', message: 'Successfully pulled image', time: t('common.duration.minAgo', { count: 12 }) },
|
||||
].map((event, index) => (
|
||||
<div
|
||||
key={index}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
} from './topology-helpers';
|
||||
|
||||
export function TopologyView() {
|
||||
const { t } = useTranslation();
|
||||
const { clusterCode } = useParams<{ clusterCode?: string }>();
|
||||
const _confirm = useConfirm();
|
||||
const [clusters, setClusters] = useState<any[]>([]);
|
||||
@@ -172,9 +174,9 @@ export function TopologyView() {
|
||||
if (connectMode) {
|
||||
if (!connectSource) {
|
||||
setConnectSource(node.id);
|
||||
toast.info(`已选择源节点: ${node.name},请点击目标节点`);
|
||||
toast.info(t('k8s.topology.sourceSelected', { name: node.name }));
|
||||
} else if (connectSource === node.id) {
|
||||
toast.info('不能连接自身');
|
||||
toast.info(t('k8s.topology.connectSelf'));
|
||||
} else {
|
||||
// 创建自定义连线
|
||||
const newEdge: TopologyEdge = {
|
||||
@@ -182,7 +184,7 @@ export function TopologyView() {
|
||||
source: connectSource, target: node.id, label: 'connect',
|
||||
};
|
||||
setTopology((prev) => ({ ...prev, edges: [...prev.edges, newEdge] }));
|
||||
toast.success('连线已添加');
|
||||
toast.success(t('k8s.topology.connectAdded'));
|
||||
setConnectSource(null);
|
||||
setConnectMode(false);
|
||||
}
|
||||
@@ -205,7 +207,7 @@ export function TopologyView() {
|
||||
};
|
||||
|
||||
const handleSaveNode = () => {
|
||||
if (!editForm.name.trim()) { toast.error('请输入节点名称'); return; }
|
||||
if (!editForm.name.trim()) { toast.error(t('k8s.topology.nodeNameRequired')); return; }
|
||||
if (editingNode) {
|
||||
// 编辑现有节点
|
||||
setTopology((prev) => ({
|
||||
@@ -214,7 +216,7 @@ export function TopologyView() {
|
||||
? { ...n, name: editForm.name, type: editForm.type, data: { ...n.data, note: editForm.note } }
|
||||
: n),
|
||||
}));
|
||||
toast.success('节点已更新');
|
||||
toast.success(t('k8s.topology.nodeUpdated'));
|
||||
} else {
|
||||
// 新增自定义节点
|
||||
const id = `custom-${customIdCounter}`;
|
||||
@@ -226,28 +228,28 @@ export function TopologyView() {
|
||||
};
|
||||
setTopology((prev) => ({ ...prev, nodes: [...prev.nodes, newNode] }));
|
||||
setCustomIdCounter((c) => c + 1);
|
||||
toast.success('节点已添加');
|
||||
toast.success(t('k8s.topology.nodeAdded'));
|
||||
}
|
||||
setEditDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleDeleteNode = async (node: TopologyNode) => {
|
||||
const ok = await _confirm({ title: '删除节点', description: `确定删除「${node.name}」?相关连线也会一并删除`, variant: 'destructive' });
|
||||
const ok = await _confirm({ title: t('k8s.topology.deleteNodeTitle'), description: t('k8s.topology.deleteNodeDesc', { name: node.name }), variant: 'destructive' });
|
||||
if (!ok) return;
|
||||
setTopology((prev) => ({
|
||||
nodes: prev.nodes.filter((n) => n.id !== node.id),
|
||||
edges: prev.edges.filter((e) => e.source !== node.id && e.target !== node.id),
|
||||
}));
|
||||
setSelectedNode(null);
|
||||
toast.success('节点已删除');
|
||||
toast.success(t('k8s.topology.nodeDeleted'));
|
||||
};
|
||||
|
||||
const handleDeleteEdge = async (edge: TopologyEdge) => {
|
||||
if (!edge.id.startsWith('custom-edge')) { toast.info('Kubernetes 原生关系不可删除'); return; }
|
||||
const ok = await _confirm({ title: '删除连线', description: '确定删除此自定义连线?', variant: 'destructive' });
|
||||
if (!edge.id.startsWith('custom-edge')) { toast.info(t('k8s.topology.nativeEdgeNoDelete')); return; }
|
||||
const ok = await _confirm({ title: t('k8s.topology.deleteEdgeTitle'), description: t('k8s.topology.deleteEdgeDesc'), variant: 'destructive' });
|
||||
if (!ok) return;
|
||||
setTopology((prev) => ({ ...prev, edges: prev.edges.filter((e) => e.id !== edge.id) }));
|
||||
toast.success('连线已删除');
|
||||
toast.success(t('k8s.topology.edgeDeleted'));
|
||||
};
|
||||
|
||||
// --- 保存/重置布局 ---
|
||||
@@ -261,13 +263,13 @@ export function TopologyView() {
|
||||
customEdges: topology.edges.filter((e) => e.id.startsWith('custom-edge')),
|
||||
};
|
||||
await kubernetesApi.saveTopology(selectedClusterId, JSON.stringify(layoutData));
|
||||
toast.success('布局已保存');
|
||||
} catch { toast.error('保存失败'); }
|
||||
toast.success(t('k8s.topology.layoutSaved'));
|
||||
} catch { toast.error(t('k8s.topology.layoutSaveFailed')); }
|
||||
finally { setSaving(false); }
|
||||
};
|
||||
|
||||
const handleResetLayout = async () => {
|
||||
const ok = await _confirm({ title: '重置布局', description: '将清除已保存的布局并重新自动排列', variant: 'destructive' });
|
||||
const ok = await _confirm({ title: t('k8s.topology.resetLayout'), description: t('k8s.topology.resetConfirm'), variant: 'destructive' });
|
||||
if (!ok) return;
|
||||
try { await kubernetesApi.saveTopology(selectedClusterId, ''); } catch { /* */ }
|
||||
setSavedLayout(null);
|
||||
@@ -275,7 +277,7 @@ export function TopologyView() {
|
||||
setConnectMode(false);
|
||||
setConnectSource(null);
|
||||
await loadTopology();
|
||||
toast.success('布局已重置');
|
||||
toast.success(t('k8s.topology.layoutReset'));
|
||||
};
|
||||
|
||||
// --- 画布尺寸 ---
|
||||
@@ -290,7 +292,7 @@ export function TopologyView() {
|
||||
const renderTopology = () => {
|
||||
if (topology.nodes.length === 0) return null;
|
||||
const ys = [60, 180, 320, 480, 640];
|
||||
const labels = ['集群', 'Ingress', 'Service', 'Deployment', 'Node'];
|
||||
const labels = [t('k8s.tab.overview'), 'Ingress', 'Service', 'Deployment', 'Node'];
|
||||
return (
|
||||
<svg ref={svgRef} width="100%" height="100%" viewBox={`0 0 ${canvasW} ${canvasH}`} className="bg-slate-950 rounded-lg" preserveAspectRatio="xMidYMin meet"
|
||||
onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp}>
|
||||
@@ -319,7 +321,7 @@ export function TopologyView() {
|
||||
markerEnd="url(#arrowhead)" opacity={isCustom ? 0.8 : 0.6} />
|
||||
{editMode && isCustom && (
|
||||
<text x={(source.x + target.x) / 2} y={(source.y + target.y) / 2}
|
||||
fill="#f59e0b" fontSize="10" textAnchor="middle" className="select-none pointer-events-none">✕ 删除</text>
|
||||
fill="#f59e0b" fontSize="10" textAnchor="middle" className="select-none pointer-events-none">✕ {t('k8s.topology.deleteText')}</text>
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
@@ -372,23 +374,23 @@ export function TopologyView() {
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-100">架构拓扑</h1>
|
||||
<h1 className="text-2xl font-bold text-slate-100">{t('k8s.topology.title')}</h1>
|
||||
<p className="text-sm text-slate-500 mt-1">
|
||||
Kubernetes 集群资源拓扑可视化 · 拖拽节点调整布局
|
||||
{editMode && <span className="text-amber-400 ml-2">· 编辑模式</span>}
|
||||
{connectMode && <span className="text-amber-400 ml-1">· 连线中{connectSource ? '(选择目标节点)' : '(选择源节点)'}</span>}
|
||||
{t('k8s.topology.subtitle')}
|
||||
{editMode && <span className="text-amber-400 ml-2">{`· ${t('k8s.topology.editModeActive')}`}</span>}
|
||||
{connectMode && <span className="text-amber-400 ml-1">{`· ${t('k8s.topology.connecting')} ${connectSource ? t('k8s.topology.selectTarget') : t('k8s.topology.selectSource')}`}</span>}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={selectedClusterId} onValueChange={(v) => { setSelectedClusterId(v); setSelectedNode(null); setEditMode(false); }}>
|
||||
<SelectTrigger className="w-44 h-9 bg-slate-800/50 border-slate-700 text-slate-200"><SelectValue placeholder="选择集群" /></SelectTrigger>
|
||||
<SelectTrigger className="w-44 h-9 bg-slate-800/50 border-slate-700 text-slate-200"><SelectValue placeholder={t('k8s.topology.selectCluster')} /></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={editMode ? 'default' : 'outline'} size="sm" className="h-9"
|
||||
onClick={() => { setEditMode(!editMode); setConnectMode(false); setConnectSource(null); }}>
|
||||
<Pencil className="h-4 w-4 mr-1" />{editMode ? '退出编辑' : '编辑'}
|
||||
<Pencil className="h-4 w-4 mr-1" />{editMode ? t('k8s.topology.exitEdit') : t('common.edit')}
|
||||
</Button>
|
||||
<Button variant="outline" size="icon" className="h-9 w-9 border-slate-700 text-slate-400" onClick={loadTopology} disabled={loading}>
|
||||
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCw className="h-4 w-4" />}
|
||||
@@ -402,9 +404,9 @@ export function TopologyView() {
|
||||
await loadTopology();
|
||||
}}
|
||||
disabled={loading}
|
||||
title="忽略保存的布局,重新根据 K8s 资源自动生成关联 (Ingress路径→Svc→Deploy→Pod)"
|
||||
title={t('k8s.topology.autoGenerateTitle')}
|
||||
>
|
||||
<RotateCcw className="h-3.5 w-3.5 mr-1" /> 重新自动生成
|
||||
<RotateCcw className="h-3.5 w-3.5 mr-1" /> {t('k8s.topology.autoGenerate')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -413,31 +415,31 @@ export function TopologyView() {
|
||||
{editMode && (
|
||||
<div className="flex items-center gap-2 p-3 rounded-lg bg-slate-800/50 border border-slate-700">
|
||||
<Button size="sm" variant="outline" className="border-slate-600 text-slate-200" onClick={handleAddNode}>
|
||||
<Plus className="h-4 w-4 mr-1" />添加节点
|
||||
<Plus className="h-4 w-4 mr-1" />{t('k8s.topology.addNode')}
|
||||
</Button>
|
||||
<Button size="sm" variant={connectMode ? 'default' : 'outline'} className={connectMode ? 'bg-amber-600 hover:bg-amber-700' : 'border-slate-600 text-slate-200'}
|
||||
onClick={() => { setConnectMode(!connectMode); setConnectSource(null); }}>
|
||||
<Link2 className="h-4 w-4 mr-1" />{connectMode ? '取消连线' : '添加连线'}
|
||||
<Link2 className="h-4 w-4 mr-1" />{connectMode ? t('k8s.topology.cancelConnect') : t('k8s.topology.addConnection')}
|
||||
</Button>
|
||||
<div className="h-5 w-px bg-slate-600" />
|
||||
<Button size="sm" variant="outline" className="border-emerald-600/30 text-emerald-400 hover:bg-emerald-600/10" onClick={handleSaveLayout} disabled={saving}>
|
||||
<Save className="h-4 w-4 mr-1" />{saving ? '保存中...' : '保存布局'}
|
||||
<Save className="h-4 w-4 mr-1" />{saving ? t('k8s.topology.saving') : t('k8s.topology.saveLayout')}
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" className="border-red-500/30 text-red-400 hover:bg-red-500/10" onClick={handleResetLayout}>
|
||||
<RotateCcw className="h-4 w-4 mr-1" />重置布局
|
||||
<RotateCcw className="h-4 w-4 mr-1" />{t('k8s.topology.resetLayout')}
|
||||
</Button>
|
||||
<span className="text-xs text-slate-500 ml-auto">双击节点编辑 · 点击虚线删除 · 拖拽调整位置</span>
|
||||
<span className="text-xs text-slate-500 ml-auto">{t('k8s.topology.editToolbarHint')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
{[
|
||||
{ label: '总节点数', value: topology.nodes.length, icon: <Cloud className="h-5 w-5" />, color: 'text-blue-400', bg: 'bg-blue-500/10' },
|
||||
{ label: '连接数', value: topology.edges.length, icon: <Link2 className="h-5 w-5" />, color: 'text-purple-400', bg: 'bg-purple-500/10' },
|
||||
{ label: t('k8s.topology.totalNodes'), value: topology.nodes.length, icon: <Cloud className="h-5 w-5" />, color: 'text-blue-400', bg: 'bg-blue-500/10' },
|
||||
{ label: t('k8s.topology.connections'), value: topology.edges.length, icon: <Link2 className="h-5 w-5" />, color: 'text-purple-400', bg: 'bg-purple-500/10' },
|
||||
{ label: 'Service', value: countByType('service'), icon: getNodeIcon('service'), color: 'text-emerald-400', bg: 'bg-emerald-500/10' },
|
||||
{ label: 'Deployment', value: countByType('app'), icon: getNodeIcon('app'), color: 'text-rose-400', bg: 'bg-rose-500/10' },
|
||||
{ label: '自定义', value: topology.nodes.filter((n) => isCustomNode(n.id)).length, icon: getNodeIcon('custom'), color: 'text-amber-400', bg: 'bg-amber-500/10' },
|
||||
{ label: t('k8s.topology.custom2'), value: topology.nodes.filter((n) => isCustomNode(n.id)).length, icon: getNodeIcon('custom'), color: 'text-amber-400', bg: 'bg-amber-500/10' },
|
||||
].map((s) => (
|
||||
<Card key={s.label} className="bg-slate-900 border-slate-800">
|
||||
<CardContent className="p-4">
|
||||
@@ -457,12 +459,12 @@ export function TopologyView() {
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative w-48">
|
||||
<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" />
|
||||
<Input placeholder={t('k8s.topology.searchPlaceholder')} value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} className="pl-9 h-9 bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||
</div>
|
||||
<Select value={viewType} onValueChange={setViewType}>
|
||||
<SelectTrigger className="w-36 h-9 bg-slate-800/50 border-slate-700 text-slate-200"><SelectValue placeholder="节点类型" /></SelectTrigger>
|
||||
<SelectTrigger className="w-36 h-9 bg-slate-800/50 border-slate-700 text-slate-200"><SelectValue placeholder={t('k8s.topology.nodeTypeLabel')} /></SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">
|
||||
<SelectItem value="all">全部类型</SelectItem>
|
||||
<SelectItem value="all">{t('k8s.topology.allTypes')}</SelectItem>
|
||||
{Object.entries(NODE_TYPE_LABELS).map(([v, l]) => <SelectItem key={v} value={v}>{l}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -479,7 +481,7 @@ export function TopologyView() {
|
||||
) : topology.nodes.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-slate-600">
|
||||
<Cloud className="h-12 w-12 mb-3 opacity-50" />
|
||||
<p className="text-sm">{selectedClusterId ? '该集群暂无拓扑数据' : '请选择集群查看拓扑'}</p>
|
||||
<p className="text-sm">{selectedClusterId ? t('k8s.topology.noDataWithCluster') : t('k8s.topology.noDataSelectCluster')}</p>
|
||||
</div>
|
||||
) : renderTopology()}
|
||||
</div>
|
||||
@@ -489,20 +491,20 @@ export function TopologyView() {
|
||||
<div>
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base text-slate-100">节点列表</CardTitle>
|
||||
<CardDescription className="text-slate-500">点击节点查看详情</CardDescription>
|
||||
<CardTitle className="text-base text-slate-100">{t('k8s.topology.nodeList')}</CardTitle>
|
||||
<CardDescription className="text-slate-500">{t('k8s.topology.clickToViewDetail')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2 max-h-[520px] overflow-auto">
|
||||
{filteredNodes.length === 0 ? (
|
||||
<p className="text-sm text-slate-600 text-center py-8">暂无节点</p>
|
||||
<p className="text-sm text-slate-600 text-center py-8">{t('k8s.topology.noNodes')}</p>
|
||||
) : (
|
||||
filteredNodes.map((node) => (
|
||||
<div key={node.id} className={`flex items-center gap-3 p-2 rounded-lg cursor-pointer transition-colors ${selectedNode?.id === node.id ? 'bg-blue-500/10 border border-blue-500/30' : 'hover:bg-slate-800/50'}`} onClick={() => setSelectedNode(node)}>
|
||||
<div className={`p-2 rounded-lg ${getNodeColor(node.type)}`}>{getNodeIcon(node.type)}</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-slate-200 truncate">{node.name}</p>
|
||||
<p className="text-xs text-slate-500">{NODE_TYPE_LABELS[node.type] || node.type}{isCustomNode(node.id) ? ' · 自定义' : ''}</p>
|
||||
<p className="text-xs text-slate-500">{NODE_TYPE_LABELS[node.type] || node.type}{isCustomNode(node.id) ? ` · ${t('k8s.topology.customSuffix')}` : ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
@@ -520,15 +522,15 @@ export function TopologyView() {
|
||||
<DialogTitle className="text-slate-100 flex items-center gap-2">
|
||||
<div className={`p-2 rounded-lg ${getNodeColor(selectedNode?.type || 'server')}`}>{selectedNode && getNodeIcon(selectedNode.type)}</div>
|
||||
{selectedNode?.name}
|
||||
{selectedNode && isCustomNode(selectedNode.id) && <Badge variant="outline" className="border-amber-500/30 text-amber-400 text-xs">自定义</Badge>}
|
||||
{selectedNode && isCustomNode(selectedNode.id) && <Badge variant="outline" className="border-amber-500/30 text-amber-400 text-xs">{t('k8s.topology.custom2')}</Badge>}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">类型: {NODE_TYPE_LABELS[selectedNode?.type || ''] || selectedNode?.type}</DialogDescription>
|
||||
<DialogDescription className="text-slate-500">{t('common.type')}: {NODE_TYPE_LABELS[selectedNode?.type || ''] || selectedNode?.type}</DialogDescription>
|
||||
</DialogHeader>
|
||||
{selectedNode && (
|
||||
<div className="space-y-4">
|
||||
{renderNodeDetailFields(selectedNode)}
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-300 mb-2">关联连接</h4>
|
||||
<h4 className="text-sm font-medium text-slate-300 mb-2">{t('k8s.topology.relatedConnections')}</h4>
|
||||
<div className="space-y-2">
|
||||
{topology.edges.filter((e) => e.source === selectedNode.id || e.target === selectedNode.id).map((edge) => {
|
||||
const isSource = edge.source === selectedNode.id;
|
||||
@@ -543,18 +545,18 @@ export function TopologyView() {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{topology.edges.filter((e) => e.source === selectedNode.id || e.target === selectedNode.id).length === 0 && <p className="text-xs text-slate-600">无关联连接</p>}
|
||||
{topology.edges.filter((e) => e.source === selectedNode.id || e.target === selectedNode.id).length === 0 && <p className="text-xs text-slate-600">{t('k8s.topology.noConnections')}</p>}
|
||||
</div>
|
||||
</div>
|
||||
{/* 编辑模式操作按钮 */}
|
||||
{editMode && selectedNode && (
|
||||
<div className="flex gap-2 pt-2 border-t border-slate-700">
|
||||
<Button size="sm" variant="outline" className="border-slate-600 text-slate-200" onClick={() => handleEditNode(selectedNode)}>
|
||||
<Pencil className="h-3.5 w-3.5 mr-1" />编辑
|
||||
<Pencil className="h-3.5 w-3.5 mr-1" />{t('common.edit')}
|
||||
</Button>
|
||||
{isCustomNode(selectedNode.id) && (
|
||||
<Button size="sm" variant="outline" className="border-red-500/30 text-red-400 hover:bg-red-500/10" onClick={() => handleDeleteNode(selectedNode)}>
|
||||
<Trash2 className="h-3.5 w-3.5 mr-1" />删除
|
||||
<Trash2 className="h-3.5 w-3.5 mr-1" />{t('common.delete')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -568,17 +570,17 @@ export function TopologyView() {
|
||||
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
|
||||
<DialogContent className="bg-slate-900 border-slate-800 max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-slate-100">{editingNode ? '编辑节点' : '添加自定义节点'}</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">{editingNode ? '修改节点属性' : '添加数据库、外部服务等非 Kubernetes 资源节点'}</DialogDescription>
|
||||
<DialogTitle className="text-slate-100">{editingNode ? t('k8s.topology.editNodeTitle') : t('k8s.topology.addNodeTitle')}</DialogTitle>
|
||||
<DialogDescription className="text-slate-500">{editingNode ? t('k8s.topology.editNodeDesc') : t('k8s.topology.addNodeDesc')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-slate-300">名称</Label>
|
||||
<Label className="text-slate-300">{t('k8s.topology.nodeName')}</Label>
|
||||
<Input value={editForm.name} onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
|
||||
placeholder="如 MySQL 主库、Redis 集群" className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||
placeholder={t('k8s.topology.namePlaceholder')} className="bg-slate-800/50 border-slate-700 text-slate-200" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-slate-300">类型</Label>
|
||||
<Label className="text-slate-300">{t('k8s.topology.nodeType')}</Label>
|
||||
<Select value={editForm.type} onValueChange={(v) => setEditForm({ ...editForm, type: v as TopologyNode['type'] })}>
|
||||
<SelectTrigger className="bg-slate-800/50 border-slate-700 text-slate-200"><SelectValue /></SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">
|
||||
@@ -587,14 +589,14 @@ export function TopologyView() {
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-slate-300">备注</Label>
|
||||
<Label className="text-slate-300">{t('k8s.topology.nodeNote')}</Label>
|
||||
<Textarea value={editForm.note} onChange={(e) => setEditForm({ ...editForm, note: e.target.value })}
|
||||
placeholder="连接信息、用途说明等" rows={2} className="bg-slate-800/50 border-slate-700 text-slate-200 resize-none" />
|
||||
placeholder={t('k8s.topology.notePlaceholder')} rows={2} className="bg-slate-800/50 border-slate-700 text-slate-200 resize-none" />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300" onClick={() => setEditDialogOpen(false)}>取消</Button>
|
||||
<Button onClick={handleSaveNode}>{editingNode ? '保存' : '添加'}</Button>
|
||||
<Button variant="outline" className="border-slate-700 text-slate-300" onClick={() => setEditDialogOpen(false)}>{t('common.cancel')}</Button>
|
||||
<Button onClick={handleSaveNode}>{editingNode ? t('common.save') : t('k8s.topology.add2')}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -77,7 +77,8 @@
|
||||
"min": "{{count}}m",
|
||||
"minSec": "{{m}}m {{s}}s",
|
||||
"hour": "{{count}}h",
|
||||
"hourMin": "{{h}}h {{m}}m"
|
||||
"hourMin": "{{h}}h {{m}}m",
|
||||
"minAgo": "{{count}} min ago"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -1145,5 +1146,207 @@
|
||||
"allType": "All Types",
|
||||
"pagination": "{{n}}/page"
|
||||
}
|
||||
},
|
||||
"k8s": {
|
||||
"cluster": {
|
||||
"detail": "Cluster Details",
|
||||
"version": "Version",
|
||||
"nodeCount": "nodes",
|
||||
"created": "Created",
|
||||
"loadFailed": "Failed to load cluster details",
|
||||
"resourceTrend": "Resource Usage Trend (simulated)",
|
||||
"refreshMetrics": "Refresh node metrics",
|
||||
"resourceDistribution": "Resource Distribution",
|
||||
"now": "Now"
|
||||
},
|
||||
"tab": {
|
||||
"overview": "Overview",
|
||||
"nodes": "Nodes",
|
||||
"pods": "Pods",
|
||||
"deployments": "Deployments",
|
||||
"services": "Services",
|
||||
"ingresses": "Ingresses",
|
||||
"events": "Events"
|
||||
},
|
||||
"column": {
|
||||
"name": "Name",
|
||||
"namespace": "Namespace",
|
||||
"role": "Role",
|
||||
"status": "Status",
|
||||
"cpu": "CPU",
|
||||
"memory": "Memory",
|
||||
"os": "OS",
|
||||
"restartCount": "Restarts",
|
||||
"node": "Node",
|
||||
"replicas": "Replicas",
|
||||
"availableReplicas": "Available",
|
||||
"image": "Image",
|
||||
"type": "Type",
|
||||
"ports": "Ports",
|
||||
"selector": "Selector",
|
||||
"hosts": "Hosts",
|
||||
"backend": "Backend",
|
||||
"tls": "TLS",
|
||||
"createdAt": "Created"
|
||||
},
|
||||
"empty": {
|
||||
"nodes": "No node data",
|
||||
"pods": "No pod data",
|
||||
"deployments": "No deployment data",
|
||||
"services": "No service data",
|
||||
"ingresses": "No ingress data",
|
||||
"events": "No events"
|
||||
},
|
||||
"label": {
|
||||
"cpu": "CPU",
|
||||
"memory": "Memory",
|
||||
"cpuPercent": "CPU(%)",
|
||||
"memoryPercent": "Memory(%)",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"cores": "cores"
|
||||
},
|
||||
"topology": {
|
||||
"title": "Architecture Topology",
|
||||
"selectCluster": "Select Cluster",
|
||||
"allResources": "All Resources",
|
||||
"editMode": "Edit Mode",
|
||||
"connectMode": "Connect Mode",
|
||||
"saveLayout": "Save Layout",
|
||||
"resetLayout": "Reset Layout",
|
||||
"addNode": "Add Node",
|
||||
"editNode": "Edit Node",
|
||||
"deleteNode": "Delete Node",
|
||||
"nodeName": "Node Name",
|
||||
"nodeType": "Node Type",
|
||||
"nodeNote": "Note",
|
||||
"searchPlaceholder": "Search nodes...",
|
||||
"noData": "No topology data, please select a cluster",
|
||||
"layoutSaved": "Layout saved",
|
||||
"layoutSaveFailed": "Save failed",
|
||||
"layoutReset": "Layout has been reset",
|
||||
"connectHint": "Click two nodes to create a connection",
|
||||
"cancelConnect": "Cancel Connection",
|
||||
"confirmDeleteNode": "Delete node \"{{name}}\"?",
|
||||
"customNode": "Custom Node",
|
||||
"dragHint": "Drag nodes to adjust position",
|
||||
"subtitle": "Kubernetes cluster resource topology visualization · drag nodes to adjust layout",
|
||||
"editModeActive": "Edit Mode",
|
||||
"connecting": "Connecting",
|
||||
"selectTarget": "select target node",
|
||||
"selectSource": "select source node",
|
||||
"sourceSelected": "Source node selected: {{name}}, click target node",
|
||||
"connectSelf": "Cannot connect to itself",
|
||||
"connectAdded": "Connection added",
|
||||
"exitEdit": "Exit Edit",
|
||||
"addConnection": "Add Connection",
|
||||
"autoGenerate": "Auto Regenerate",
|
||||
"autoGenerateTitle": "Ignore saved layout and regenerate based on K8s resources (Ingress path→Svc→Deploy→Pod)",
|
||||
"saving": "Saving...",
|
||||
"resetConfirm": "This will clear the saved layout and auto-arrange again",
|
||||
"editToolbarHint": "Double-click node to edit · Click dashed line to delete · Drag to reposition",
|
||||
"totalNodes": "Total Nodes",
|
||||
"connections": "Connections",
|
||||
"custom2": "Custom",
|
||||
"allTypes": "All Types",
|
||||
"noDataWithCluster": "No topology data for this cluster",
|
||||
"noDataSelectCluster": "Select a cluster to view topology",
|
||||
"nodeList": "Node List",
|
||||
"clickToViewDetail": "Click a node to view details",
|
||||
"noNodes": "No nodes",
|
||||
"customSuffix": "Custom",
|
||||
"relatedConnections": "Related Connections",
|
||||
"noConnections": "No related connections",
|
||||
"editNodeTitle": "Edit Node",
|
||||
"addNodeTitle": "Add Custom Node",
|
||||
"editNodeDesc": "Modify node properties",
|
||||
"addNodeDesc": "Add non-Kubernetes resources like databases, external services",
|
||||
"namePlaceholder": "e.g. MySQL Primary, Redis Cluster",
|
||||
"notePlaceholder": "Connection info, usage notes, etc.",
|
||||
"add2": "Add",
|
||||
"nodeUpdated": "Node updated",
|
||||
"nodeAdded": "Node added",
|
||||
"nodeDeleted": "Node deleted",
|
||||
"deleteNodeTitle": "Delete Node",
|
||||
"deleteNodeDesc": "Delete \"{{name}}\"? Related connections will also be removed",
|
||||
"deleteEdgeTitle": "Delete Connection",
|
||||
"deleteEdgeDesc": "Delete this custom connection?",
|
||||
"edgeDeleted": "Connection deleted",
|
||||
"nativeEdgeNoDelete": "Kubernetes native relationships cannot be deleted",
|
||||
"nodeTypeLabel": "Node Type",
|
||||
"nodeNameRequired": "Please enter a node name",
|
||||
"deleteText": "✕ Delete"
|
||||
},
|
||||
"resource": {
|
||||
"configMaps": "ConfigMaps",
|
||||
"secrets": "Secrets",
|
||||
"services": "Services",
|
||||
"ingresses": "Ingresses",
|
||||
"pvcs": "Persistent Volume Claims",
|
||||
"visualEditor": "Visual Editor",
|
||||
"yamlEditor": "YAML Editor",
|
||||
"editResource": "Edit Resource",
|
||||
"visualMode": "Visual Mode",
|
||||
"yamlMode": "YAML Mode",
|
||||
"keyValuePairs": "Key-Value Pairs",
|
||||
"addRow": "Add Row",
|
||||
"key": "Key",
|
||||
"value": "Value",
|
||||
"decodedValue": "Decoded Value",
|
||||
"portConfig": "Port Configuration",
|
||||
"selectorConfig": "Selector Configuration",
|
||||
"routeRules": "Route Rules",
|
||||
"storageConfig": "Storage Configuration",
|
||||
"targetPort": "Target Port",
|
||||
"protocol": "Protocol",
|
||||
"nodePort": "Node Port",
|
||||
"serviceType": "Service Type",
|
||||
"storageClass": "Storage Class",
|
||||
"accessMode": "Access Mode",
|
||||
"size": "Size",
|
||||
"path": "Path",
|
||||
"pathType": "Path Type",
|
||||
"serviceName": "Service Name",
|
||||
"servicePort": "Service Port",
|
||||
"host": "Host",
|
||||
"addPort": "Add Port",
|
||||
"addRoute": "Add Route",
|
||||
"addSelector": "Add Selector",
|
||||
"labelKey": "Label Key",
|
||||
"labelValue": "Label Value",
|
||||
"noPorts": "No port configuration",
|
||||
"noRoutes": "No route rules",
|
||||
"noSelectors": "No selectors",
|
||||
"saveSuccess": "Resource saved successfully",
|
||||
"saveFailed": "Failed to save resource",
|
||||
"parseFailed": "YAML parsing failed, please check the format",
|
||||
"plaintext": "Plaintext",
|
||||
"encoded": "Encoded"
|
||||
},
|
||||
"workload": {
|
||||
"scheduling": "Scheduling",
|
||||
"nodeName": "Node Name",
|
||||
"nodeSelector": "Node Selector",
|
||||
"nodeAffinity": "Node Affinity",
|
||||
"service": "Service",
|
||||
"storage": "Storage",
|
||||
"updateStrategy": "Update Strategy",
|
||||
"rollingUpdate": "Rolling Update",
|
||||
"recreate": "Recreate",
|
||||
"volumes": "Volumes",
|
||||
"mounts": "Mounts",
|
||||
"addVolume": "Add Volume",
|
||||
"addMount": "Add Mount",
|
||||
"portName": "Port Name",
|
||||
"port": "Port",
|
||||
"presetHttp": "HTTP",
|
||||
"presetWebSocket": "WebSocket",
|
||||
"presetGrpc": "gRPC",
|
||||
"capacity": "Capacity",
|
||||
"accessModes": "Access Modes",
|
||||
"mountPath": "Mount Path",
|
||||
"subPath": "Sub Path",
|
||||
"readOnly": "Read Only"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,8 @@
|
||||
"min": "{{count}}分",
|
||||
"minSec": "{{m}}分{{s}}秒",
|
||||
"hour": "{{count}}时",
|
||||
"hourMin": "{{h}}时{{m}}分"
|
||||
"hourMin": "{{h}}时{{m}}分",
|
||||
"minAgo": "{{count}}分钟前"
|
||||
}
|
||||
},
|
||||
"menu": {
|
||||
@@ -1145,5 +1146,207 @@
|
||||
"allType": "全部类型",
|
||||
"pagination": "{{n}}条/页"
|
||||
}
|
||||
},
|
||||
"k8s": {
|
||||
"cluster": {
|
||||
"detail": "集群详情",
|
||||
"version": "版本",
|
||||
"nodeCount": "节点",
|
||||
"created": "创建于",
|
||||
"loadFailed": "加载集群详情失败",
|
||||
"resourceTrend": "资源使用趋势(模拟最近变化)",
|
||||
"refreshMetrics": "刷新节点指标",
|
||||
"resourceDistribution": "资源分布",
|
||||
"now": "现在"
|
||||
},
|
||||
"tab": {
|
||||
"overview": "概览",
|
||||
"nodes": "节点",
|
||||
"pods": "Pod",
|
||||
"deployments": "Deployment",
|
||||
"services": "Service",
|
||||
"ingresses": "Ingress",
|
||||
"events": "事件"
|
||||
},
|
||||
"column": {
|
||||
"name": "名称",
|
||||
"namespace": "命名空间",
|
||||
"role": "角色",
|
||||
"status": "状态",
|
||||
"cpu": "CPU",
|
||||
"memory": "内存",
|
||||
"os": "操作系统",
|
||||
"restartCount": "重启次数",
|
||||
"node": "节点",
|
||||
"replicas": "副本数",
|
||||
"availableReplicas": "可用副本",
|
||||
"image": "镜像",
|
||||
"type": "类型",
|
||||
"ports": "端口",
|
||||
"selector": "Selector",
|
||||
"hosts": "Hosts",
|
||||
"backend": "后端服务",
|
||||
"tls": "TLS",
|
||||
"createdAt": "创建时间"
|
||||
},
|
||||
"empty": {
|
||||
"nodes": "暂无节点数据",
|
||||
"pods": "暂无Pod数据",
|
||||
"deployments": "暂无Deployment数据",
|
||||
"services": "暂无Service数据",
|
||||
"ingresses": "暂无Ingress数据",
|
||||
"events": "暂无事件"
|
||||
},
|
||||
"label": {
|
||||
"cpu": "CPU",
|
||||
"memory": "内存",
|
||||
"cpuPercent": "CPU(%)",
|
||||
"memoryPercent": "内存(%)",
|
||||
"enabled": "已启用",
|
||||
"disabled": "未启用",
|
||||
"cores": "核"
|
||||
},
|
||||
"topology": {
|
||||
"title": "架构拓扑",
|
||||
"selectCluster": "选择集群",
|
||||
"allResources": "全部资源",
|
||||
"editMode": "编辑模式",
|
||||
"connectMode": "连接模式",
|
||||
"saveLayout": "保存布局",
|
||||
"resetLayout": "重置布局",
|
||||
"addNode": "添加节点",
|
||||
"editNode": "编辑节点",
|
||||
"deleteNode": "删除节点",
|
||||
"nodeName": "节点名称",
|
||||
"nodeType": "节点类型",
|
||||
"nodeNote": "节点备注",
|
||||
"searchPlaceholder": "搜索节点...",
|
||||
"noData": "暂无拓扑数据,请选择集群",
|
||||
"layoutSaved": "布局已保存",
|
||||
"layoutSaveFailed": "保存失败",
|
||||
"layoutReset": "布局已重置",
|
||||
"connectHint": "点击两个节点创建连接",
|
||||
"cancelConnect": "取消连线",
|
||||
"confirmDeleteNode": "确定删除节点「{{name}}」?",
|
||||
"customNode": "自定义节点",
|
||||
"dragHint": "拖拽节点调整位置",
|
||||
"subtitle": "Kubernetes 集群资源拓扑可视化 · 拖拽节点调整布局",
|
||||
"editModeActive": "编辑模式",
|
||||
"connecting": "连线中",
|
||||
"selectTarget": "选择目标节点",
|
||||
"selectSource": "选择源节点",
|
||||
"sourceSelected": "已选择源节点: {{name}},请点击目标节点",
|
||||
"connectSelf": "不能连接自身",
|
||||
"connectAdded": "连线已添加",
|
||||
"exitEdit": "退出编辑",
|
||||
"addConnection": "添加连线",
|
||||
"autoGenerate": "重新自动生成",
|
||||
"autoGenerateTitle": "忽略保存的布局,重新根据 K8s 资源自动生成关联 (Ingress路径→Svc→Deploy→Pod)",
|
||||
"saving": "保存中...",
|
||||
"resetConfirm": "将清除已保存的布局并重新自动排列",
|
||||
"editToolbarHint": "双击节点编辑 · 点击虚线删除 · 拖拽调整位置",
|
||||
"totalNodes": "总节点数",
|
||||
"connections": "连接数",
|
||||
"custom2": "自定义",
|
||||
"allTypes": "全部类型",
|
||||
"noDataWithCluster": "该集群暂无拓扑数据",
|
||||
"noDataSelectCluster": "请选择集群查看拓扑",
|
||||
"nodeList": "节点列表",
|
||||
"clickToViewDetail": "点击节点查看详情",
|
||||
"noNodes": "暂无节点",
|
||||
"customSuffix": "自定义",
|
||||
"relatedConnections": "关联连接",
|
||||
"noConnections": "无关联连接",
|
||||
"editNodeTitle": "编辑节点",
|
||||
"addNodeTitle": "添加自定义节点",
|
||||
"editNodeDesc": "修改节点属性",
|
||||
"addNodeDesc": "添加数据库、外部服务等非 Kubernetes 资源节点",
|
||||
"namePlaceholder": "如 MySQL 主库、Redis 集群",
|
||||
"notePlaceholder": "连接信息、用途说明等",
|
||||
"add2": "添加",
|
||||
"nodeUpdated": "节点已更新",
|
||||
"nodeAdded": "节点已添加",
|
||||
"nodeDeleted": "节点已删除",
|
||||
"deleteNodeTitle": "删除节点",
|
||||
"deleteNodeDesc": "确定删除「{{name}}」?相关连线也会一并删除",
|
||||
"deleteEdgeTitle": "删除连线",
|
||||
"deleteEdgeDesc": "确定删除此自定义连线?",
|
||||
"edgeDeleted": "连线已删除",
|
||||
"nativeEdgeNoDelete": "Kubernetes 原生关系不可删除",
|
||||
"nodeTypeLabel": "节点类型",
|
||||
"nodeNameRequired": "请输入节点名称",
|
||||
"deleteText": "✕ 删除"
|
||||
},
|
||||
"resource": {
|
||||
"configMaps": "配置映射",
|
||||
"secrets": "密钥",
|
||||
"services": "服务",
|
||||
"ingresses": "路由",
|
||||
"pvcs": "持久卷声明",
|
||||
"visualEditor": "可视化编辑",
|
||||
"yamlEditor": "YAML 编辑",
|
||||
"editResource": "编辑资源",
|
||||
"visualMode": "可视化模式",
|
||||
"yamlMode": "YAML 模式",
|
||||
"keyValuePairs": "键值对",
|
||||
"addRow": "添加行",
|
||||
"key": "键",
|
||||
"value": "值",
|
||||
"decodedValue": "解码值",
|
||||
"portConfig": "端口配置",
|
||||
"selectorConfig": "选择器配置",
|
||||
"routeRules": "路由规则",
|
||||
"storageConfig": "存储配置",
|
||||
"targetPort": "目标端口",
|
||||
"protocol": "协议",
|
||||
"nodePort": "节点端口",
|
||||
"serviceType": "服务类型",
|
||||
"storageClass": "存储类",
|
||||
"accessMode": "访问模式",
|
||||
"size": "大小",
|
||||
"path": "路径",
|
||||
"pathType": "路径类型",
|
||||
"serviceName": "服务名称",
|
||||
"servicePort": "服务端口",
|
||||
"host": "主机",
|
||||
"addPort": "添加端口",
|
||||
"addRoute": "添加路由",
|
||||
"addSelector": "添加选择器",
|
||||
"labelKey": "标签键",
|
||||
"labelValue": "标签值",
|
||||
"noPorts": "暂无端口配置",
|
||||
"noRoutes": "暂无路由规则",
|
||||
"noSelectors": "暂无选择器",
|
||||
"saveSuccess": "资源保存成功",
|
||||
"saveFailed": "资源保存失败",
|
||||
"parseFailed": "YAML 解析失败,请检查格式",
|
||||
"plaintext": "明文",
|
||||
"encoded": "编码"
|
||||
},
|
||||
"workload": {
|
||||
"scheduling": "调度",
|
||||
"nodeName": "指定节点",
|
||||
"nodeSelector": "节点选择器",
|
||||
"nodeAffinity": "节点亲和性",
|
||||
"service": "Service",
|
||||
"storage": "存储",
|
||||
"updateStrategy": "更新策略",
|
||||
"rollingUpdate": "滚动更新",
|
||||
"recreate": "重建",
|
||||
"volumes": "数据卷",
|
||||
"mounts": "挂载点",
|
||||
"addVolume": "添加数据卷",
|
||||
"addMount": "添加挂载",
|
||||
"portName": "端口名称",
|
||||
"port": "端口",
|
||||
"presetHttp": "HTTP",
|
||||
"presetWebSocket": "WebSocket",
|
||||
"presetGrpc": "gRPC",
|
||||
"capacity": "容量",
|
||||
"accessModes": "访问模式",
|
||||
"mountPath": "挂载路径",
|
||||
"subPath": "子路径",
|
||||
"readOnly": "只读"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user