批量转换 163 个文件,3204 处中文替换为 t() 调用: - Monitor: 日志中心/告警中心/数据源/大屏/站点拨测/SNMP/中间件 全部转换 - Kubernetes: ResourceVisualEditor/NamespaceResourcePage/拓扑/事件 全部转换 - Deploy: 流水线/发布/凭证/RBAC 全部转换 - Server/Cloud/Agent/Task/Layout/UI: 全部转换 - 翻译词典扩展至 6474 行 (22 namespaces, 5000+ keys) - tsc 编译零错误
577 lines
30 KiB
TypeScript
577 lines
30 KiB
TypeScript
// 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>
|
||
);
|
||
}
|