feat(i18n): CredFormDialog + ClusterComparePage 全量国际化 — 75 新增翻译键
- CredFormDialog.tsx: 凭据表单弹框全量迁移 (deploy.cred.form.* 37 键)
SSH/Docker/TLS/AKSK/Database/API/Generic 7 种类型表单字段全覆盖
- ClusterComparePage.tsx: 多集群对比看板全量迁移 (k8s.compare.* 35 键)
图表标签/表头/状态/空状态/标题全部国际化
- 新增 common.username/password/port 3 个通用键
- 翻译键总数 6182 → 6257 (zh-CN/en-US 完全对齐)
- tsc -b=0, npm build=✅ (20.76s), ESLint=0, 17/17 集成测试通过
This commit is contained in:
1 parent
88122a72a2
commit
777d9e4e71
4 files changed
+257
-95
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Key, Eye, EyeOff } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -19,6 +20,7 @@ export interface CredFormDialogProps {
|
||||
|
||||
// 凭据新增/编辑弹窗 // Logan.Li
|
||||
export function CredFormDialog({ open, onClose, onSaved, editData }: CredFormDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const isEdit = !!editData;
|
||||
const [name, setName] = useState('');
|
||||
const [varName, setVarName] = useState('');
|
||||
@@ -95,7 +97,7 @@ export function CredFormDialog({ open, onClose, onSaved, editData }: CredFormDia
|
||||
const toggleShow = (key: string) => setShowMap(prev => ({ ...prev, [key]: !prev[key] }));
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!name.trim()) { toast.error('请填写凭据名称'); return; }
|
||||
if (!name.trim()) { toast.error(t('deploy.cred.form.nameRequired')); return; }
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const payload: Record<string, unknown> = {
|
||||
@@ -141,16 +143,16 @@ export function CredFormDialog({ open, onClose, onSaved, editData }: CredFormDia
|
||||
|
||||
if (isEdit) {
|
||||
await credentialApi.update(editData!.id, payload);
|
||||
toast.success('已更新');
|
||||
toast.success(t('deploy.cred.form.updated'));
|
||||
} else {
|
||||
await credentialApi.create(payload);
|
||||
toast.success('已创建');
|
||||
toast.success(t('deploy.cred.form.created'));
|
||||
}
|
||||
onSaved();
|
||||
onClose();
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { message?: string } } };
|
||||
toast.error(err?.response?.data?.message || '操作失败');
|
||||
toast.error(err?.response?.data?.message || t('deploy.cred.form.operationFailed'));
|
||||
} finally { setSubmitting(false); }
|
||||
};
|
||||
|
||||
@@ -190,29 +192,29 @@ export function CredFormDialog({ open, onClose, onSaved, editData }: CredFormDia
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
||||
<TIicon className={`h-5 w-5 ${TI?.color || ''}`} />
|
||||
{isEdit ? '编辑凭据' : '新增凭据'}
|
||||
{isEdit ? t('deploy.cred.form.editTitle') : t('deploy.cred.form.newTitle')}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-[var(--dg-text-muted)] text-xs">
|
||||
{TI?.desc || '管理敏感凭据信息'}
|
||||
{TI?.desc || t('deploy.cred.descDefault')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-2">
|
||||
{/* 基本信息 // Logan.Li */}
|
||||
<div className="border-b border-[var(--dg-border)] pb-3">
|
||||
<div className="text-xs font-medium text-[var(--dg-text-secondary)] mb-2">基本信息</div>
|
||||
<div className="text-xs font-medium text-[var(--dg-text-secondary)] mb-2">{t('deploy.cred.form.basicInfo')}</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">凭据名称 <span className="dg-text-red">*</span></Label>
|
||||
<Input value={name} onChange={e => setName(e.target.value)} placeholder="如:生产环境 SSH"
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">{t('deploy.cred.credName')} <span className="dg-text-red">*</span></Label>
|
||||
<Input value={name} onChange={e => setName(e.target.value)} placeholder={t('deploy.cred.form.credNamePlaceholder')}
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">变量别名</Label>
|
||||
<Input value={varName} onChange={e => setVarName(e.target.value)} placeholder="如:MYSQL_PROD"
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">{t('deploy.cred.form.varAlias')}</Label>
|
||||
<Input value={varName} onChange={e => setVarName(e.target.value)} placeholder={t('deploy.cred.form.varAliasPlaceholder')}
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] font-mono text-sm" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">类型{!isEdit && <span className="dg-text-red"> *</span>}</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">{t('common.type')}{!isEdit && <span className="dg-text-red"> *</span>}</Label>
|
||||
<Select value={secretType} onValueChange={(v) => setSecretType(v as SecretType)} disabled={isEdit}>
|
||||
<SelectTrigger className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||
<SelectValue />
|
||||
@@ -227,14 +229,14 @@ export function CredFormDialog({ open, onClose, onSaved, editData }: CredFormDia
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">标签</Label>
|
||||
<Input value={tags} onChange={e => setTags(e.target.value)} placeholder="生产环境, 阿里云"
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">{t('deploy.cred.form.tagsLabel')}</Label>
|
||||
<Input value={tags} onChange={e => setTags(e.target.value)} placeholder={t('deploy.cred.form.tagsPlaceholder')}
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">描述</Label>
|
||||
<Input value={description} onChange={e => setDescription(e.target.value)} placeholder="可选备注信息"
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">{t('common.description')}</Label>
|
||||
<Input value={description} onChange={e => setDescription(e.target.value)} placeholder={t('deploy.cred.form.descPlaceholder')}
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -242,33 +244,33 @@ export function CredFormDialog({ open, onClose, onSaved, editData }: CredFormDia
|
||||
{/* 类型专属字段 // Logan.Li */}
|
||||
<div>
|
||||
<div className="text-xs font-medium text-[var(--dg-text-secondary)] mb-2 flex items-center gap-2">
|
||||
<TIicon className="h-3.5 w-3.5" />{TI?.label || secretType} 配置
|
||||
{isEdit && <span className="text-[var(--dg-text-muted)]">(仅填写需要修改的字段)</span>}
|
||||
<TIicon className="h-3.5 w-3.5" />{TI?.label || secretType} {t('deploy.cred.form.typeConfig')}
|
||||
{isEdit && <span className="text-[var(--dg-text-muted)]">{t('deploy.cred.form.editHint')}</span>}
|
||||
</div>
|
||||
|
||||
{secretType === 'ssh' && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">SSH 用户</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">{t('deploy.cred.form.sshUser')}</Label>
|
||||
<Input value={sshUser} onChange={e => setSshUser(e.target.value)} placeholder="root"
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">SSH 端口</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">{t('deploy.cred.form.sshPort')}</Label>
|
||||
<Input value={sshPort} onChange={e => setSshPort(e.target.value)} placeholder="22" type="number"
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<FieldWithToggle label="SSH 密码" value={sshPassword} onChange={setSshPassword}
|
||||
placeholder="密码(或留空用密钥)" fieldKey="ssh_pwd" />
|
||||
<FieldWithToggle label={t('deploy.cred.form.sshPassword')} value={sshPassword} onChange={setSshPassword}
|
||||
placeholder={t('deploy.cred.form.sshPwdPlaceholder')} fieldKey="ssh_pwd" />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<FieldWithToggle label="私钥内容" value={sshPrivateKey} onChange={setSshPrivateKey}
|
||||
placeholder="粘贴 SSH 私钥..." fieldKey="ssh_pk" isLong />
|
||||
<FieldWithToggle label={t('deploy.cred.form.privateKey')} value={sshPrivateKey} onChange={setSshPrivateKey}
|
||||
placeholder={t('deploy.cred.form.privateKeyPlaceholder')} fieldKey="ssh_pk" isLong />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<FieldWithToggle label="公钥内容" value={sshPublicKey} onChange={setSshPublicKey}
|
||||
placeholder="粘贴 SSH 公钥..." fieldKey="ssh_pub" isLong />
|
||||
<FieldWithToggle label={t('deploy.cred.form.publicKey')} value={sshPublicKey} onChange={setSshPublicKey}
|
||||
placeholder={t('deploy.cred.form.publicKeyPlaceholder')} fieldKey="ssh_pub" isLong />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -276,28 +278,28 @@ export function CredFormDialog({ open, onClose, onSaved, editData }: CredFormDia
|
||||
{secretType === 'docker' && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="col-span-2 space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">Registry 地址 <span className="dg-text-red">*</span></Label>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">{t('deploy.cred.form.registryAddr')} <span className="dg-text-red">*</span></Label>
|
||||
<Input value={dockerServer} onChange={e => setDockerServer(e.target.value)} placeholder="registry.example.com"
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] font-mono" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">用户名</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">{t('common.username')}</Label>
|
||||
<Input value={dockerUsername} onChange={e => setDockerUsername(e.target.value)} placeholder="admin"
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
</div>
|
||||
<div><FieldWithToggle label="密码" value={dockerPassword} onChange={setDockerPassword}
|
||||
placeholder="Registry 密码" fieldKey="docker_pwd" required /></div>
|
||||
<div><FieldWithToggle label={t('common.password')} value={dockerPassword} onChange={setDockerPassword}
|
||||
placeholder={t('deploy.cred.form.registryPwdPlaceholder')} fieldKey="docker_pwd" required /></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{secretType === 'tls' && (
|
||||
<div className="space-y-3">
|
||||
<FieldWithToggle label="证书 (PEM)" value={tlsCert} onChange={setTlsCert}
|
||||
<FieldWithToggle label={t('deploy.cred.form.certPem')} value={tlsCert} onChange={setTlsCert}
|
||||
placeholder="-----BEGIN CERTIFICATE-----" fieldKey="tls_cert" isLong required />
|
||||
<FieldWithToggle label="私钥 (PEM)" value={tlsKey} onChange={setTlsKey}
|
||||
<FieldWithToggle label={t('deploy.cred.form.keyPem')} value={tlsKey} onChange={setTlsKey}
|
||||
placeholder="-----BEGIN PRIVATE KEY-----" fieldKey="tls_key" isLong required />
|
||||
<FieldWithToggle label="CA 证书" value={tlsCa} onChange={setTlsCa}
|
||||
placeholder="CA 证书内容..." fieldKey="tls_ca" isLong />
|
||||
<FieldWithToggle label={t('deploy.cred.form.caCert')} value={tlsCa} onChange={setTlsCa}
|
||||
placeholder={t('deploy.cred.form.caCertPlaceholder')} fieldKey="tls_ca" isLong />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -310,7 +312,7 @@ export function CredFormDialog({ open, onClose, onSaved, editData }: CredFormDia
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<FieldWithToggle label="Secret Access Key" value={akskSecretKey} onChange={setAkskSecretKey}
|
||||
placeholder="输入 Secret Key" fieldKey="aksk_sk" required />
|
||||
placeholder={t('deploy.cred.form.sshPwdPlaceholder')} fieldKey="aksk_sk" required />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -318,7 +320,7 @@ export function CredFormDialog({ open, onClose, onSaved, editData }: CredFormDia
|
||||
{secretType === 'database' && (
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">数据库类型</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">{t('deploy.cred.form.dbType')}</Label>
|
||||
<Select value={dbType} onValueChange={setDbType}>
|
||||
<SelectTrigger className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
||||
<SelectValue />
|
||||
@@ -334,38 +336,38 @@ export function CredFormDialog({ open, onClose, onSaved, editData }: CredFormDia
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">主机 <span className="dg-text-red">*</span></Label>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">{t('deploy.cred.form.host')} <span className="dg-text-red">*</span></Label>
|
||||
<Input value={dbHost} onChange={e => setDbHost(e.target.value)} placeholder="10.0.0.1"
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] font-mono" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">端口</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">{t('common.port')}</Label>
|
||||
<Input value={dbPort} onChange={e => setDbPort(e.target.value)} placeholder="3306" type="number"
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">数据库名</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">{t('deploy.cred.form.dbName')}</Label>
|
||||
<Input value={dbName} onChange={e => setDbName(e.target.value)} placeholder="aiops"
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">用户名</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">{t('common.username')}</Label>
|
||||
<Input value={dbUsername} onChange={e => setDbUsername(e.target.value)} placeholder="root"
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
|
||||
</div>
|
||||
<div><FieldWithToggle label="密码" value={dbPassword} onChange={setDbPassword}
|
||||
placeholder="数据库密码" fieldKey="db_pwd" /></div>
|
||||
<div><FieldWithToggle label={t('common.password')} value={dbPassword} onChange={setDbPassword}
|
||||
placeholder={t('deploy.cred.form.dbPwdPlaceholder')} fieldKey="db_pwd" /></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{secretType === 'api' && (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">API 端点</Label>
|
||||
<Label className="text-[var(--dg-text-secondary)] text-xs">{t('deploy.cred.form.apiEndpoint')}</Label>
|
||||
<Input value={apiEndpoint} onChange={e => setApiEndpoint(e.target.value)} placeholder="https://api.example.com"
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] font-mono" />
|
||||
</div>
|
||||
<FieldWithToggle label="API Key / Token" value={apiKey} onChange={setApiKey}
|
||||
<FieldWithToggle label={t('deploy.cred.form.apiKeyToken')} value={apiKey} onChange={setApiKey}
|
||||
placeholder="sk-xxxx..." fieldKey="api_key" required />
|
||||
</div>
|
||||
)}
|
||||
@@ -378,15 +380,15 @@ export function CredFormDialog({ open, onClose, onSaved, editData }: CredFormDia
|
||||
className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] font-mono" />
|
||||
</div>
|
||||
<FieldWithToggle label="Value" value={genericValue} onChange={setGenericValue}
|
||||
placeholder="密钥值" fieldKey="generic_val" required />
|
||||
placeholder={t('deploy.cred.form.secretValuePlaceholder')} fieldKey="generic_val" required />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>取消</Button>
|
||||
<Button variant="outline" onClick={onClose}>{t('common.cancel')}</Button>
|
||||
<Button onClick={handleSubmit} disabled={submitting} className="bg-[var(--dg-blue)] hover:bg-[var(--dg-blue)]/80">
|
||||
{submitting ? '保存中...' : (isEdit ? '更新' : '创建')}
|
||||
{submitting ? t('deploy.cred.form.saving') : (isEdit ? t('common.update') : t('common.create'))}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { StatStrip } from '@/components/ui/stat-strip';
|
||||
@@ -30,6 +31,7 @@ import type { JsonData } from '@/types/common';
|
||||
const CHART_COLORS = ['#3B82F6', '#207060', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#14B8A6', '#F97316'];
|
||||
|
||||
export default function ClusterComparePage() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState<JsonData>(null);
|
||||
@@ -41,13 +43,13 @@ export default function ClusterComparePage() {
|
||||
try {
|
||||
const res: JsonData = await kubernetesApi.getMultiClusterComparison();
|
||||
setData(res || null);
|
||||
} catch (e) {
|
||||
setError(e?.message || '加载失败');
|
||||
} catch {
|
||||
setError(t('k8s.compare.loadFailed'));
|
||||
setData(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => { loadData(); }, [loadData]);
|
||||
|
||||
@@ -57,7 +59,7 @@ export default function ClusterComparePage() {
|
||||
// 图表数据准备
|
||||
const resourceBarData = clusters.map((c: JsonData) => ({
|
||||
name: c.code || c.name,
|
||||
节点: c.node_count || 0,
|
||||
[t('k8s.compare.chartNodes')]: c.node_count || 0,
|
||||
Pod: c.pod_count || 0,
|
||||
Deployment: c.deployment_count || 0,
|
||||
Service: c.service_count || 0,
|
||||
@@ -65,17 +67,17 @@ export default function ClusterComparePage() {
|
||||
|
||||
const usageBarData = clusters.map((c: JsonData) => ({
|
||||
name: c.code || c.name,
|
||||
'CPU %': Number((c.cpu_usage || 0).toFixed(1)),
|
||||
'内存 %': Number((c.memory_usage || 0).toFixed(1)),
|
||||
[t('k8s.compare.cpuPercent')]: Number((c.cpu_usage || 0).toFixed(1)),
|
||||
[t('k8s.compare.memPercent')]: Number((c.memory_usage || 0).toFixed(1)),
|
||||
}));
|
||||
|
||||
const radarData = clusters.length > 0 ? [
|
||||
{ metric: '节点数', ...Object.fromEntries(clusters.map((c: JsonData) => [c.code || c.name, c.node_count || 0])) },
|
||||
{ metric: 'Pod数', ...Object.fromEntries(clusters.map((c: JsonData) => [c.code || c.name, Math.min(c.pod_count || 0, 100)])) },
|
||||
{ metric: '部署数', ...Object.fromEntries(clusters.map((c: JsonData) => [c.code || c.name, c.deployment_count || 0])) },
|
||||
{ metric: 'CPU使用', ...Object.fromEntries(clusters.map((c: JsonData) => [c.code || c.name, Number((c.cpu_usage || 0).toFixed(1))])) },
|
||||
{ metric: '内存使用', ...Object.fromEntries(clusters.map((c: JsonData) => [c.code || c.name, Number((c.memory_usage || 0).toFixed(1))])) },
|
||||
{ metric: 'CPU核数', ...Object.fromEntries(clusters.map((c: JsonData) => [c.code || c.name, c.total_cpu_cores || 0])) },
|
||||
{ metric: t('k8s.compare.chartNodes'), ...Object.fromEntries(clusters.map((c: JsonData) => [c.code || c.name, c.node_count || 0])) },
|
||||
{ metric: t('k8s.compare.chartPods'), ...Object.fromEntries(clusters.map((c: JsonData) => [c.code || c.name, Math.min(c.pod_count || 0, 100)])) },
|
||||
{ metric: t('k8s.compare.chartDeploy'), ...Object.fromEntries(clusters.map((c: JsonData) => [c.code || c.name, c.deployment_count || 0])) },
|
||||
{ metric: t('k8s.compare.chartCpuUsage'), ...Object.fromEntries(clusters.map((c: JsonData) => [c.code || c.name, Number((c.cpu_usage || 0).toFixed(1))])) },
|
||||
{ metric: t('k8s.compare.chartMemUsage'), ...Object.fromEntries(clusters.map((c: JsonData) => [c.code || c.name, Number((c.memory_usage || 0).toFixed(1))])) },
|
||||
{ metric: t('k8s.compare.chartCpuCores'), ...Object.fromEntries(clusters.map((c: JsonData) => [c.code || c.name, c.total_cpu_cores || 0])) },
|
||||
] : [];
|
||||
|
||||
const nodeRolePie = clusters.flatMap((c: JsonData) => [
|
||||
@@ -96,12 +98,12 @@ export default function ClusterComparePage() {
|
||||
};
|
||||
|
||||
// 格式化同步时间
|
||||
const fmtTime = (t: string | null | undefined) => {
|
||||
if (!t) return '从未同步';
|
||||
const fmtTime = (ts: string | null | undefined) => {
|
||||
if (!ts) return t('k8s.compare.neverSynced');
|
||||
try {
|
||||
const d = new Date(t);
|
||||
const d = new Date(ts);
|
||||
return d.toLocaleString('zh-CN', { hour12: false });
|
||||
} catch { return t; }
|
||||
} catch { return ts; }
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -111,19 +113,19 @@ export default function ClusterComparePage() {
|
||||
<div className="flex items-center gap-3">
|
||||
<Layers className="h-5 w-5 dg-text-blue" />
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold">多集群资源对比看板</h1>
|
||||
<p className="text-xs dg-text-muted">跨集群资源用量、节点分布、健康状态对比</p>
|
||||
<h1 className="text-lg font-semibold">{t('k8s.compare.title')}</h1>
|
||||
<p className="text-xs dg-text-muted">{t('k8s.compare.subtitle')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={loadData} disabled={loading}
|
||||
className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]">
|
||||
<RefreshCw className={`h-3.5 w-3.5 mr-1.5 ${loading ? 'animate-spin' : ''}`} />
|
||||
刷新
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
<Link to="/kubernetes">
|
||||
<Button variant="outline" size="sm" className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]">
|
||||
<Cloud className="h-3.5 w-3.5 mr-1.5" />集群列表
|
||||
<Cloud className="h-3.5 w-3.5 mr-1.5" />{t('k8s.compare.clusterList')}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
@@ -139,8 +141,8 @@ export default function ClusterComparePage() {
|
||||
{/* 概览卡片 */}
|
||||
<StatStrip
|
||||
items={[
|
||||
{ label: '集群', value: totals.cluster_count || 0, color: 'blue' },
|
||||
{ label: '节点', value: totals.node_count || 0, color: 'emerald' },
|
||||
{ label: t('k8s.compare.statCluster'), value: totals.cluster_count || 0, color: 'blue' },
|
||||
{ label: t('k8s.compare.statNodes'), value: totals.node_count || 0, color: 'emerald' },
|
||||
{ label: 'Pod', value: totals.pod_count || 0, color: 'purple' },
|
||||
{ label: 'Deploy', value: totals.deployment_count || 0, color: 'amber' },
|
||||
{ label: 'Service', value: totals.service_count || 0, color: 'cyan' },
|
||||
@@ -151,10 +153,10 @@ export default function ClusterComparePage() {
|
||||
<Card className="bg-[var(--dg-bg-secondary)]/60 border-[var(--dg-border)]">
|
||||
<CardContent className="py-12 text-center">
|
||||
<Cloud className="h-12 w-12 dg-text-muted mx-auto mb-3" />
|
||||
<p className="dg-text-muted">暂无集群数据,请先添加 Kubernetes 集群并同步</p>
|
||||
<p className="dg-text-muted">{t('k8s.compare.noData')}</p>
|
||||
<Link to="/kubernetes" className="inline-block mt-3">
|
||||
<Button variant="outline" size="sm" className="border-[var(--dg-border)]">
|
||||
去添加集群 <ChevronRight className="h-3.5 w-3.5 ml-1" />
|
||||
{t('k8s.compare.goAddCluster')} <ChevronRight className="h-3.5 w-3.5 ml-1" />
|
||||
</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
@@ -169,7 +171,7 @@ export default function ClusterComparePage() {
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Layers className="h-4 w-4 dg-text-blue" />
|
||||
<h3 className="text-sm font-semibold">资源数量对比</h3>
|
||||
<h3 className="text-sm font-semibold">{t('k8s.compare.resourceCompare')}</h3>
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={280}>
|
||||
<BarChart data={resourceBarData} margin={{ top: 5, right: 10, left: -15, bottom: 5 }}>
|
||||
@@ -178,7 +180,7 @@ export default function ClusterComparePage() {
|
||||
<YAxis tick={{ fill: '#94a3b8', fontSize: 11 }} />
|
||||
<Tooltip contentStyle={{ background: '#0f172a', border: '1px solid #334155', borderRadius: 8, fontSize: 12 }} />
|
||||
<Legend wrapperStyle={{ fontSize: 11 }} />
|
||||
<Bar dataKey="节点" fill={CHART_COLORS[1]} radius={[3, 3, 0, 0]} />
|
||||
<Bar dataKey={t('k8s.compare.chartNodes')} fill={CHART_COLORS[1]} radius={[3, 3, 0, 0]} />
|
||||
<Bar dataKey="Pod" fill={CHART_COLORS[0]} radius={[3, 3, 0, 0]} />
|
||||
<Bar dataKey="Deployment" fill={CHART_COLORS[2]} radius={[3, 3, 0, 0]} />
|
||||
<Bar dataKey="Service" fill={CHART_COLORS[5]} radius={[3, 3, 0, 0]} />
|
||||
@@ -191,7 +193,7 @@ export default function ClusterComparePage() {
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Activity className="h-4 w-4 dg-text-green" />
|
||||
<h3 className="text-sm font-semibold">CPU / 内存使用率对比</h3>
|
||||
<h3 className="text-sm font-semibold">{t('k8s.compare.usageCompare')}</h3>
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={280}>
|
||||
<BarChart data={usageBarData} margin={{ top: 5, right: 10, left: -15, bottom: 5 }}>
|
||||
@@ -200,8 +202,8 @@ export default function ClusterComparePage() {
|
||||
<YAxis tick={{ fill: '#94a3b8', fontSize: 11 }} unit="%" />
|
||||
<Tooltip contentStyle={{ background: '#0f172a', border: '1px solid #334155', borderRadius: 8, fontSize: 12 }} />
|
||||
<Legend wrapperStyle={{ fontSize: 11 }} />
|
||||
<Bar dataKey="CPU %" fill={CHART_COLORS[0]} radius={[3, 3, 0, 0]} />
|
||||
<Bar dataKey="内存 %" fill={CHART_COLORS[1]} radius={[3, 3, 0, 0]} />
|
||||
<Bar dataKey={t('k8s.compare.cpuPercent')} fill={CHART_COLORS[0]} radius={[3, 3, 0, 0]} />
|
||||
<Bar dataKey={t('k8s.compare.memPercent')} fill={CHART_COLORS[1]} radius={[3, 3, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
@@ -215,7 +217,7 @@ export default function ClusterComparePage() {
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<TrendingUp className="h-4 w-4 dg-text-purple" />
|
||||
<h3 className="text-sm font-semibold">资源雷达对比</h3>
|
||||
<h3 className="text-sm font-semibold">{t('k8s.compare.radarCompare')}</h3>
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={320}>
|
||||
<RadarChart data={radarData}>
|
||||
@@ -242,7 +244,7 @@ export default function ClusterComparePage() {
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Server className="h-4 w-4 dg-text-yellow" />
|
||||
<h3 className="text-sm font-semibold">节点角色分布</h3>
|
||||
<h3 className="text-sm font-semibold">{t('k8s.compare.nodeRoleDist')}</h3>
|
||||
</div>
|
||||
{nodeRolePie.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height={320}>
|
||||
@@ -260,7 +262,7 @@ export default function ClusterComparePage() {
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="h-[320px] flex items-center justify-center dg-text-muted text-sm">无节点角色数据</div>
|
||||
<div className="h-[320px] flex items-center justify-center dg-text-muted text-sm">{t('k8s.compare.noNodeRoleData')}</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -272,25 +274,25 @@ export default function ClusterComparePage() {
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<HardDrive className="h-4 w-4 dg-text-cyan" />
|
||||
<h3 className="text-sm font-semibold">详细对比</h3>
|
||||
<span className="text-xs dg-text-muted ml-2">点击行可进入集群概览</span>
|
||||
<h3 className="text-sm font-semibold">{t('k8s.compare.detailCompare')}</h3>
|
||||
<span className="text-xs dg-text-muted ml-2">{t('k8s.compare.clickRowHint')}</span>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
|
||||
<TableHead className="dg-text-secondary">集群</TableHead>
|
||||
<TableHead className="dg-text-secondary">状态</TableHead>
|
||||
<TableHead className="dg-text-secondary text-right">节点</TableHead>
|
||||
<TableHead className="dg-text-secondary">{t('k8s.compare.colCluster')}</TableHead>
|
||||
<TableHead className="dg-text-secondary">{t('common.status')}</TableHead>
|
||||
<TableHead className="dg-text-secondary text-right">{t('k8s.compare.colNodes')}</TableHead>
|
||||
<TableHead className="dg-text-secondary text-right">Pod</TableHead>
|
||||
<TableHead className="dg-text-secondary text-right">Deployment</TableHead>
|
||||
<TableHead className="dg-text-secondary text-right">Service</TableHead>
|
||||
<TableHead className="dg-text-secondary text-right">CPU 核数</TableHead>
|
||||
<TableHead className="dg-text-secondary text-right">内存</TableHead>
|
||||
<TableHead className="dg-text-secondary text-right">CPU 使用率</TableHead>
|
||||
<TableHead className="dg-text-secondary text-right">内存使用率</TableHead>
|
||||
<TableHead className="dg-text-secondary">角色</TableHead>
|
||||
<TableHead className="dg-text-secondary">同步时间</TableHead>
|
||||
<TableHead className="dg-text-secondary text-right">{t('k8s.compare.colCpuCores')}</TableHead>
|
||||
<TableHead className="dg-text-secondary text-right">{t('k8s.compare.colMemory')}</TableHead>
|
||||
<TableHead className="dg-text-secondary text-right">{t('k8s.compare.colCpuUsage')}</TableHead>
|
||||
<TableHead className="dg-text-secondary text-right">{t('k8s.compare.colMemUsage')}</TableHead>
|
||||
<TableHead className="dg-text-secondary">{t('k8s.compare.colRole')}</TableHead>
|
||||
<TableHead className="dg-text-secondary">{t('k8s.compare.colSyncTime')}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -305,7 +307,7 @@ export default function ClusterComparePage() {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline" className={`text-xs ${statusColorMap[c.status] || statusColorMap.error}`}>
|
||||
{c.status === 'healthy' ? '✓ 健康' : c.status === 'warning' ? '⚠ 警告' : '✗ 异常'}
|
||||
{c.status === 'healthy' ? t('k8s.compare.statusHealthy') : c.status === 'warning' ? t('k8s.compare.statusWarning') : t('k8s.compare.statusError')}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono text-[var(--dg-text-secondary)]">{c.node_count}</TableCell>
|
||||
|
||||
@@ -519,7 +519,10 @@
|
||||
"yesterday": "Yesterday",
|
||||
"zone": "Zone",
|
||||
"zoomIn": "Zoom In",
|
||||
"zoomOut": "Zoom Out"
|
||||
"zoomOut": "Zoom Out",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"port": "Port"
|
||||
},
|
||||
"menu": {
|
||||
"dashboard": "Dashboard",
|
||||
@@ -1412,7 +1415,46 @@
|
||||
"selectDockerPwd": "Select Docker password credential",
|
||||
"selectApiCred": "Select credential (API/password etc.)",
|
||||
"selectCred": "Select credential",
|
||||
"linkedCred": "Linked Credential"
|
||||
"linkedCred": "Linked Credential",
|
||||
"form": {
|
||||
"editTitle": "Edit Credential",
|
||||
"newTitle": "Add Credential",
|
||||
"basicInfo": "Basic Information",
|
||||
"credNamePlaceholder": "e.g. Production SSH",
|
||||
"varAlias": "Variable Alias",
|
||||
"varAliasPlaceholder": "e.g. MYSQL_PROD",
|
||||
"tagsPlaceholder": "production, aliyun",
|
||||
"descPlaceholder": "Optional remarks",
|
||||
"typeConfig": "Configuration",
|
||||
"editHint": "(only fill fields you want to change)",
|
||||
"sshUser": "SSH User",
|
||||
"sshPort": "SSH Port",
|
||||
"sshPassword": "SSH Password",
|
||||
"sshPwdPlaceholder": "Password (or leave empty to use key)",
|
||||
"privateKey": "Private Key",
|
||||
"privateKeyPlaceholder": "Paste SSH private key...",
|
||||
"publicKey": "Public Key",
|
||||
"publicKeyPlaceholder": "Paste SSH public key...",
|
||||
"registryAddr": "Registry Address",
|
||||
"registryPwdPlaceholder": "Registry password",
|
||||
"certPem": "Certificate (PEM)",
|
||||
"keyPem": "Private Key (PEM)",
|
||||
"caCert": "CA Certificate",
|
||||
"caCertPlaceholder": "CA certificate content...",
|
||||
"dbType": "Database Type",
|
||||
"host": "Host",
|
||||
"dbName": "Database Name",
|
||||
"dbPwdPlaceholder": "Database password",
|
||||
"apiEndpoint": "API Endpoint",
|
||||
"apiKeyToken": "API Key / Token",
|
||||
"secretValuePlaceholder": "Secret value",
|
||||
"saving": "Saving...",
|
||||
"nameRequired": "Please enter credential name",
|
||||
"updated": "Updated",
|
||||
"created": "Created",
|
||||
"operationFailed": "Operation failed",
|
||||
"tagsLabel": "Tags"
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"title": "Application Release",
|
||||
@@ -2312,6 +2354,43 @@
|
||||
"decreaseReplica": "Decrease replicas",
|
||||
"increaseReplica": "Increase replicas",
|
||||
"noPod": "No Pods"
|
||||
},
|
||||
"compare": {
|
||||
"title": "Multi-Cluster Resource Comparison",
|
||||
"subtitle": "Cross-cluster resource usage, node distribution, health status comparison",
|
||||
"clusterList": "Cluster List",
|
||||
"noData": "No cluster data. Please add a Kubernetes cluster and sync first.",
|
||||
"goAddCluster": "Add Cluster",
|
||||
"resourceCompare": "Resource Count Comparison",
|
||||
"usageCompare": "CPU / Memory Usage Comparison",
|
||||
"radarCompare": "Resource Radar Comparison",
|
||||
"nodeRoleDist": "Node Role Distribution",
|
||||
"noNodeRoleData": "No node role data",
|
||||
"detailCompare": "Detailed Comparison",
|
||||
"clickRowHint": "Click a row to view cluster overview",
|
||||
"loadFailed": "Failed to load",
|
||||
"neverSynced": "Never synced",
|
||||
"statusHealthy": "✓ Healthy",
|
||||
"statusWarning": "⚠ Warning",
|
||||
"statusError": "✗ Error",
|
||||
"chartNodes": "Nodes",
|
||||
"chartPods": "Pods",
|
||||
"chartDeploy": "Deployments",
|
||||
"chartCpuUsage": "CPU Usage",
|
||||
"chartMemUsage": "Memory Usage",
|
||||
"chartCpuCores": "CPU Cores",
|
||||
"cpuPercent": "CPU %",
|
||||
"memPercent": "Memory %",
|
||||
"colCluster": "Cluster",
|
||||
"colNodes": "Nodes",
|
||||
"colCpuCores": "CPU Cores",
|
||||
"colMemory": "Memory",
|
||||
"colCpuUsage": "CPU Usage",
|
||||
"colMemUsage": "Memory Usage",
|
||||
"colRole": "Role",
|
||||
"colSyncTime": "Sync Time",
|
||||
"statCluster": "Clusters",
|
||||
"statNodes": "Nodes"
|
||||
}
|
||||
},
|
||||
"monitor": {
|
||||
|
||||
@@ -519,7 +519,10 @@
|
||||
"yesterday": "昨天",
|
||||
"zone": "可用区",
|
||||
"zoomIn": "放大",
|
||||
"zoomOut": "缩小"
|
||||
"zoomOut": "缩小",
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"port": "端口"
|
||||
},
|
||||
"menu": {
|
||||
"dashboard": "仪表盘",
|
||||
@@ -1412,7 +1415,46 @@
|
||||
"selectDockerPwd": "选择 Docker 密码凭据",
|
||||
"selectApiCred": "选择凭据(API/密码等)",
|
||||
"selectCred": "选择凭证",
|
||||
"linkedCred": "关联凭证"
|
||||
"linkedCred": "关联凭证",
|
||||
"form": {
|
||||
"editTitle": "编辑凭据",
|
||||
"newTitle": "新增凭据",
|
||||
"basicInfo": "基本信息",
|
||||
"credNamePlaceholder": "如:生产环境 SSH",
|
||||
"varAlias": "变量别名",
|
||||
"varAliasPlaceholder": "如:MYSQL_PROD",
|
||||
"tagsPlaceholder": "生产环境, 阿里云",
|
||||
"descPlaceholder": "可选备注信息",
|
||||
"typeConfig": "配置",
|
||||
"editHint": "(仅填写需要修改的字段)",
|
||||
"sshUser": "SSH 用户",
|
||||
"sshPort": "SSH 端口",
|
||||
"sshPassword": "SSH 密码",
|
||||
"sshPwdPlaceholder": "密码(或留空用密钥)",
|
||||
"privateKey": "私钥内容",
|
||||
"privateKeyPlaceholder": "粘贴 SSH 私钥...",
|
||||
"publicKey": "公钥内容",
|
||||
"publicKeyPlaceholder": "粘贴 SSH 公钥...",
|
||||
"registryAddr": "Registry 地址",
|
||||
"registryPwdPlaceholder": "Registry 密码",
|
||||
"certPem": "证书 (PEM)",
|
||||
"keyPem": "私钥 (PEM)",
|
||||
"caCert": "CA 证书",
|
||||
"caCertPlaceholder": "CA 证书内容...",
|
||||
"dbType": "数据库类型",
|
||||
"host": "主机",
|
||||
"dbName": "数据库名",
|
||||
"dbPwdPlaceholder": "数据库密码",
|
||||
"apiEndpoint": "API 端点",
|
||||
"apiKeyToken": "API Key / Token",
|
||||
"secretValuePlaceholder": "密钥值",
|
||||
"saving": "保存中...",
|
||||
"nameRequired": "请填写凭据名称",
|
||||
"updated": "已更新",
|
||||
"created": "已创建",
|
||||
"operationFailed": "操作失败",
|
||||
"tagsLabel": "标签"
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"title": "应用发布",
|
||||
@@ -2312,6 +2354,43 @@
|
||||
"decreaseReplica": "减少副本",
|
||||
"increaseReplica": "增加副本",
|
||||
"noPod": "暂无 Pod"
|
||||
},
|
||||
"compare": {
|
||||
"title": "多集群资源对比看板",
|
||||
"subtitle": "跨集群资源用量、节点分布、健康状态对比",
|
||||
"clusterList": "集群列表",
|
||||
"noData": "暂无集群数据,请先添加 Kubernetes 集群并同步",
|
||||
"goAddCluster": "去添加集群",
|
||||
"resourceCompare": "资源数量对比",
|
||||
"usageCompare": "CPU / 内存使用率对比",
|
||||
"radarCompare": "资源雷达对比",
|
||||
"nodeRoleDist": "节点角色分布",
|
||||
"noNodeRoleData": "无节点角色数据",
|
||||
"detailCompare": "详细对比",
|
||||
"clickRowHint": "点击行可进入集群概览",
|
||||
"loadFailed": "加载失败",
|
||||
"neverSynced": "从未同步",
|
||||
"statusHealthy": "✓ 健康",
|
||||
"statusWarning": "⚠ 警告",
|
||||
"statusError": "✗ 异常",
|
||||
"chartNodes": "节点",
|
||||
"chartPods": "Pod数",
|
||||
"chartDeploy": "部署数",
|
||||
"chartCpuUsage": "CPU使用",
|
||||
"chartMemUsage": "内存使用",
|
||||
"chartCpuCores": "CPU核数",
|
||||
"cpuPercent": "CPU %",
|
||||
"memPercent": "内存 %",
|
||||
"colCluster": "集群",
|
||||
"colNodes": "节点",
|
||||
"colCpuCores": "CPU 核数",
|
||||
"colMemory": "内存",
|
||||
"colCpuUsage": "CPU 使用率",
|
||||
"colMemUsage": "内存使用率",
|
||||
"colRole": "角色",
|
||||
"colSyncTime": "同步时间",
|
||||
"statCluster": "集群",
|
||||
"statNodes": "节点"
|
||||
}
|
||||
},
|
||||
"monitor": {
|
||||
|
||||
Reference in new issue
Block a user