Files
aiops/frontend/src/components/kubernetes/WorkloadViewPage.tsx
T
attacker ecec73989b feat(i18n): 剩余模块全量国际化 — 51% 覆盖率 (231 文件接入 t())
批量转换 163 个文件,3204 处中文替换为 t() 调用:
- Monitor: 日志中心/告警中心/数据源/大屏/站点拨测/SNMP/中间件 全部转换
- Kubernetes: ResourceVisualEditor/NamespaceResourcePage/拓扑/事件 全部转换
- Deploy: 流水线/发布/凭证/RBAC 全部转换
- Server/Cloud/Agent/Task/Layout/UI: 全部转换
- 翻译词典扩展至 6474 行 (22 namespaces, 5000+ keys)
- tsc 编译零错误
2026-07-03 01:51:57 +08:00

577 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// WorkloadViewPage.tsx — StatefulSet / DaemonSet 详情页(对标 DeploymentWorkload)
// 支持 view / yaml 双模式
// 作者: Logan <admin@attacker.club>
import { useState, useEffect, useCallback } from 'react';
import { useParams, useNavigate, useLocation } from 'react-router-dom';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
} from '@/components/ui/table';
import {
ArrowLeft, RotateCcw, FileCode, Activity, Container, Terminal,
HardDrive, Minus, Plus, Boxes, Tag, Settings, Database,
} from 'lucide-react';
import { kubernetesApi } from '@/api';
import { useToast } from '@/hooks/use-toast';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { YamlViewer } from './YamlViewer';
import { PodExecTerminal } from './PodExecTerminal';
import { PodFileBrowser } from './PodFileBrowser';
import { formatK8sTime, statusColor, STATUS_COLORS } from './k8s-yaml-utils';
import { PageShell } from '@/components/layout/PageShell';
import type { JsonData } from '@/types/common';
import { useTranslation } from 'react-i18next';
type WorkloadKind = 'StatefulSet' | 'DaemonSet';
type ViewMode = 'view' | 'yaml';
function StatCard({ label, value, sub, color }: { label: string; value: React.ReactNode; sub?: string; color?: string }) {
const { t } = useTranslation();
return (
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-3">
<p className="text-xs text-[var(--dg-text-muted)]">{label}</p>
<p className="text-base font-bold mt-0.5" style={{ color: color || 'var(--dg-text-primary)' }}>{value}</p>
{sub && <p className="text-[10px] text-[var(--dg-text-muted)] mt-0.5">{sub}</p>}
</CardContent>
</Card>
);
}
// Helper: render key-value pairs as badges
function LabelBadges({ labels }: { labels: Record<string, string> | undefined }) {
const { t } = useTranslation();
const entries = Object.entries(labels || {});
if (entries.length === 0) return <span className="text-xs text-[var(--dg-text-muted)]">{t('common.none')}</span>;
return (
<div className="flex flex-wrap gap-1.5">
{entries.map(([k, v]) => (
<Badge key={k} variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)] text-[10px] font-mono">
{k}={v}
</Badge>
))}
</div>
);
}
export default function WorkloadViewPage() {
const { t } = useTranslation();
const params = useParams<{ clusterCode: string; namespace: string; name: string; mode?: string }>();
const navigate = useNavigate();
const location = useLocation();
const { toast } = useToast();
const confirm = useConfirm();
const clusterCode = params.clusterCode || '';
const namespace = params.namespace || '';
const name = params.name || '';
// ⚠️ Fix: kind 是静态路径段(StatefulSet/DaemonSet),不是 :param
// 用 useLocation().pathname 解析,而非 useParams
const kind: WorkloadKind = location.pathname.includes('/DaemonSet/') ? 'DaemonSet' : 'StatefulSet';
const mode: ViewMode = params.mode === 'yaml' ? 'yaml' : 'view';
const [manifest, setManifest] = useState<JsonData>(null);
const [pods, setPods] = useState<JsonData[]>([]);
const [loading, setLoading] = useState(true);
const [scaleModal, setScaleModal] = useState(false);
const [scaleValue, setScaleValue] = useState(1);
const [actionLoading, setActionLoading] = useState(false);
const [terminalPod, setTerminalPod] = useState<JsonData>(null);
const [fileBrowserPod, setFileBrowserPod] = useState<JsonData>(null);
const [yamlModal, setYamlModal] = useState(false);
const loadData = useCallback(async () => {
setLoading(true);
try {
const [yamlRes, podsRes] = await Promise.all([
kubernetesApi.getResourceYaml(clusterCode, namespace, kind.toLowerCase() + 's', name).catch(() => ''),
kubernetesApi.getPods(clusterCode).catch(() => []),
]);
// yamlRes is raw YAML text — try to parse as JSON (K8s API accepts JSON as YAML superset)
let parsed: Record<string, unknown> = null;
if (yamlRes && typeof yamlRes === 'string') {
try { parsed = JSON.parse(yamlRes); } catch { parsed = { _raw: yamlRes }; }
} else if (yamlRes && typeof yamlRes === 'object') {
parsed = yamlRes;
}
setManifest(parsed);
// Filter pods by name prefix and namespace
const podList = Array.isArray(podsRes) ? podsRes : (podsRes?.list || podsRes?.data || []);
const filtered = podList.filter((p: JsonData) => {
const podName = p.name || p.metadata?.name || '';
const podNs = p.namespace || p.namespace_name || '';
return podName.startsWith(name) && (!podNs || podNs === namespace);
});
setPods(filtered);
} catch (e) {
console.error('load error', e);
} finally {
setLoading(false);
}
}, [clusterCode, namespace, name, kind]);
useEffect(() => { loadData(); }, [loadData]);
const handleModeChange = (m: ViewMode) => {
if (m === mode) return;
navigate(`/kubernetes/${clusterCode}/namespace/${namespace}/workload/${m}/${kind}/${name}`, { replace: true });
};
const handleRestart = async () => {
const ok = await confirm({
title: t('kubernetes.deployment.restart'),
description: t('kubernetes.workloadviewpage.s6139f5', { kind: kind, name: name }),
confirmText: t('common.reboot'),
});
if (!ok) return;
setActionLoading(true);
try {
if (kind === 'StatefulSet') {
await kubernetesApi.restartStatefulSet(clusterCode, namespace, name);
} else {
await kubernetesApi.restartDaemonSet(clusterCode, namespace, name);
}
toast({ title: t('kubernetes.workloadviewpage.s68a700'), description: t('kubernetes.workloadviewpage.sb0614d', { kind: kind }) });
setTimeout(loadData, 1000);
} catch (e) {
toast({ title: t('kubernetes.deployment.restartFailed'), description: e?.message || String(e), variant: 'destructive' });
} finally { setActionLoading(false); }
};
const handleScale = async () => {
setActionLoading(true);
try {
await kubernetesApi.scaleStatefulSet(clusterCode, namespace, name, scaleValue);
toast({ title: t('kubernetes.workloadviewpage.s0b3ea1'), description: t('kubernetes.workloadviewpage.s27c3cf', { scaleValue: scaleValue }) });
setScaleModal(false);
setTimeout(loadData, 1000);
} catch (e) {
toast({ title: t('kubernetes.workloadviewpage.s0b329f'), description: e?.message || String(e), variant: 'destructive' });
} finally { setActionLoading(false); }
};
// Parse status from manifest
const spec = manifest?.spec || {};
const status = manifest?.status || {};
const containers: Record<string, unknown>[] = spec?.template?.spec?.containers || [];
const initContainers: Record<string, unknown>[] = spec?.template?.spec?.initContainers || [];
// For StatefulSet
const replicas = spec.replicas ?? 1;
const readyReplicas = status.readyReplicas ?? 0;
const currentReplicas = status.currentReplicas ?? 0;
const updatedReplicas = status.updatedReplicas ?? 0;
// For DaemonSet
const desiredScheduled = status.desiredNumberScheduled ?? 0;
const currentScheduled = status.currentNumberScheduled ?? 0;
const numberReady = status.numberReady ?? 0;
const numberAvailable = status.numberAvailable ?? 0;
const numberUpdated = status.updatedNumberScheduled ?? 0;
const isSts = kind === 'StatefulSet';
const isHealthy = isSts
? (readyReplicas === replicas && replicas > 0)
: (numberReady === desiredScheduled && desiredScheduled > 0);
// Update strategy
const updateStrategy = spec.updateStrategy || {};
const strategyType = updateStrategy.type || (isSts ? 'RollingUpdate' : 'RollingUpdate');
// VolumeClaimTemplates (StatefulSet only)
const vcts: Record<string, unknown>[] = spec.volumeClaimTemplates || [];
// Selector
const selectorLabels = spec.selector?.matchLabels || {};
// Labels & Annotations
const labels = manifest?.metadata?.labels || {};
const annotations = manifest?.metadata?.annotations || {};
const modeTabs = (
<div className="flex items-center gap-1 bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-lg p-0.5">
<button className={`px-3 py-1 text-xs rounded transition-colors ${mode === 'view' ? 'bg-[var(--dg-blue)]/20 text-[var(--dg-blue)]' : 'text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]'}`} onClick={() => handleModeChange('view')}>{t('common.view')}</button>
<button className={`px-3 py-1 text-xs rounded transition-colors ${mode === 'yaml' ? 'bg-[var(--dg-blue)]/20 text-[var(--dg-blue)]' : 'text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]'}`} onClick={() => handleModeChange('yaml')}>YAML</button>
</div>
);
return (
<PageShell
variant="workspace"
title={name}
subtitle={`${clusterCode} / ${namespace} · ${kind}`}
icon={<Boxes className="h-5 w-5" />}
loading={loading}
loadingLabel=t('kubernetes.workloadviewpage.s4eb429')
actions={(
<>
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={() => navigate(-1)}>
<ArrowLeft className="h-4 w-4 mr-1" />返回
</Button>
{modeTabs}
{isSts && (
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={() => { setScaleValue(replicas); setScaleModal(true); }}>
<Boxes className="h-3.5 w-3.5 mr-1" />伸缩
</Button>
)}
<Button variant="outline" size="sm" className="dg-btn-secondary" disabled={actionLoading} onClick={handleRestart}>
<RotateCcw className="h-3.5 w-3.5 mr-1" />{actionLoading ? t('kubernetes.workloadviewpage.s9ac104') : t('common.reboot')}
</Button>
<Button variant="outline" size="sm" className="dg-btn-secondary" onClick={() => setYamlModal(true)}>
<FileCode className="h-3.5 w-3.5 mr-1" />YAML
</Button>
</>
)}
>
<div className="flex items-center gap-2 mb-4">
<Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">{kind}</Badge>
<Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">{namespace}</Badge>
{isHealthy
? <Badge className="bg-[var(--dg-green)]/20 text-[var(--dg-green)] border-[var(--dg-green)]/30">{t('common.healthy')}</Badge>
: <Badge className="bg-[var(--dg-orange)]/20 text-[var(--dg-orange)] border-[var(--dg-orange)]/30">{t('kubernetes.workloadviewpage.sf339d4')}</Badge>}
</div>
{!loading && (mode === 'yaml' ? (
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4">
<YamlViewer
clusterCode={clusterCode}
namespace={namespace}
resource={kind.toLowerCase() + 's'}
name={name}
onClose={() => {}}
inline
autoEdit
/>
</CardContent>
</Card>
) : (
<>
{/* Status cards */}
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3">
{isSts ? (
<>
<StatCard label=t('kubernetes.workloadviewpage.sea6396') value={`${readyReplicas}/${replicas}`} color={isHealthy ? STATUS_COLORS.green : STATUS_COLORS.orange} sub={isHealthy ? t('kubernetes.workloadviewpage.s23c944') : t('kubernetes.workloadviewpage.sac1002')} />
<StatCard label=t('kubernetes.hpa.currentReplicas') value={currentReplicas} color={STATUS_COLORS.blue} />
<StatCard label=t('kubernetes.deployment.updatedReplicas') value={updatedReplicas} color={STATUS_COLORS.green} />
<StatCard label=t('kubernetes.workloadviewpage.sf592ef') value={spec.serviceName || '-'} color="var(--dg-text-secondary)" />
<StatCard label=t('kubernetes.workloadviewpage.s55edf2') value={pods.length} color="var(--dg-text-secondary)" />
<StatCard label=t('common.createdAt') value={<span className="text-xs">{formatK8sTime(manifest?.metadata?.creationTimestamp)}</span>} color="var(--dg-text-secondary)" />
</>
) : (
<>
<StatCard label=t('kubernetes.workloadviewpage.sd78bc8') value={desiredScheduled} color={STATUS_COLORS.blue} />
<StatCard label=t('kubernetes.workloadviewpage.sb76164') value={currentScheduled} color="var(--dg-text-secondary)" />
<StatCard label=t('kubernetes.daemonset.numberReady') value={numberReady} color={isHealthy ? STATUS_COLORS.green : STATUS_COLORS.orange} />
<StatCard label=t('kubernetes.daemonset.numberAvailable') value={numberAvailable} color={STATUS_COLORS.green} />
<StatCard label=t('deploy.toast.updated') value={numberUpdated} color="var(--dg-text-secondary)" />
<StatCard label=t('common.createdAt') value={<span className="text-xs">{formatK8sTime(manifest?.metadata?.creationTimestamp)}</span>} color="var(--dg-text-secondary)" />
</>
)}
</div>
{/* Update Strategy + Selector */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
<Settings className="h-4 w-4 text-[var(--dg-orange)]" />更新策略
</CardTitle>
</CardHeader>
<CardContent className="pt-0 space-y-2 text-xs">
<div className="flex justify-between">
<span className="text-[var(--dg-text-muted)]">{t('kubernetes.workloadviewpage.s0511a2')}</span>
<Badge variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]">{strategyType}</Badge>
</div>
{strategyType === 'RollingUpdate' && isSts && updateStrategy.rollingUpdate && (
<>
<div className="flex justify-between">
<span className="text-[var(--dg-text-muted)]">{t('kubernetes.workloadviewpage.s45cb9d')}</span>
<span className="text-[var(--dg-text-secondary)] font-mono">{updateStrategy.rollingUpdate.partition ?? 0}</span>
</div>
{updateStrategy.rollingUpdate.maxUnavailable && (
<div className="flex justify-between">
<span className="text-[var(--dg-text-muted)]">{t('kubernetes.daemonset.maxUnavailable')}</span>
<span className="text-[var(--dg-text-secondary)] font-mono">{typeof updateStrategy.rollingUpdate.maxUnavailable === 'object' ? JSON.stringify(updateStrategy.rollingUpdate.maxUnavailable) : updateStrategy.rollingUpdate.maxUnavailable}</span>
</div>
)}
</>
)}
{strategyType === 'RollingUpdate' && !isSts && updateStrategy.rollingUpdate && (
<>
{updateStrategy.rollingUpdate.maxUnavailable && (
<div className="flex justify-between">
<span className="text-[var(--dg-text-muted)]">{t('kubernetes.daemonset.maxUnavailable')}</span>
<span className="text-[var(--dg-text-secondary)] font-mono">{typeof updateStrategy.rollingUpdate.maxUnavailable === 'object' ? JSON.stringify(updateStrategy.rollingUpdate.maxUnavailable) : updateStrategy.rollingUpdate.maxUnavailable}</span>
</div>
)}
{updateStrategy.rollingUpdate.maxSurge && (
<div className="flex justify-between">
<span className="text-[var(--dg-text-muted)]">{t('kubernetes.workloadviewpage.sfed4ce')}</span>
<span className="text-[var(--dg-text-secondary)] font-mono">{typeof updateStrategy.rollingUpdate.maxSurge === 'object' ? JSON.stringify(updateStrategy.rollingUpdate.maxSurge) : updateStrategy.rollingUpdate.maxSurge}</span>
</div>
)}
</>
)}
{isSts && (
<div className="flex justify-between">
<span className="text-[var(--dg-text-muted)]">{t('kubernetes.statefulset.podManagementPolicy')}</span>
<span className="text-[var(--dg-text-secondary)] font-mono">{spec.podManagementPolicy || 'OrderedReady'}</span>
</div>
)}
<div className="flex justify-between">
<span className="text-[var(--dg-text-muted)]">{t('kubernetes.workloadviewpage.s450a4e')}</span>
<span className="text-[var(--dg-text-secondary)] font-mono">{spec.revisionHistoryLimit ?? 10}</span>
</div>
</CardContent>
</Card>
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
<Tag className="h-4 w-4 text-[var(--dg-purple)]" />选择器与标签
</CardTitle>
</CardHeader>
<CardContent className="pt-0 space-y-3 text-xs">
<div>
<p className="text-[var(--dg-text-muted)] mb-1.5">Selector (matchLabels)</p>
<LabelBadges labels={selectorLabels} />
</div>
<div>
<p className="text-[var(--dg-text-muted)] mb-1.5">Labels</p>
<LabelBadges labels={labels} />
</div>
{Object.keys(annotations).length > 0 && (
<div>
<p className="text-[var(--dg-text-muted)] mb-1.5">Annotations</p>
<div className="flex flex-wrap gap-1.5">
{Object.entries(annotations).slice(0, 8).map(([k, _v]) => (
<Badge key={k} variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-muted)] text-[10px] font-mono max-w-[300px] truncate">
{k}
</Badge>
))}
</div>
</div>
)}
</CardContent>
</Card>
</div>
{/* VolumeClaimTemplates (StatefulSet only) */}
{isSts && vcts.length > 0 && (
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
<Database className="h-4 w-4 text-[var(--dg-cyan)]" />存储卷声明模板 (VolumeClaimTemplates)
</CardTitle>
</CardHeader>
<CardContent className="pt-0">
<Table>
<TableHeader>
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
<TableHead className="text-[var(--dg-text-secondary)]">{t('common.name')}</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">{t('k8s.resource.accessMode')}</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">{t('k8s.resource.storageClass')}</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">{t('deploy.table.size')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{vcts.map((vct: JsonData, idx: number) => {
const accessModes = (vct.spec?.accessModes || []).join(', ');
const storageClass = vct.spec?.storageClassName || vct.spec?.storageClassName || '-';
const size = vct.spec?.resources?.requests?.storage || '-';
return (
<TableRow key={idx} className="border-[var(--dg-border)]">
<TableCell className="font-medium text-[var(--dg-text-primary)]">{vct.metadata?.name || '-'}</TableCell>
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{accessModes || '-'}</TableCell>
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{storageClass}</TableCell>
<TableCell className="text-sm text-[var(--dg-text-secondary)] font-mono">{size}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</CardContent>
</Card>
)}
{/* Container template */}
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
<Container className="h-4 w-4 text-[var(--dg-blue)]" />容器模板(Pod Template)
</CardTitle>
</CardHeader>
<CardContent className="pt-0">
{initContainers.length > 0 && (
<div className="mb-3">
<p className="text-[11px] text-[var(--dg-text-muted)] mb-2 uppercase tracking-wide">Init Containers</p>
<div className="space-y-2">
{initContainers.map((c: JsonData, idx: number) => (
<ContainerInfo key={`init-${idx}`} c={c} />
))}
</div>
</div>
)}
{containers.length === 0 ? (
<p className="text-xs text-[var(--dg-text-muted)] py-3">{t('kubernetes.workloadviewpage.se4b610')}</p>
) : (
<div className="space-y-3">
<p className="text-[11px] text-[var(--dg-text-muted)] mb-1 uppercase tracking-wide">Containers</p>
{containers.map((c: JsonData, idx: number) => (
<ContainerInfo key={idx} c={c} />
))}
</div>
)}
</CardContent>
</Card>
{/* Pod list */}
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
<Activity className="h-4 w-4 text-[var(--dg-green)]" />Pod 实例 ({pods.length})
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow className="border-[var(--dg-border)] hover:bg-transparent">
<TableHead className="text-[var(--dg-text-secondary)]">{t('kubernetes.pod.podName')}</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">{t('common.status')}</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">{t('kubernetes.pod.nodeName')}</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">Pod IP</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">{t('common.reboot')}</TableHead>
<TableHead className="text-[var(--dg-text-secondary)]">{t('common.actions')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{pods.length === 0 ? (
<TableRow><TableCell colSpan={6} className="text-center text-[var(--dg-text-muted)] py-8">{t('k8s.deploymentDetail.noPod')}</TableCell></TableRow>
) : pods.map((pod: JsonData) => (
<TableRow key={pod.name} className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/30">
<TableCell className="font-medium text-[var(--dg-text-primary)]">{pod.name}</TableCell>
<TableCell>
<Badge style={{ background: `${statusColor(pod.status)}22`, color: statusColor(pod.status), border: `1px solid ${statusColor(pod.status)}44` }}>
{pod.status || 'Unknown'}
</Badge>
</TableCell>
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{pod.node_name || pod.node || '-'}</TableCell>
<TableCell className="text-sm text-[var(--dg-text-secondary)] font-mono">{pod.ip || pod.pod_ip || '-'}</TableCell>
<TableCell className={(pod.restart_count || 0) > 0 ? 'text-[var(--dg-orange)]' : 'text-[var(--dg-text-muted)]'}>{pod.restart_count || 0}</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs text-[var(--dg-green)] hover:text-[var(--dg-green)] hover:bg-[var(--dg-green)]/10"
onClick={() => setTerminalPod(pod)}>
<Terminal className="h-3.5 w-3.5 mr-1" />终端
</Button>
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs text-[var(--dg-orange)] hover:text-[var(--dg-orange)] hover:bg-[var(--dg-orange)]/10"
onClick={() => setFileBrowserPod(pod)}>
<HardDrive className="h-3.5 w-3.5 mr-1" />文件浏览器
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</>
))}
{/* Scale modal */}
{scaleModal && (
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center" onClick={() => setScaleModal(false)}>
<div className="bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-lg p-6 w-80" onClick={e => e.stopPropagation()}>
<h3 className="text-sm font-semibold text-[var(--dg-text-primary)] mb-4">{t('kubernetes.workloadviewpage.s7f541e')}</h3>
<div className="flex items-center justify-center gap-4 mb-4">
<Button variant="outline" size="icon" className="h-9 w-9" onClick={() => setScaleValue(Math.max(0, scaleValue - 1))}>
<Minus className="h-4 w-4" />
</Button>
<span className="text-2xl font-bold text-[var(--dg-text-primary)] w-12 text-center">{scaleValue}</span>
<Button variant="outline" size="icon" className="h-9 w-9" onClick={() => setScaleValue(scaleValue + 1)}>
<Plus className="h-4 w-4" />
</Button>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" className="flex-1 border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={() => setScaleModal(false)}>{t('common.cancel')}</Button>
<Button size="sm" className="flex-1 bg-[var(--dg-blue)] hover:opacity-90" disabled={actionLoading} onClick={handleScale}>
{actionLoading ? t('pipeline.executing') : t('common.confirm')}
</Button>
</div>
</div>
</div>
)}
{/* YAML modal */}
{yamlModal && (
<YamlViewer
clusterCode={clusterCode}
namespace={namespace}
resource={kind.toLowerCase() + 's'}
name={name}
onClose={() => setYamlModal(false)}
/>
)}
{/* Pod terminal */}
{terminalPod && (
<PodExecTerminal
open={true}
onOpenChange={(o) => { if (!o) setTerminalPod(null); }}
clusterId={clusterCode}
namespace={namespace}
podName={terminalPod.name}
containers={terminalPod.containers?.map((c: JsonData) => c.name).filter(Boolean) || []}
/>
)}
{/* Pod file browser */}
{fileBrowserPod && (
<PodFileBrowser
open={true}
onOpenChange={(o) => { if (!o) setFileBrowserPod(null); }}
clusterCode={clusterCode}
namespace={namespace}
podName={fileBrowserPod.name}
containers={fileBrowserPod.containers?.map((c: JsonData) => c.name).filter(Boolean) || []}
/>
)}
</PageShell>
);
}
// Container info sub-component
function ContainerInfo({ c }: { c: JsonData}) {
const { t } = useTranslation();
return (
<div className="border border-[var(--dg-border)] rounded-lg p-3 bg-[var(--dg-bg-primary)]/30">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Container className="h-4 w-4 text-[var(--dg-blue)]" />
<span className="text-sm font-semibold text-[var(--dg-text-primary)]">{c.name}</span>
</div>
<code className="text-[11px] text-[var(--dg-text-secondary)] font-mono truncate max-w-[400px]">{c.image}</code>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 text-xs">
<div className="text-[var(--dg-text-secondary)]">
CPU: <span className="text-[var(--dg-text-primary)] font-mono">{c.resources?.requests?.cpu || '-'}/{c.resources?.limits?.cpu || '-'}</span>
</div>
<div className="text-[var(--dg-text-secondary)]">
内存: <span className="text-[var(--dg-text-primary)] font-mono">{c.resources?.requests?.memory || '-'}/{c.resources?.limits?.memory || '-'}</span>
</div>
<div className="text-[var(--dg-text-secondary)]">
端口: <span className="text-[var(--dg-text-primary)] font-mono">
{(c.ports || []).map((p: JsonData) => p.containerPort || p.port).join(', ') || '-'}
</span>
</div>
</div>
</div>
);
}