- 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 回归通过
240 lines
9.5 KiB
TypeScript
240 lines
9.5 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';
|
||
|
||
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>
|
||
);
|
||
}
|