// WorkloadViewPage.tsx — StatefulSet / DaemonSet 详情页(对标 DeploymentWorkload) // 支持 view / yaml 双模式 // 作者: Logan 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 (

{label}

{value}

{sub &&

{sub}

}
); } // Helper: render key-value pairs as badges function LabelBadges({ labels }: { labels: Record | undefined }) { const { t } = useTranslation(); const entries = Object.entries(labels || {}); if (entries.length === 0) return {t('common.none')}; return (
{entries.map(([k, v]) => ( {k}={v} ))}
); } 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(null); const [pods, setPods] = useState([]); const [loading, setLoading] = useState(true); const [scaleModal, setScaleModal] = useState(false); const [scaleValue, setScaleValue] = useState(1); const [actionLoading, setActionLoading] = useState(false); const [terminalPod, setTerminalPod] = useState(null); const [fileBrowserPod, setFileBrowserPod] = useState(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 = 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[] = spec?.template?.spec?.containers || []; const initContainers: Record[] = 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[] = spec.volumeClaimTemplates || []; // Selector const selectorLabels = spec.selector?.matchLabels || {}; // Labels & Annotations const labels = manifest?.metadata?.labels || {}; const annotations = manifest?.metadata?.annotations || {}; const modeTabs = (
); return ( } loading={loading} loadingLabel=t('kubernetes.workloadviewpage.s4eb429') actions={( <> {modeTabs} {isSts && ( )} )} >
{kind} {namespace} {isHealthy ? {t('common.healthy')} : {t('kubernetes.workloadviewpage.sf339d4')}}
{!loading && (mode === 'yaml' ? ( {}} inline autoEdit /> ) : ( <> {/* Status cards */}
{isSts ? ( <> {formatK8sTime(manifest?.metadata?.creationTimestamp)}} color="var(--dg-text-secondary)" /> ) : ( <> {formatK8sTime(manifest?.metadata?.creationTimestamp)}} color="var(--dg-text-secondary)" /> )}
{/* Update Strategy + Selector */}
更新策略
{t('kubernetes.workloadviewpage.s0511a2')} {strategyType}
{strategyType === 'RollingUpdate' && isSts && updateStrategy.rollingUpdate && ( <>
{t('kubernetes.workloadviewpage.s45cb9d')} {updateStrategy.rollingUpdate.partition ?? 0}
{updateStrategy.rollingUpdate.maxUnavailable && (
{t('kubernetes.daemonset.maxUnavailable')} {typeof updateStrategy.rollingUpdate.maxUnavailable === 'object' ? JSON.stringify(updateStrategy.rollingUpdate.maxUnavailable) : updateStrategy.rollingUpdate.maxUnavailable}
)} )} {strategyType === 'RollingUpdate' && !isSts && updateStrategy.rollingUpdate && ( <> {updateStrategy.rollingUpdate.maxUnavailable && (
{t('kubernetes.daemonset.maxUnavailable')} {typeof updateStrategy.rollingUpdate.maxUnavailable === 'object' ? JSON.stringify(updateStrategy.rollingUpdate.maxUnavailable) : updateStrategy.rollingUpdate.maxUnavailable}
)} {updateStrategy.rollingUpdate.maxSurge && (
{t('kubernetes.workloadviewpage.sfed4ce')} {typeof updateStrategy.rollingUpdate.maxSurge === 'object' ? JSON.stringify(updateStrategy.rollingUpdate.maxSurge) : updateStrategy.rollingUpdate.maxSurge}
)} )} {isSts && (
{t('kubernetes.statefulset.podManagementPolicy')} {spec.podManagementPolicy || 'OrderedReady'}
)}
{t('kubernetes.workloadviewpage.s450a4e')} {spec.revisionHistoryLimit ?? 10}
选择器与标签

Selector (matchLabels)

Labels

{Object.keys(annotations).length > 0 && (

Annotations

{Object.entries(annotations).slice(0, 8).map(([k, _v]) => ( {k} ))}
)}
{/* VolumeClaimTemplates (StatefulSet only) */} {isSts && vcts.length > 0 && ( 存储卷声明模板 (VolumeClaimTemplates) {t('common.name')} {t('k8s.resource.accessMode')} {t('k8s.resource.storageClass')} {t('deploy.table.size')} {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 ( {vct.metadata?.name || '-'} {accessModes || '-'} {storageClass} {size} ); })}
)} {/* Container template */} 容器模板(Pod Template) {initContainers.length > 0 && (

Init Containers

{initContainers.map((c: JsonData, idx: number) => ( ))}
)} {containers.length === 0 ? (

{t('kubernetes.workloadviewpage.se4b610')}

) : (

Containers

{containers.map((c: JsonData, idx: number) => ( ))}
)}
{/* Pod list */} Pod 实例 ({pods.length}) {t('kubernetes.pod.podName')} {t('common.status')} {t('kubernetes.pod.nodeName')} Pod IP {t('common.reboot')} {t('common.actions')} {pods.length === 0 ? ( {t('k8s.deploymentDetail.noPod')} ) : pods.map((pod: JsonData) => ( {pod.name} {pod.status || 'Unknown'} {pod.node_name || pod.node || '-'} {pod.ip || pod.pod_ip || '-'} 0 ? 'text-[var(--dg-orange)]' : 'text-[var(--dg-text-muted)]'}>{pod.restart_count || 0}
))}
))} {/* Scale modal */} {scaleModal && (
setScaleModal(false)}>
e.stopPropagation()}>

{t('kubernetes.workloadviewpage.s7f541e')}

{scaleValue}
)} {/* YAML modal */} {yamlModal && ( setYamlModal(false)} /> )} {/* Pod terminal */} {terminalPod && ( { 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 && ( { if (!o) setFileBrowserPod(null); }} clusterCode={clusterCode} namespace={namespace} podName={fileBrowserPod.name} containers={fileBrowserPod.containers?.map((c: JsonData) => c.name).filter(Boolean) || []} /> )}
); } // Container info sub-component function ContainerInfo({ c }: { c: JsonData}) { const { t } = useTranslation(); return (
{c.name}
{c.image}
CPU: {c.resources?.requests?.cpu || '-'}/{c.resources?.limits?.cpu || '-'}
内存: {c.resources?.requests?.memory || '-'}/{c.resources?.limits?.memory || '-'}
端口: {(c.ports || []).map((p: JsonData) => p.containerPort || p.port).join(', ') || '-'}
); }