feat(i18n): 增量国际化迁移 — deploy/kubernetes/monitor/task 全模块 + 补全 28 缺失翻译键

迁移范围:
- deploy 模块 (PipelineEditor/PipelineDetailDialog/PipelineFormDialog/PipelineRunDialog/ReleaseCreateDialog/ReleaseDetailDialog/ReleaseManager/ToolConfigTab/JobRoleCreateDialog 等 14 文件)
- kubernetes 模块 (YamlEditorToolbar/DeploymentHpaTab/DeploymentLogsTab/DeploymentPodsTab/DnsHostSection/SchedulingSection/EventsTab/HPACronHPATab/HistoryTab/PodsTab/ServicesTab/ActionModals 等 12 文件)
- monitor 模块 (AlertEventsTab/AlertCenterDetail/DatasourcePage/LogAlertBell/LogCenter/LogCenterArchBanner/StatChartPanel/LogRoleCreateDialog 等 8 文件)
- task 模块 (CronJobStatsPanel/CronJobTable/CronJobViewDialog/ExecutionHistoryDialog 4 文件)
- layout (PageEmpty/PageLoading 2 文件)
- site-monitor/snmp-monitor 模块 (6 文件)

翻译键: 3975 → 4003 (+28 缺失键补全), 22 命名空间, zh-CN/en-US 完全对齐
新增命名空间: agent/aiops/cloud/kubernetes/monitor/pipeline/rbac/system/task/ui

验证: go build✅ tsc -b=0 npm build✅ (20.78s) 17/17 集成测试通过
This commit is contained in:
attacker committed 2026-07-02 12:39:36 +08:00
1 parent eddf01dd79
commit 63e4c220d4
59 files changed
+6713 -641

No files matched your search

@@ -1,4 +1,5 @@
// YAML 查看/编辑统一工具栏:复制 · 下载 · 编辑 · 保存 // Logan.Li
import { useTranslation } from 'react-i18next';
import { Copy, Check, Save, Loader2, Pencil, Download } from 'lucide-react';
import { Button } from '@/components/ui/button';
@@ -23,29 +24,30 @@ export function YamlEditorToolbar({
readOnly = false, showRawToggle = true,
onCopy, onDownload, onEdit, onCancel, onSave, onToggleRaw,
}: YamlEditorToolbarProps) {
const { t } = useTranslation();
const sz = compact ? 'h-7 text-xs' : 'h-8';
return (
<div className="flex items-center gap-2 flex-wrap">
<Button variant="outline" size="sm" className={`${sz} border-[var(--dg-border)] text-[var(--dg-text-secondary)]`} onClick={onCopy}>
{copied ? <Check className="h-3 w-3 mr-1 dg-text-green" /> : <Copy className="h-3 w-3 mr-1" />}
{copied ? '已复制' : '复制'}
{copied ? t('common.copied') : t('common.copy')}
</Button>
{onDownload && (
<Button variant="outline" size="sm" className={`${sz} border-[var(--dg-border)] text-[var(--dg-text-secondary)]`} onClick={onDownload}>
<Download className="h-3 w-3 mr-1" />下载
<Download className="h-3 w-3 mr-1" />{t('common.download')}
</Button>
)}
{!readOnly && (
!editing ? (
<Button variant="outline" size="sm" className={`${sz} border-[var(--dg-border)] text-[var(--dg-text-secondary)]`} onClick={onEdit}>
<Pencil className="h-3 w-3 mr-1" />编辑
<Pencil className="h-3 w-3 mr-1" />{t('common.edit')}
</Button>
) : (
<>
<Button variant="outline" size="sm" className={`${sz} border-[var(--dg-border)] text-[var(--dg-text-secondary)]`} onClick={onCancel}>取消</Button>
<Button variant="outline" size="sm" className={`${sz} border-[var(--dg-border)] text-[var(--dg-text-secondary)]`} onClick={onCancel}>{t('common.cancel')}</Button>
<Button size="sm" className={`${sz} bg-blue-600 hover:bg-blue-700`} onClick={onSave} disabled={saving}>
{saving ? <Loader2 className="h-3 w-3 animate-spin mr-1" /> : <Save className="h-3 w-3 mr-1" />}
保存
{t('common.save')}
</Button>
</>
)
@@ -56,9 +58,9 @@ export function YamlEditorToolbar({
onClick={onToggleRaw}
className="text-[11px] dg-text-muted hover:text-[var(--dg-text-secondary)] underline-offset-2 hover:underline"
>
{showRaw ? '精简视图' : '原始 YAML'}
{showRaw ? t('k8s.yamlToolbar.compactView') : t('k8s.yamlToolbar.rawYaml')}
</button>
)}
</div>
);
}
}
@@ -1,4 +1,5 @@
// DeploymentHpaTab.tsx — Deployment 详情 HPA Tab // Logan.Li
import { useTranslation } from 'react-i18next';
import { Gauge, Trash2 } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@@ -26,12 +27,13 @@ export function DeploymentHpaTab({
onCreated,
onDeleteHpa,
}: DeploymentHpaTabProps) {
const { t } = useTranslation();
return (
<TabsContent value="hpa">
<Card className="dg-card">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-sm dg-text-secondary">HPA 自动扩缩容</CardTitle>
<CardTitle className="text-sm dg-text-secondary">{t('k8s.deploymentDetail.hpaTitle')}</CardTitle>
<CreateHPADialog
clusterCode={clusterCode}
namespace={namespace}
@@ -44,18 +46,18 @@ export function DeploymentHpaTab({
{hpas.length === 0 ? (
<div className="text-center py-12 dg-text-muted">
<Gauge className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p>未配置 HPA</p>
<p className="text-xs mt-1">点击右上角「创建 HPA」设置自动扩缩容</p>
<p>{t('k8s.deploymentDetail.noHpa')}</p>
<p className="text-xs mt-1">{t('k8s.deploymentDetail.createHpaHint')}</p>
</div>
) : (
<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">指标</TableHead>
<TableHead className="dg-text-secondary">当前/目标</TableHead>
<TableHead className="dg-text-secondary">操作</TableHead>
<TableHead className="dg-text-secondary">{t('k8s.column.name')}</TableHead>
<TableHead className="dg-text-secondary">{t('k8s.hpaCronHpa.col.minMax')}</TableHead>
<TableHead className="dg-text-secondary">{t('k8s.hpaCronHpa.col.metrics')}</TableHead>
<TableHead className="dg-text-secondary">{t('k8s.deploymentDetail.currentTarget')}</TableHead>
<TableHead className="dg-text-secondary">{t('common.actions')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -74,7 +76,7 @@ export function DeploymentHpaTab({
<TableCell className="text-[var(--dg-text-secondary)]">{hpa.current_replicas || 0}</TableCell>
<TableCell>
<Button variant="ghost" size="icon" className="h-7 w-7 dg-text-red hover:dg-text-red"
onClick={() => onDeleteHpa(hpa)} title="删除 HPA">
onClick={() => onDeleteHpa(hpa)} title={t('k8s.deploymentDetail.deleteHpa')}>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</TableCell>
@@ -88,4 +90,4 @@ export function DeploymentHpaTab({
</Card>
</TabsContent>
);
}
}
@@ -1,4 +1,5 @@
// DeploymentLogsTab.tsx — Deployment 详情日志 Tab // Logan.Li
import { useTranslation } from 'react-i18next';
import { FileText, ExternalLink } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@@ -31,19 +32,20 @@ export function DeploymentLogsTab({
onLogsTabPodChange,
onOpenLogsViewer,
}: DeploymentLogsTabProps) {
const { t } = useTranslation();
return (
<TabsContent value="logs">
<Card className="dg-card">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-sm dg-text-secondary">Pod 日志</CardTitle>
<CardTitle className="text-sm dg-text-secondary">{t('k8s.deploymentDetail.podLogs')}</CardTitle>
<div className="flex items-center gap-2">
<span className="text-xs dg-text-muted">最近</span>
<span className="text-xs dg-text-muted">{t('k8s.deploymentDetail.recent')}</span>
<Input
type="number" value={logTail} onChange={(e) => onLogTailChange(Number(e.target.value))}
className="w-20 h-7 dg-input text-xs"
/>
<span className="text-xs dg-text-muted">行</span>
<span className="text-xs dg-text-muted">{t('k8s.deploymentDetail.lines')}</span>
</div>
</div>
</CardHeader>
@@ -51,15 +53,15 @@ export function DeploymentLogsTab({
{pods.length === 0 ? (
<div className="text-center py-12 dg-text-muted">
<FileText className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p>暂无 Pod</p>
<p>{t('k8s.deploymentDetail.noPod')}</p>
</div>
) : (
<div className="flex items-center gap-3 py-10 justify-center flex-wrap">
<FileText className="h-5 w-5 dg-text-blue" />
<span className="text-sm dg-text-secondary">选择 Pod 打开增强版日志查看器</span>
<span className="text-sm dg-text-secondary">{t('k8s.deploymentDetail.selectPodHint')}</span>
<Select value={logsTabPod} onValueChange={onLogsTabPodChange}>
<SelectTrigger className="w-[280px] h-8 dg-input text-xs">
<SelectValue placeholder="选择 Pod…" />
<SelectValue placeholder={t('k8s.deploymentDetail.selectPodPlaceholder')} />
</SelectTrigger>
<SelectContent>
{pods.map((p: JsonData) => (
@@ -76,10 +78,10 @@ export function DeploymentLogsTab({
onOpenLogsViewer(logsTabPod, pod ? extractContainers(pod) : []);
}}
>
<ExternalLink className="h-3 w-3 mr-1" />打开日志
<ExternalLink className="h-3 w-3 mr-1" />{t('k8s.deploymentDetail.openLogs')}
</Button>
<span className="text-xs dg-text-muted block w-full text-center mt-2">
支持实时跟踪、多容器切换、previous 日志、关键字过滤、导出
{t('k8s.deploymentDetail.logsFeatures')}
</span>
</div>
)}
@@ -87,4 +89,4 @@ export function DeploymentLogsTab({
</Card>
</TabsContent>
);
}
}
@@ -1,4 +1,5 @@
// DeploymentPodsTab.tsx — Deployment 详情 Pod 列表 Tab // Logan.Li
import { useTranslation } from 'react-i18next';
import { FileText, Terminal, FolderOpen, FileCode } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@@ -20,6 +21,7 @@ export interface DeploymentPodsTabProps {
}
export function DeploymentPodsTab({ pods, onOpenLogsViewer, onOpenExec, onOpenFileBrowser, onOpenYaml }: DeploymentPodsTabProps) {
const { t } = useTranslation();
return (
<TabsContent value="pods">
<Card className="dg-card">
@@ -27,19 +29,19 @@ export function DeploymentPodsTab({ pods, onOpenLogsViewer, onOpenExec, onOpenFi
<Table>
<TableHeader>
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
<TableHead className="dg-text-secondary">Pod 名称</TableHead>
<TableHead className="dg-text-secondary">状态</TableHead>
<TableHead className="dg-text-secondary">节点</TableHead>
<TableHead className="dg-text-secondary">{t('k8s.deploymentDetail.podName')}</TableHead>
<TableHead className="dg-text-secondary">{t('k8s.column.status')}</TableHead>
<TableHead className="dg-text-secondary">{t('k8s.column.node')}</TableHead>
<TableHead className="dg-text-secondary">IP</TableHead>
<TableHead className="dg-text-secondary">重启</TableHead>
<TableHead className="dg-text-secondary">操作</TableHead>
<TableHead className="dg-text-secondary">{t('k8s.deploymentDetail.restart')}</TableHead>
<TableHead className="dg-text-secondary">{t('common.actions')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{pods.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center dg-text-muted py-8">
暂无 Pod
{t('k8s.deploymentDetail.noPod')}
</TableCell>
</TableRow>
) : (
@@ -61,22 +63,22 @@ export function DeploymentPodsTab({ pods, onOpenLogsViewer, onOpenExec, onOpenFi
<TableCell>
<div className="flex items-center gap-1">
<Button variant="ghost" size="icon" className="h-7 w-7 dg-text-secondary hover:dg-text-blue"
title="查看日志"
title={t('k8s.deploymentDetail.viewLogs')}
onClick={() => onOpenLogsViewer({ name: pod.name, containers: extractContainers(pod) })}>
<FileText className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7 dg-text-secondary hover:dg-text-green"
title="exec 终端"
title={t('k8s.deploymentDetail.execTerminal')}
onClick={() => onOpenExec({ name: pod.name, containers: extractContainers(pod) })}>
<Terminal className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7 dg-text-secondary hover:dg-text-yellow"
title="文件浏览器"
title={t('k8s.deploymentDetail.fileBrowser')}
onClick={() => onOpenFileBrowser({ name: pod.name, containers: extractContainers(pod) })}>
<FolderOpen className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7 dg-text-secondary hover:dg-text-blue"
title="查看 YAML"
title={t('k8s.deploymentDetail.viewYaml')}
onClick={() => onOpenYaml(pod.name)}>
<FileCode className="h-3.5 w-3.5" />
</Button>
@@ -91,4 +93,4 @@ export function DeploymentPodsTab({ pods, onOpenLogsViewer, onOpenExec, onOpenFi
</Card>
</TabsContent>
);
}
}
@@ -1,4 +1,5 @@
// DnsHostSection.tsx — DNS 配置 + 主机别名 // Logan.Li
import { useTranslation } from 'react-i18next';
import { Globe, MapPin, Plus, Trash2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -36,6 +37,7 @@ export function DnsHostSection({
hostAliases,
onHostAliasesChange,
}: DnsHostSectionProps) {
const { t } = useTranslation();
const addDnsOption = () => onDnsOptionsChange([...dnsOptions, { name: '', value: '' }]);
const updateDnsOption = (index: number, patch: Partial<DnsOptionEntry>) =>
onDnsOptionsChange(dnsOptions.map((o, i) => (i === index ? { ...o, ...patch } : o)));
@@ -53,16 +55,16 @@ export function DnsHostSection({
<Card className="dg-card">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
<Globe className="h-4 w-4 text-indigo-400" />DNS 配置 (dnsConfig)
<Globe className="h-4 w-4 text-indigo-400" />{t('k8s.dnsHost.dnsConfigTitle')}
</CardTitle>
</CardHeader>
<CardContent className="pt-0 space-y-3">
<p className="text-[11px] dg-text-muted">
提示:dnsConfig 通常与 <code className="dg-text-secondary">dnsPolicy: None</code> 搭配使用。每行一个值。
{t('k8s.dnsHost.hintPrefix')} <code className="dg-text-secondary">dnsPolicy: None</code> {t('k8s.dnsHost.hintSuffix')}
</p>
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="dg-text-muted text-xs">nameservers(每行一个 IP)</Label>
<Label className="dg-text-muted text-xs">{t('k8s.dnsHost.nameservers')}</Label>
<textarea
value={dnsNameservers}
onChange={e => onDnsNameserversChange(e.target.value)}
@@ -72,7 +74,7 @@ export function DnsHostSection({
/>
</div>
<div>
<Label className="dg-text-muted text-xs">searches(搜索域,每行一个)</Label>
<Label className="dg-text-muted text-xs">{t('k8s.dnsHost.searches')}</Label>
<textarea
value={dnsSearches}
onChange={e => onDnsSearchesChange(e.target.value)}
@@ -84,13 +86,13 @@ export function DnsHostSection({
</div>
<div>
<div className="flex items-center justify-between mb-1">
<Label className="dg-text-secondary text-xs">options(DNS 选项)</Label>
<Label className="dg-text-secondary text-xs">{t('k8s.dnsHost.options')}</Label>
<Button size="sm" variant="ghost" className="h-6 text-xs dg-text-secondary" onClick={addDnsOption}>
<Plus className="h-3 w-3 mr-1" />添加
<Plus className="h-3 w-3 mr-1" />{t('common.add')}
</Button>
</div>
{dnsOptions.length === 0 ? (
<p className="text-xs dg-text-muted italic">未配置</p>
<p className="text-xs dg-text-muted italic">{t('k8s.dnsHost.notConfigured')}</p>
) : (
dnsOptions.map((o, i) => (
<div key={i} className="flex items-center gap-2 mb-1.5">
@@ -104,7 +106,7 @@ export function DnsHostSection({
<Input
value={o.value}
onChange={e => updateDnsOption(i, { value: e.target.value })}
placeholder="5(可留空)"
placeholder={`5${t('k8s.dnsHost.optionalHint')}`}
className="flex-1 h-7 dg-input text-[var(--dg-text-secondary)] text-xs font-mono"
/>
<Button size="icon" variant="ghost" className="h-6 w-6 dg-text-red" onClick={() => removeDnsOption(i)}>
@@ -120,18 +122,18 @@ export function DnsHostSection({
<Card className="dg-card">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
<MapPin className="h-4 w-4 dg-text-yellow" />主机别名 (hostAliases)
<MapPin className="h-4 w-4 dg-text-yellow" />{t('k8s.dnsHost.hostAliasTitle')}
</CardTitle>
</CardHeader>
<CardContent className="pt-0 space-y-2">
<div className="flex items-center justify-between mb-1">
<Label className="dg-text-secondary text-xs">IP → 主机名映射(注入 Pod 的 /etc/hosts)</Label>
<Label className="dg-text-secondary text-xs">{t('k8s.dnsHost.hostAliasHint')}</Label>
<Button size="sm" variant="ghost" className="h-6 text-xs dg-text-secondary" onClick={addHostAlias}>
<Plus className="h-3 w-3 mr-1" />添加
<Plus className="h-3 w-3 mr-1" />{t('common.add')}
</Button>
</div>
{hostAliases.length === 0 ? (
<p className="text-xs dg-text-muted italic">未配置</p>
<p className="text-xs dg-text-muted italic">{t('k8s.dnsHost.notConfigured')}</p>
) : (
hostAliases.map((a, i) => (
<div key={i} className="flex items-center gap-2">
@@ -158,4 +160,4 @@ export function DnsHostSection({
</Card>
</>
);
}
}
@@ -1,4 +1,5 @@
// SchedulingSection.tsx — 调度:节点名称 / 标签选择器 / 节点亲和性 // Logan.Li
import { useTranslation } from 'react-i18next';
import { Loader2, Plus, Server, Trash2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -31,10 +32,10 @@ export interface SchedulingSectionProps {
nodeLabels: Record<string, string[]>;
}
const SCHEDULE_MODES: Array<{ key: ScheduleMode; label: string }> = [
{ key: 'nodeName', label: '节点名称' },
{ key: 'nodeSelector', label: '标签选择器' },
{ key: 'nodeAffinity', label: '节点亲和性' },
const SCHEDULE_MODES: Array<{ key: ScheduleMode }> = [
{ key: 'nodeName' },
{ key: 'nodeSelector' },
{ key: 'nodeAffinity' },
];
export function SchedulingSection({
@@ -50,12 +51,13 @@ export function SchedulingSection({
onUpdateLabelSelector,
nodeLabels,
}: SchedulingSectionProps) {
const { t } = useTranslation();
return (
<Card className="dg-card">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
<Server className="h-4 w-4 dg-text-green" />调度 (Scheduling)
<Server className="h-4 w-4 dg-text-green" />{t('k8s.scheduling.title')}
</CardTitle>
<div className="flex items-center gap-1 bg-[var(--dg-bg-tertiary)] rounded-lg p-0.5 border border-[var(--dg-border)]">
{SCHEDULE_MODES.map(m => (
@@ -68,24 +70,24 @@ export function SchedulingSection({
: 'dg-text-secondary hover:text-[var(--dg-text-secondary)]'
}`}
>
{m.label}
{t(`k8s.scheduling.mode.${m.key}`)}
</button>
))}
</div>
</div>
<p className="text-xs dg-text-muted mt-1">
{scheduleMode === 'nodeName' && 'Pod 将直接调度到指定节点'}
{scheduleMode === 'nodeSelector' && 'Pod 将调度到匹配标签的节点'}
{scheduleMode === 'nodeAffinity' && '通过亲和性规则控制 Pod 调度到指定节点'}
{scheduleMode === 'nodeName' && t('k8s.scheduling.hint.nodeName')}
{scheduleMode === 'nodeSelector' && t('k8s.scheduling.hint.nodeSelector')}
{scheduleMode === 'nodeAffinity' && t('k8s.scheduling.hint.nodeAffinity')}
</p>
</CardHeader>
<CardContent className="pt-0">
{scheduleMode === 'nodeName' && (
<div className="space-y-2">
<Label className="dg-text-secondary text-xs">请选择节点</Label>
<Label className="dg-text-secondary text-xs">{t('k8s.scheduling.selectNode')}</Label>
{nodesLoading ? (
<div className="flex items-center gap-1 dg-text-muted text-xs">
<Loader2 className="h-3 w-3 animate-spin" />加载节点列表...
<Loader2 className="h-3 w-3 animate-spin" />{t('k8s.scheduling.loadingNodes')}
</div>
) : (
<select
@@ -93,7 +95,7 @@ export function SchedulingSection({
onChange={e => onNodeNameChange(e.target.value)}
className="h-8 w-full bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded text-xs text-[var(--dg-text-secondary)] px-2"
>
<option value="">(不指定)</option>
<option value="">{t('k8s.scheduling.notSpecified')}</option>
{availableNodes.map(n => (
<option key={n.name} value={n.name}>
{n.name} {n.ip ? `(${n.ip})` : ''}
@@ -103,7 +105,7 @@ export function SchedulingSection({
)}
{nodeName && (
<div className="text-[10px] dg-text-muted">
已选择: <code className="bg-[var(--dg-bg-tertiary)] px-1 py-0.5 rounded dg-text-green">{nodeName}</code>
{t('k8s.scheduling.selected')} <code className="bg-[var(--dg-bg-tertiary)] px-1 py-0.5 rounded dg-text-green">{nodeName}</code>
</div>
)}
</div>
@@ -112,13 +114,13 @@ export function SchedulingSection({
{scheduleMode === 'nodeSelector' && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="dg-text-secondary text-xs">请选择标签</Label>
<Label className="dg-text-secondary text-xs">{t('k8s.scheduling.selectLabel')}</Label>
<Button size="sm" variant="ghost" className="h-6 text-xs dg-text-secondary" onClick={onAddLabelSelector}>
<Plus className="h-3 w-3 mr-1" />添加
<Plus className="h-3 w-3 mr-1" />{t('k8s.scheduling.add')}
</Button>
</div>
{labelSelectorKv.length === 0 ? (
<p className="text-xs dg-text-muted italic">未配置标签选择器</p>
<p className="text-xs dg-text-muted italic">{t('k8s.scheduling.noLabelSelector')}</p>
) : (
labelSelectorKv.map((n, i) => (
<div key={i} className="flex items-center gap-2">
@@ -127,7 +129,7 @@ export function SchedulingSection({
onChange={e => onUpdateLabelSelector(i, { key: e.target.value, value: '' })}
className="flex-1 h-7 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded text-xs text-[var(--dg-text-secondary)] px-1.5"
>
<option value="">选择标签</option>
<option value="">{t('k8s.scheduling.selectLabelPlaceholder')}</option>
{Object.entries(nodeLabels).map(([k, vals]) => (
<option key={k} value={k}>{k} ({vals.length})</option>
))}
@@ -138,7 +140,7 @@ export function SchedulingSection({
className="flex-1 h-7 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded text-xs text-[var(--dg-text-secondary)] px-1.5"
disabled={!n.key}
>
<option value="">选择值</option>
<option value="">{t('k8s.scheduling.selectValuePlaceholder')}</option>
{(nodeLabels[n.key] || []).map(v => (
<option key={v} value={v}>{v}</option>
))}
@@ -161,14 +163,14 @@ export function SchedulingSection({
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="dg-text-secondary text-xs">
请选择标签 <span className="dg-text-muted">(生成 requiredDuringScheduling 规则)</span>
{t('k8s.scheduling.selectLabelAffinity')} <span className="dg-text-muted">{t('k8s.scheduling.affinityRuleHint')}</span>
</Label>
<Button size="sm" variant="ghost" className="h-6 text-xs dg-text-secondary" onClick={onAddLabelSelector}>
<Plus className="h-3 w-3 mr-1" />添加
<Plus className="h-3 w-3 mr-1" />{t('k8s.scheduling.add')}
</Button>
</div>
{labelSelectorKv.length === 0 ? (
<p className="text-xs dg-text-muted italic">未配置亲和性规则</p>
<p className="text-xs dg-text-muted italic">{t('k8s.scheduling.noAffinityRule')}</p>
) : (
labelSelectorKv.map((n, i) => (
<div key={i} className="flex items-center gap-2">
@@ -177,7 +179,7 @@ export function SchedulingSection({
onChange={e => onUpdateLabelSelector(i, { key: e.target.value, value: '' })}
className="flex-1 h-7 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded text-xs text-[var(--dg-text-secondary)] px-1.5"
>
<option value="">选择标签</option>
<option value="">{t('k8s.scheduling.selectLabelPlaceholder')}</option>
{Object.entries(nodeLabels).map(([k, vals]) => (
<option key={k} value={k}>{k} ({vals.length})</option>
))}
@@ -188,7 +190,7 @@ export function SchedulingSection({
className="flex-1 h-7 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded text-xs text-[var(--dg-text-secondary)] px-1.5"
disabled={!n.key}
>
<option value="">选择值</option>
<option value="">{t('k8s.scheduling.selectValuePlaceholder')}</option>
{(nodeLabels[n.key] || []).map(v => (
<option key={v} value={v}>{v}</option>
))}
@@ -1,13 +1,15 @@
// deployment-view/EventsTab.tsx — 事件 Tab // Logan.Li
import { useTranslation } from 'react-i18next';
import { EventTable } from '../EventTable';
import type { EventItem } from './types';
export function EventsTab({ events, loading }: { events: EventItem[]; loading: string }) {
const { t } = useTranslation();
return (
<EventTable
events={events}
loading={loading}
emptyText="暂无事件"
emptyText={t('k8s.empty.events')}
/>
);
}
}
@@ -1,4 +1,5 @@
// deployment-view/HPACronHPATab.tsx — 容器伸缩 Tab (HPA + CronHPA) // Logan.Li
import { useTranslation } from 'react-i18next';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
@@ -9,6 +10,7 @@ import { Activity, Clock, Loader2 } from 'lucide-react';
import type { JsonData } from '@/types/common';
export function HPACronHPATab({ hpas, cronHpas, loading, replicas }: { hpas: JsonData[]; cronHpas: JsonData[]; loading: string; replicas: number }) {
const { t } = useTranslation();
if (loading === 'scale') {
return <div className="flex items-center justify-center py-12"><Loader2 className="h-6 w-6 animate-spin text-[var(--dg-text-muted)]" /></div>;
}
@@ -17,29 +19,29 @@ export function HPACronHPATab({ hpas, cronHpas, loading, replicas }: { hpas: Jso
<div className="space-y-4">
{/* 当前副本数提示 */}
<div className="flex items-center gap-2 px-2">
<span className="text-sm text-[var(--dg-text-muted)]">当前副本:</span>
<span className="text-sm text-[var(--dg-text-muted)]">{t('k8s.hpaCronHpa.currentReplicas')}</span>
<span className="text-sm font-bold text-[var(--dg-text-primary)]">{replicas}</span>
<span className="text-xs text-[var(--dg-text-muted)]">(可通过 Tab 栏右侧 +/- 手动调整)</span>
<span className="text-xs text-[var(--dg-text-muted)]">{t('k8s.hpaCronHpa.scaleHint')}</span>
</div>
{/* HPA 策略 */}
<Card className="dg-card">
<CardHeader className="pb-2"><CardTitle className="text-sm flex items-center gap-2"><Activity className="h-4 w-4 text-[var(--dg-orange)]" />HPA — 水平自动伸缩</CardTitle></CardHeader>
<CardHeader className="pb-2"><CardTitle className="text-sm flex items-center gap-2"><Activity className="h-4 w-4 text-[var(--dg-orange)]" />{t('k8s.hpaCronHpa.hpaTitle')}</CardTitle></CardHeader>
<CardContent>
{hpas.length === 0 ? (
<div className="text-sm text-[var(--dg-text-muted)] py-4">
未关联 HPA — <Button variant="link" className="h-auto p-0 text-[var(--dg-blue)] text-sm">创建 HPA</Button>
{t('k8s.hpaCronHpa.noHpaLinked')}<Button variant="link" className="h-auto p-0 text-[var(--dg-blue)] text-sm">{t('k8s.hpaCronHpa.createHpa')}</Button>
</div>
) : (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="border-[var(--dg-border)]">
<TableHead className="text-[var(--dg-text-muted)]">名称</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">最小/最大</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">指标</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">当前负载</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">状态</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">{t('k8s.column.name')}</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">{t('k8s.hpaCronHpa.col.minMax')}</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">{t('k8s.hpaCronHpa.col.metrics')}</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">{t('k8s.hpaCronHpa.col.currentLoad')}</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">{t('k8s.hpaCronHpa.col.status')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -65,7 +67,7 @@ export function HPACronHPATab({ hpas, cronHpas, loading, replicas }: { hpas: Jso
<TableCell className="text-sm font-mono text-[var(--dg-text-secondary)]">{curR}</TableCell>
<TableCell>
<Badge className={ok ? 'bg-[var(--dg-green)]/20 text-[var(--dg-green)]' : 'bg-[var(--dg-red)]/20 text-[var(--dg-red)]'}>
{ok ? '正常' : '异常'}
{ok ? t('k8s.hpaCronHpa.status.normal') : t('k8s.hpaCronHpa.status.abnormal')}
</Badge>
</TableCell>
</TableRow>
@@ -80,22 +82,22 @@ export function HPACronHPATab({ hpas, cronHpas, loading, replicas }: { hpas: Jso
{/* CronHPA 策略 */}
<Card className="dg-card">
<CardHeader className="pb-2"><CardTitle className="text-sm flex items-center gap-2"><Clock className="h-4 w-4 text-[var(--dg-blue)]" />CronHPA — 定时伸缩</CardTitle></CardHeader>
<CardHeader className="pb-2"><CardTitle className="text-sm flex items-center gap-2"><Clock className="h-4 w-4 text-[var(--dg-blue)]" />{t('k8s.hpaCronHpa.cronHpaTitle')}</CardTitle></CardHeader>
<CardContent>
{cronHpas.length === 0 ? (
<div className="text-sm text-[var(--dg-text-muted)] py-4">
未关联 CronHPA — <Button variant="link" className="h-auto p-0 text-[var(--dg-blue)] text-sm">创建 CronHPA</Button>
{t('k8s.hpaCronHpa.noCronHpaLinked')}<Button variant="link" className="h-auto p-0 text-[var(--dg-blue)] text-sm">{t('k8s.hpaCronHpa.createCronHpa')}</Button>
</div>
) : (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="border-[var(--dg-border)]">
<TableHead className="text-[var(--dg-text-muted)]">名称</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">目标副本</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">调度规则</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">时区</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">状态</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">{t('k8s.column.name')}</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">{t('k8s.hpaCronHpa.cronCol.targetReplicas')}</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">{t('k8s.hpaCronHpa.cronCol.scheduleRule')}</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">{t('k8s.hpaCronHpa.cronCol.timezone')}</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">{t('k8s.hpaCronHpa.cronCol.status')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -111,7 +113,7 @@ export function HPACronHPATab({ hpas, cronHpas, loading, replicas }: { hpas: Jso
<TableCell className="text-xs font-mono text-[var(--dg-text-muted)]">{jobs.map((j: JsonData) => j.schedule || '?').join(', ')}</TableCell>
<TableCell className="text-xs text-[var(--dg-text-muted)]">{tz}</TableCell>
<TableCell>
<Badge className="bg-[var(--dg-green)]/20 text-[var(--dg-green)]">已启用</Badge>
<Badge className="bg-[var(--dg-green)]/20 text-[var(--dg-green)]">{t('k8s.label.enabled')}</Badge>
</TableCell>
</TableRow>
);
@@ -124,4 +126,4 @@ export function HPACronHPATab({ hpas, cronHpas, loading, replicas }: { hpas: Jso
</Card>
</div>
);
}
}
@@ -1,4 +1,5 @@
// deployment-view/HistoryTab.tsx — 历史版本 Tab // Logan.Li
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
@@ -9,8 +10,9 @@ import { formatK8sTime } from '../k8s-yaml-utils';
import type { JsonData } from '@/types/common';
export function HistoryTab({ revisions, rollbackLoading, onRollback }: { revisions: JsonData[]; rollbackLoading: number | null; onRollback: (rev: JsonData) => void }) {
const { t } = useTranslation();
if (revisions.length === 0) {
return <div className="text-center text-[var(--dg-text-muted)] py-12">暂无历史版本</div>;
return <div className="text-center text-[var(--dg-text-muted)] py-12">{t('k8s.history.empty')}</div>;
}
return (
<div className="overflow-x-auto">
@@ -18,10 +20,10 @@ export function HistoryTab({ revisions, rollbackLoading, onRollback }: { revisio
<TableHeader>
<TableRow className="border-[var(--dg-border)]">
<TableHead className="text-[var(--dg-text-muted)] w-24">Revision</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">镜像</TableHead>
<TableHead className="text-[var(--dg-text-muted)] w-24">状态</TableHead>
<TableHead className="text-[var(--dg-text-muted)] w-40">更新时间</TableHead>
<TableHead className="text-[var(--dg-text-muted)] w-24">操作</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">{t('k8s.column.image')}</TableHead>
<TableHead className="text-[var(--dg-text-muted)] w-24">{t('k8s.column.status')}</TableHead>
<TableHead className="text-[var(--dg-text-muted)] w-40">{t('k8s.history.updateTime')}</TableHead>
<TableHead className="text-[var(--dg-text-muted)] w-24">{t('common.actions')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -34,14 +36,14 @@ export function HistoryTab({ revisions, rollbackLoading, onRollback }: { revisio
<TableCell className="text-xs font-mono text-[var(--dg-text-muted)] max-w-[300px] truncate">{rev.image || rev.images?.[0] || '-'}</TableCell>
<TableCell>
{isLatest
? <Badge className="bg-[var(--dg-green)]/20 text-[var(--dg-green)] border-[var(--dg-green)]/30">当前</Badge>
: <Badge variant="outline" className="text-[var(--dg-text-muted)]">历史</Badge>}
? <Badge className="bg-[var(--dg-green)]/20 text-[var(--dg-green)] border-[var(--dg-green)]/30">{t('k8s.history.current')}</Badge>
: <Badge variant="outline" className="text-[var(--dg-text-muted)]">{t('k8s.history.historical')}</Badge>}
</TableCell>
<TableCell className="text-xs text-[var(--dg-text-muted)]">{formatK8sTime(rev.created_at || rev.time)}</TableCell>
<TableCell>
<Button variant="ghost" size="sm" className="h-7 dg-text-yellow" disabled={isLatest || rollbackLoading !== null} onClick={() => onRollback(rev)}>
{rollbackLoading === revision ? <Loader2 className="h-3 w-3 animate-spin mr-1" /> : <RotateCcw className="h-3 w-3 mr-1" />}
回滚
{t('k8s.history.rollback')}
</Button>
</TableCell>
</TableRow>
@@ -51,4 +53,4 @@ export function HistoryTab({ revisions, rollbackLoading, onRollback }: { revisio
</Table>
</div>
);
}
}
@@ -1,6 +1,7 @@
// deployment-view/PodsTab.tsx — 容器组 Tab // Logan.Li
// v2: 虚拟滚动,支持 1000+ Pod 场景流畅渲染
import { useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useVirtualizer } from '@tanstack/react-virtual';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
@@ -27,6 +28,7 @@ export function PodsTab({
onDeletePod,
expandedPod, setExpandedPod,
}: DeploymentViewProps & { expandedPod: string | null; setExpandedPod: (v: string | null) => void }) {
const { t } = useTranslation();
const scrollRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
@@ -37,7 +39,7 @@ export function PodsTab({
});
if (pods.length === 0) {
return <div className="text-center text-[var(--dg-text-muted)] py-12">暂无容器组</div>;
return <div className="text-center text-[var(--dg-text-muted)] py-12">{t('k8s.pods.empty')}</div>;
}
const virtualRows = virtualizer.getVirtualItems();
@@ -52,13 +54,13 @@ export function PodsTab({
style={{ gridTemplateColumns: GRID_COLS }}
>
<span />
<span>名称</span>
<span>状态</span>
<span>节点</span>
<span>{t('k8s.pods.col.name')}</span>
<span>{t('k8s.pods.col.status')}</span>
<span>{t('k8s.pods.col.node')}</span>
<span>IP</span>
<span className="text-center">重启</span>
<span className="text-center">{t('k8s.pods.col.restart')}</span>
<span>Age</span>
<span className="text-right">操作</span>
<span className="text-right">{t('k8s.pods.col.actions')}</span>
</div>
{/* 虚拟滚动列表 */}
@@ -134,10 +136,10 @@ export function PodsTab({
</span>
<div className="flex items-center justify-end gap-0.5 shrink-0 w-[88px]">
<Button variant="ghost" size="sm" className="h-7 w-7 p-0 shrink-0 dg-text-green" title="终端" onClick={() => onShowPodTerminal(pod)}>
<Button variant="ghost" size="sm" className="h-7 w-7 p-0 shrink-0 dg-text-green" title={t('k8s.pods.action.terminal')} onClick={() => onShowPodTerminal(pod)}>
<Terminal className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="sm" className="h-7 w-7 p-0 shrink-0 dg-text-yellow" title="文件" onClick={() => onShowPodFileBrowser(pod)}>
<Button variant="ghost" size="sm" className="h-7 w-7 p-0 shrink-0 dg-text-yellow" title={t('k8s.pods.action.file')} onClick={() => onShowPodFileBrowser(pod)}>
<HardDrive className="h-3.5 w-3.5" />
</Button>
<DropdownMenu>
@@ -146,21 +148,21 @@ export function PodsTab({
variant="ghost"
size="sm"
className="h-7 w-7 p-0 shrink-0 text-[var(--dg-text-muted)] hover:text-[var(--dg-blue)]"
title="更多"
aria-label="更多"
title={t('k8s.pods.action.more')}
aria-label={t('k8s.pods.action.more')}
>
<MoreVertical className="h-3.5 w-3.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<DropdownMenuItem onClick={() => onShowPodLogs(pod)} className="text-[var(--dg-text-primary)]">
<FileText className="h-3.5 w-3.5 mr-2" /> 日志
<FileText className="h-3.5 w-3.5 mr-2" /> {t('k8s.pods.action.logs')}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onShowPodYaml(pod)} className="text-[var(--dg-text-primary)]">
<FileCode className="h-3.5 w-3.5 mr-2" /> YAML
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onDeletePod?.(pod)} className="dg-text-red focus:dg-text-red">
<Trash2 className="h-3.5 w-3.5 mr-2" /> 删除
<Trash2 className="h-3.5 w-3.5 mr-2" /> {t('k8s.pods.action.delete')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -173,7 +175,7 @@ export function PodsTab({
<PodConditionsRow pod={pod} />
{containers && containers.length > 0 && (
<div>
<p className="text-xs text-[var(--dg-text-muted)] mb-2">容器</p>
<p className="text-xs text-[var(--dg-text-muted)] mb-2">{t('k8s.pods.containers')}</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{containers.map((c, idx) => (
<div key={idx} className="rounded-md border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] px-3 py-2.5 text-xs">
@@ -187,7 +189,7 @@ export function PodsTab({
{c.resources && (
<div className="mt-1.5 flex gap-4 text-[var(--dg-text-muted)]">
<span>CPU {String((c.resources as Record<string, Record<string, string>>)?.requests?.cpu ?? '0')} / {String((c.resources as Record<string, Record<string, string>>)?.limits?.cpu ?? '—')}</span>
<span>内存 {String((c.resources as Record<string, Record<string, string>>)?.requests?.memory ?? '0')} / {String((c.resources as Record<string, Record<string, string>>)?.limits?.memory ?? '—')}</span>
<span>{t('k8s.pods.memory')} {String((c.resources as Record<string, Record<string, string>>)?.requests?.memory ?? '0')} / {String((c.resources as Record<string, Record<string, string>>)?.limits?.memory ?? '—')}</span>
</div>
)}
</div>
@@ -206,7 +208,7 @@ export function PodsTab({
{/* 行数统计 */}
{pods.length > 0 && (
<div className="text-center py-2 text-xs text-[var(--dg-text-muted)] border-t border-[var(--dg-border)]">
共 {pods.length} 个容器组 {pods.length > 200 && '· 虚拟滚动渲染'}
{t('k8s.pods.totalCount', { count: pods.length })} {pods.length > 200 && `· ${t('k8s.pods.virtualScroll')}`}
</div>
)}
</CardContent>
@@ -1,4 +1,5 @@
// deployment-view/ServicesTab.tsx — 访问方式 Tab // Logan.Li
import { useTranslation } from 'react-i18next';
import { Badge } from '@/components/ui/badge';
import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
@@ -8,6 +9,7 @@ import type { SvcItem, IngItem } from './types';
import type { JsonData } from '@/types/common';
export function ServicesTab({ services, ingresses, loading }: { services: SvcItem[]; ingresses: IngItem[]; loading: string; namespace: string }) {
const { t } = useTranslation();
if (loading === 'services') {
return <div className="flex items-center justify-center py-12"><Loader2 className="h-6 w-6 animate-spin text-[var(--dg-text-muted)]" /></div>;
}
@@ -16,20 +18,20 @@ export function ServicesTab({ services, ingresses, loading }: { services: SvcIte
{/* Services */}
<div>
<h3 className="text-sm font-semibold text-[var(--dg-text-primary)] mb-3 flex items-center gap-2">
<Network className="h-4 w-4 text-[var(--dg-blue)]" />关联 Service
<Network className="h-4 w-4 text-[var(--dg-blue)]" />{t('k8s.services.relatedService')}
</h3>
{services.length === 0 ? (
<p className="text-sm text-[var(--dg-text-muted)] py-4">暂无匹配的 Service(Deployment 选择器未关联任何 Service)</p>
<p className="text-sm text-[var(--dg-text-muted)] py-4">{t('k8s.services.emptyService')}</p>
) : (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="border-[var(--dg-border)]">
<TableHead className="text-[var(--dg-text-muted)]">名称</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">类型</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">{t('k8s.column.name')}</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">{t('k8s.column.type')}</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">ClusterIP</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">端口</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">Selector</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">{t('k8s.column.ports')}</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">{t('k8s.column.selector')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -55,19 +57,19 @@ export function ServicesTab({ services, ingresses, loading }: { services: SvcIte
{/* Ingresses */}
<div>
<h3 className="text-sm font-semibold text-[var(--dg-text-primary)] mb-3 flex items-center gap-2">
<ExternalLink className="h-4 w-4 text-[var(--dg-purple)]" />关联 Ingress
<ExternalLink className="h-4 w-4 text-[var(--dg-purple)]" />{t('k8s.services.relatedIngress')}
</h3>
{ingresses.length === 0 ? (
<p className="text-sm text-[var(--dg-text-muted)] py-4">暂无匹配的 Ingress</p>
<p className="text-sm text-[var(--dg-text-muted)] py-4">{t('k8s.services.emptyIngress')}</p>
) : (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="border-[var(--dg-border)]">
<TableHead className="text-[var(--dg-text-muted)]">名称</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">{t('k8s.column.name')}</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">Class</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">Host</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">路径</TableHead>
<TableHead className="text-[var(--dg-text-muted)]">{t('k8s.column.path')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -88,4 +90,4 @@ export function ServicesTab({ services, ingresses, loading }: { services: SvcIte
</div>
</div>
);
}
}
@@ -1,5 +1,6 @@
// ActionModals.tsx — 调整镜像 / 伸缩 / 删除 弹窗
// 作者: Logan.Li
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { AlertTriangle, Loader2, Minus, Plus, Trash2 } from 'lucide-react';
@@ -39,6 +40,7 @@ export function ActionModals({
onScaleTo,
onDelete,
}: ActionModalsProps) {
const { t } = useTranslation();
return (
<>
{/* ============ 弹窗:调整镜像版本 ============ */}
@@ -55,9 +57,9 @@ export function ActionModals({
{/* ============ 弹窗:伸缩 ============ */}
{modal?.kind === 'scale' && (
<ModalShell title="伸缩 (Scale)" onClose={onClose}>
<ModalShell title={t('k8s.actionModal.scaleTitle')} onClose={onClose}>
<div className="space-y-3">
<p className="text-xs dg-text-muted">当前副本: {deployment.replicas || 0}</p>
<p className="text-xs dg-text-muted">{t('k8s.actionModal.currentReplicas')} {deployment.replicas || 0}</p>
<div className="flex items-center justify-center gap-3 py-3">
<Button variant="outline" size="icon" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]"
disabled={actionLoading === 'scale' || scaleValue <= 0}
@@ -74,9 +76,9 @@ export function ActionModals({
</Button>
</div>
<div className="flex justify-end gap-2 pt-2">
<Button variant="outline" className="border-[var(--dg-border)] dg-text-secondary" onClick={onClose}>取消</Button>
<Button variant="outline" className="border-[var(--dg-border)] dg-text-secondary" onClick={onClose}>{t('common.cancel')}</Button>
<Button className="bg-blue-600 hover:bg-blue-700" disabled={actionLoading === 'scale'} onClick={onScaleTo}>
{actionLoading === 'scale' ? <Loader2 className="h-4 w-4 animate-spin" /> : '确认'}
{actionLoading === 'scale' ? <Loader2 className="h-4 w-4 animate-spin" /> : t('common.confirm')}
</Button>
</div>
</div>
@@ -85,21 +87,21 @@ export function ActionModals({
{/* ============ 弹窗:删除 ============ */}
{modal?.kind === 'delete' && (
<ModalShell title="删除 Deployment" onClose={onClose}>
<ModalShell title={t('k8s.actionModal.deleteTitle')} onClose={onClose}>
<div className="space-y-3">
<div className="flex items-center gap-2 dg-text-red">
<AlertTriangle className="h-5 w-5" />
<span className="font-semibold">此操作不可撤销</span>
<span className="font-semibold">{t('k8s.actionModal.irreversible')}</span>
</div>
<p className="text-sm dg-text-secondary">
确认删除 Deployment <code className="text-[var(--dg-text-primary)] font-mono">{namespace}/{deploymentName}</code>?
其管理的所有 Pod 将被一并回收。
{t('k8s.actionModal.deleteConfirm', { namespace, name: deploymentName })}
{t('k8s.actionModal.deleteConfirmDesc')}
</p>
<div className="flex justify-end gap-2 pt-2">
<Button variant="outline" className="border-[var(--dg-border)] dg-text-secondary" onClick={onClose}>取消</Button>
<Button variant="outline" className="border-[var(--dg-border)] dg-text-secondary" onClick={onClose}>{t('common.cancel')}</Button>
<Button variant="destructive" disabled={actionLoading === 'delete'} onClick={onDelete}>
{actionLoading === 'delete' ? <Loader2 className="h-4 w-4 animate-spin mr-1" /> : <Trash2 className="h-4 w-4 mr-1" />}
确认删除
{t('k8s.actionModal.confirmDelete')}
</Button>
</div>
</div>