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>
|
||||
|
||||
Reference in new issue
Block a user