Files
aiops/frontend/src/components/kubernetes/DeploymentDetail.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

242 lines
9.9 KiB
TypeScript

// DeploymentDetail.tsx — Deployment 详情页编排组件 // Logan.Li
import { useState, useEffect, useCallback } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { FileText, Gauge, Loader2, XCircle } from 'lucide-react';
import { kubernetesApi } from '@/api';
import { useToast } from '@/hooks/use-toast';
import { useConfirm } from '@/components/ui/confirm-dialog';
import type { DeploymentDetailProps, PodWithContainers } from './deployment-detail/types';
import { DeploymentOverview } from './deployment-detail/DeploymentOverview';
import { DeploymentPodsTab } from './deployment-detail/DeploymentPodsTab';
import { DeploymentLogsTab } from './deployment-detail/DeploymentLogsTab';
import { DeploymentHpaTab } from './deployment-detail/DeploymentHpaTab';
import { DeploymentPodModals } from './deployment-detail/DeploymentPodModals';
import { YamlViewer } from './YamlViewer';
import type { JsonData } from '@/types/common';
import { useTranslation } from 'react-i18next';
export function DeploymentDetail({ clusterCode, namespace, deploymentName, onBack }: DeploymentDetailProps) {
const { t } = useTranslation();
const { toast } = useToast();
const confirm = useConfirm();
const [deployment, setDeployment] = useState<JsonData>(null);
const [pods, setPods] = useState<JsonData[]>([]);
const [hpas, setHpas] = useState<JsonData[]>([]);
const [loading, setLoading] = useState(true);
const [actionLoading, setActionLoading] = useState('');
const [, setScaleValue] = useState(0);
const [newImage, setNewImage] = useState('');
const [showImageUpdate, setShowImageUpdate] = useState(false);
const [logTail, setLogTail] = useState(200);
const [execPod, setExecPod] = useState<PodWithContainers | null>(null);
const [execOpen, setExecOpen] = useState(false);
const [logsViewerPod, setLogsViewerPod] = useState<PodWithContainers | null>(null);
const [logsViewerOpen, setLogsViewerOpen] = useState(false);
const [fileBrowserPod, setFileBrowserPod] = useState<PodWithContainers | null>(null);
const [fileBrowserOpen, setFileBrowserOpen] = useState(false);
const [yamlPodName, setYamlPodName] = useState<string | null>(null);
const [logsTabPod, setLogsTabPod] = useState<string>('');
const loadData = useCallback(async () => {
setLoading(true);
try {
const [depRes, podRes, hpaRes] = await Promise.all([
kubernetesApi.getNamespaceDeployments(clusterCode, namespace).catch(() => []),
kubernetesApi.getNamespacePods(clusterCode, namespace).catch(() => []),
kubernetesApi.getHPAs(clusterCode, namespace).catch(() => ({ items: [] })),
]);
const toArray = (v: JsonData) => (Array.isArray(v) ? v : (v?.list || v?.items || []));
const allDeps = toArray(depRes);
const allPods = toArray(podRes);
const allHpas = toArray(hpaRes);
const dep = allDeps.find((d: JsonData) => d.name === deploymentName && d.namespace === namespace);
setDeployment(dep || null);
if (dep) {
setScaleValue(dep.replicas || 0);
setNewImage(dep.image || '');
}
setPods(allPods.filter((p: JsonData) => p.namespace === namespace && p.name.startsWith(deploymentName)));
setHpas(allHpas.filter((h: JsonData) => h.target_name === deploymentName && h.namespace === namespace));
} catch (e) {
console.error(t('kubernetes.deploymentdetail.sb46ea9'), e);
} finally {
setLoading(false);
}
}, [clusterCode, namespace, deploymentName]);
useEffect(() => { loadData(); }, [loadData]);
const handleScale = async (delta: number) => {
const newReplicas = (deployment?.replicas || 0) + delta;
if (newReplicas < 0) return;
setActionLoading('scale');
try {
await kubernetesApi.scaleDeployment(clusterCode, namespace, deploymentName, newReplicas);
toast({ title: t('kubernetes.deploymentdetail.s0b3ea1'), description: t('kubernetes.deploymentdetail.sacbcbd', { newReplicas: newReplicas }) });
loadData();
} catch (e) {
toast({ title: t('common.operationFailed'), description: e?.message || String(e), variant: 'destructive' });
} finally { setActionLoading(''); }
};
const handleRestart = async () => {
setActionLoading('restart');
try {
await kubernetesApi.restartDeployment(clusterCode, namespace, deploymentName);
toast({ title: t('kubernetes.deploymentdetail.s68a700'), description: t('kubernetes.deploymentdetail.sd18761') });
} catch (e) {
toast({ title: t('kubernetes.deployment.restartFailed'), description: e?.message || String(e), variant: 'destructive' });
} finally { setActionLoading(''); }
};
const handleUpdateImage = async () => {
if (!newImage.trim()) return;
setActionLoading('image');
try {
await kubernetesApi.updateDeploymentImage(clusterCode, namespace, deploymentName, newImage);
toast({ title: t('kubernetes.deploymentdetail.s6df3c3'), description: t('kubernetes.deploymentdetail.sa982c3', { newImage: newImage }) });
setShowImageUpdate(false);
loadData();
} catch (e) {
toast({ title: t('alert.updateFailed'), description: e?.message || String(e), variant: 'destructive' });
} finally { setActionLoading(''); }
};
const handleHpaToggle = async (hpa: JsonData) => {
const ok = await confirm({
title: t('kubernetes.hpa.deleteHpa'),
description: t('kubernetes.deploymentdetail.s209f7e', { hpa.name: hpa.name }),
confirmText: t('common.delete'),
variant: 'destructive',
});
if (!ok) return;
try {
await kubernetesApi.deleteHPA(clusterCode, namespace, hpa.name);
toast({ title: t('kubernetes.deploymentdetail.s0f958f') });
loadData();
} catch (e) {
toast({ title: t('common.deleteFailed'), description: e?.message || String(e), variant: 'destructive' });
}
};
if (loading) {
return (
<Card className="dg-card">
<CardContent className="p-12 text-center">
<Loader2 className="h-6 w-6 animate-spin mx-auto dg-text-muted" />
<p className="dg-text-muted mt-3">{t('common.loading')}</p>
</CardContent>
</Card>
);
}
if (!deployment) {
return (
<Card className="dg-card">
<CardContent className="p-12 text-center">
<XCircle className="h-8 w-8 mx-auto dg-text-red" />
<p className="dg-text-secondary mt-3">未找到 Deployment: {deploymentName}</p>
<Button variant="outline" className="mt-4" onClick={onBack}>{t('common.back')}</Button>
</CardContent>
</Card>
);
}
const ready = deployment.ready_replicas || deployment.readyReplicas || 0;
const total = deployment.replicas || 0;
const available = deployment.available_replicas || deployment.availableReplicas || 0;
return (
<div className="space-y-6">
<DeploymentOverview
deploymentName={deploymentName}
namespace={namespace}
deployment={deployment}
hpas={hpas}
clusterCode={clusterCode}
ready={ready}
total={total}
available={available}
isHealthy={ready === total}
actionLoading={actionLoading}
newImage={newImage}
showImageUpdate={showImageUpdate}
onBack={onBack}
onRefresh={loadData}
onScale={handleScale}
onRestart={handleRestart}
onNewImageChange={setNewImage}
onToggleImageUpdate={() => setShowImageUpdate((v) => !v)}
onUpdateImage={handleUpdateImage}
/>
<Tabs defaultValue="pods">
<TabsList className="bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)]">
<TabsTrigger value="pods" className="data-[state=active]:bg-[var(--dg-bg-active)]">
Pod 列表 ({pods.length})
</TabsTrigger>
<TabsTrigger value="logs" className="data-[state=active]:bg-[var(--dg-bg-active)]">
<FileText className="h-4 w-4 mr-1" />日志
</TabsTrigger>
<TabsTrigger value="hpa" className="data-[state=active]:bg-[var(--dg-bg-active)]">
<Gauge className="h-4 w-4 mr-1" />HPA ({hpas.length})
</TabsTrigger>
</TabsList>
<DeploymentPodsTab
pods={pods}
onOpenLogsViewer={(pod) => { setLogsViewerPod(pod); setLogsViewerOpen(true); }}
onOpenExec={(pod) => { setExecPod(pod); setExecOpen(true); }}
onOpenFileBrowser={(pod) => { setFileBrowserPod(pod); setFileBrowserOpen(true); }}
onOpenYaml={setYamlPodName}
/>
<DeploymentLogsTab
pods={pods}
logTail={logTail}
onLogTailChange={setLogTail}
logsTabPod={logsTabPod}
onLogsTabPodChange={setLogsTabPod}
onOpenLogsViewer={(name, containers) => { setLogsViewerPod({ name, containers }); setLogsViewerOpen(true); }}
/>
<DeploymentHpaTab
hpas={hpas}
clusterCode={clusterCode}
namespace={namespace}
deploymentName={deploymentName}
onCreated={loadData}
onDeleteHpa={handleHpaToggle}
/>
</Tabs>
{yamlPodName && (
<YamlViewer
clusterCode={clusterCode}
namespace={namespace}
resource="pods"
name={yamlPodName}
readOnly
onClose={() => setYamlPodName(null)}
/>
)}
<DeploymentPodModals
clusterCode={clusterCode}
namespace={namespace}
execPod={execPod}
execOpen={execOpen}
onExecOpenChange={(o) => { setExecOpen(o); if (!o) setExecPod(null); }}
logsViewerPod={logsViewerPod}
logsViewerOpen={logsViewerOpen}
onLogsViewerOpenChange={(o) => { setLogsViewerOpen(o); if (!o) setLogsViewerPod(null); }}
fileBrowserPod={fileBrowserPod}
fileBrowserOpen={fileBrowserOpen}
onFileBrowserOpenChange={(o) => { setFileBrowserOpen(o); if (!o) setFileBrowserPod(null); }}
/>
</div>
);
}