Files
aiops/frontend/src/components/kubernetes/DeploymentDetail.tsx
T
attacker c68aef4444 feat(alert): A2 告警引擎分布式安全 — Redis 分布式锁(SetNX)
- Cache 接口新增 SetNX 方法(go-redis 原子 / redka check-then-set 单副本安全)
- AlertEngine 改造为支持分布式锁: tryAcquireLock/releaseLock/GetEngineStatus
- 多副本部署时仅 Leader 执行告警检查, 60s TTL 防死锁, fail-open 容错
- 新增 GET /alerts/engine/status 监控端点(is_leader/instance_id/lock_holder)
- 修复 GrafanaDashboard.tsx Panel.options 类型不匹配导致构建中断
- 同步未提交的前端类型安全迁移(140+ 文件)
- ROADMAP/TDD_PLAN/PROJECT_EVALUATION 三文档同步更新

验证: go build 零错误, Docker 部署 healthy, 引擎状态端点正常,
锁竞争测试通过(非 Leader 跳过检查), 5/5 核心 API 回归通过
2026-06-26 21:13:15 +08:00

240 lines
9.5 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.
// 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';
export function DeploymentDetail({ clusterCode, namespace, deploymentName, onBack }: DeploymentDetailProps) {
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('加载详情失败:', 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: '扩缩容成功', description: `副本数已设置为 ${newReplicas}` });
loadData();
} catch (e) {
toast({ title: '操作失败', description: e?.message || String(e), variant: 'destructive' });
} finally { setActionLoading(''); }
};
const handleRestart = async () => {
setActionLoading('restart');
try {
await kubernetesApi.restartDeployment(clusterCode, namespace, deploymentName);
toast({ title: '重启指令已下发', description: 'Deployment 正在滚动重启' });
} catch (e) {
toast({ title: '重启失败', 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: '镜像更新成功', description: `正在滚动更新为 ${newImage}` });
setShowImageUpdate(false);
loadData();
} catch (e) {
toast({ title: '更新失败', description: e?.message || String(e), variant: 'destructive' });
} finally { setActionLoading(''); }
};
const handleHpaToggle = async (hpa: JsonData) => {
const ok = await confirm({
title: '删除 HPA',
description: `确定删除 HPA "${hpa.name}"?`,
confirmText: '删除',
variant: 'destructive',
});
if (!ok) return;
try {
await kubernetesApi.deleteHPA(clusterCode, namespace, hpa.name);
toast({ title: 'HPA 已删除' });
loadData();
} catch (e) {
toast({ title: '删除失败', description: e?.message || String(e), variant: 'destructive' });
}
};
if (loading) {
return (
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-12 text-center">
<Loader2 className="h-6 w-6 animate-spin mx-auto text-slate-500" />
<p className="text-slate-500 mt-3">加载中...</p>
</CardContent>
</Card>
);
}
if (!deployment) {
return (
<Card className="bg-slate-900 border-slate-800">
<CardContent className="p-12 text-center">
<XCircle className="h-8 w-8 mx-auto text-red-400" />
<p className="text-slate-400 mt-3">未找到 Deployment: {deploymentName}</p>
<Button variant="outline" className="mt-4" onClick={onBack}>返回</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-slate-800 border border-slate-700">
<TabsTrigger value="pods" className="data-[state=active]:bg-slate-700">
Pod 列表 ({pods.length})
</TabsTrigger>
<TabsTrigger value="logs" className="data-[state=active]:bg-slate-700">
<FileText className="h-4 w-4 mr-1" />日志
</TabsTrigger>
<TabsTrigger value="hpa" className="data-[state=active]:bg-slate-700">
<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>
);
}