feat: 合并批量安装/升级为统一弹窗 — 类阿里云风格

- 批量安装/升级合并为一个「批量部署」按钮,弹窗内切换安装/升级模式
- 弹窗展示选中主机列表、当前版本、操作目标
- 增加专业警告文案(AIOps语境,区分安装/升级注意事项)
- 「我已知悉」勾选确认后才可提交
- Agent版本列:提交后显示loading转圈「部署中」状态
- 移除批量卸载按钮
This commit is contained in:
attacker committed 2026-06-19 01:44:03 +00:00
1 parent 0d61be1413
commit 502674d73a
1 file changed
+192 -62
@@ -1,16 +1,16 @@
// 监控大屏 — 标签页切换 Agent / Zabbix / Prometheus + 自定义仪表盘 // Logan.Li
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useMemo } from 'react';
import {
Monitor, Plus, RefreshCw, Maximize2, Minimize2, BarChart3, Clock,
Server, Activity, Database, Wifi, WifiOff, ArrowUpDown, ArrowUp, ArrowDown,
ExternalLink, Download, Trash2, Rocket, Zap
ExternalLink, Download, Rocket, PackageOpen, Loader2, Info
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription,
} from '@/components/ui/dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Label } from '@/components/ui/label';
@@ -24,7 +24,6 @@ import { http } from '@/services/http';
import { asList } from '@/components/agent/agentUtils';
import { AgentWizard } from '@/components/agent/AgentWizard';
import { Checkbox } from '@/components/ui/checkbox';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { useNavigate } from 'react-router-dom';
// ═══════════ Tab 配置 ═══════════
@@ -41,7 +40,6 @@ interface AgentHost { server_id: number; hostname: string; ip: string; cpu_usage
type SortField = 'hostname' | 'agent_version' | 'agent_status' | 'cpu_usage' | 'mem_usage' | 'disk_usage' | 'region' | 'ip';
const AgentTab: React.FC = () => {
const _confirm = useConfirm();
const navigate = useNavigate();
const [hosts, setHosts] = useState<AgentHost[]>([]);
const [loading, setLoading] = useState(true);
@@ -52,6 +50,13 @@ const AgentTab: React.FC = () => {
const [selected, setSelected] = useState<Set<string>>(new Set());
const [wizardOpen, setWizardOpen] = useState(false);
const [wizardServer, setWizardServer] = useState<any>(null);
// 批量部署对话框
const [batchOpen, setBatchOpen] = useState(false);
const [batchMode, setBatchMode] = useState<'install' | 'upgrade'>('install');
const [batchAck, setBatchAck] = useState(false);
const [batchSubmitting, setBatchSubmitting] = useState(false);
// 正在执行操作的主机 ID 集合(版本列 loading)
const [operating, setOperating] = useState<Set<number>>(new Set());
const totalPages = Math.max(1, Math.ceil(hosts.length / pageSize));
const fetch = async () => { try { setHosts(asList(await http.get('/agent/latest'))); } catch {} finally { setLoading(false); } };
useEffect(() => { fetch(); const t = setInterval(fetch, 30000); return () => clearInterval(t); }, []);
@@ -72,53 +77,52 @@ const AgentTab: React.FC = () => {
const selAny = selected.size > 0;
const selIds = [...selected].map(Number).filter(n => !isNaN(n));
// 批量安装 Agent // Logan.Li
const [batchInstalling, setBatchInstalling] = useState(false);
const handleBatchInstall = async () => {
// 选中主机按安装/在线分组
const selectedHosts = useMemo(() => {
return hosts.filter(h => selected.has(String(h.server_id)));
}, [hosts, selected]);
const needInstall = useMemo(() => selectedHosts.filter(h => h.agent_status !== 'online'), [selectedHosts]);
const needUpgrade = useMemo(() => selectedHosts.filter(h => h.agent_status === 'online'), [selectedHosts]);
// 打开批量部署对话框
const openBatchDialog = () => {
setBatchAck(false);
// 自动推断模式: 全无Agent → install,全在线 → upgrade,混合 → install
if (needInstall.length > 0 && needUpgrade.length === 0) setBatchMode('install');
else if (needInstall.length === 0 && needUpgrade.length > 0) setBatchMode('upgrade');
else setBatchMode('install');
setBatchOpen(true);
};
// 执行批量部署
const handleBatchSubmit = async () => {
if (!batchAck) { toast.error('请先确认并勾选「我已知悉」'); return; }
setBatchSubmitting(true);
const token = useUserStore.getState().token;
if (!token) { toast.error('未登录'); return; }
const ok = await _confirm({ title: '批量安装 Agent', description: `确定对选中的 ${selIds.length} 台主机远程安装 Agent?安装将在后台异步执行。`, confirmText: '安装', variant: 'destructive' });
if (!ok) return;
setBatchInstalling(true);
const ids = selectedHosts.map(h => h.server_id);
setOperating(new Set(ids));
try {
const res = await serverApi.batchInstallAgent(selIds, token);
toast.success(`已提交:批量安装 ${res?.total ?? selIds.length} 台主机的 Agent,后台执行中,30秒后自动刷新`);
setSelected(new Set());
setTimeout(() => fetch(), 30000);
} catch { toast.error('提交失败'); }
finally { setBatchInstalling(false); }
};
// 批量卸载 Agent // Logan.Li
const [batchUninstalling, setBatchUninstalling] = useState(false);
const handleBatchUninstall = async () => {
const ok = await _confirm({ title: '批量卸载 Agent', description: `确定卸载选中的 ${selIds.length} 台主机的 AIOps Agent?卸载将在后台异步执行。`, confirmText: '卸载', variant: 'destructive' });
if (!ok) return;
setBatchUninstalling(true);
try {
await agentApi.batchUninstall(selIds);
toast.success(`已提交:批量卸载 ${selIds.length} 台主机的 Agent,后台执行中,30秒后自动刷新`);
setSelected(new Set());
setTimeout(() => fetch(), 30000);
} catch { toast.error('提交失败'); }
finally { setBatchUninstalling(false); }
};
// 批量升级 Agent 到最新版本
const [batchUpgrading, setBatchUpgrading] = useState(false);
const handleBatchUpgrade = async () => {
const online = hosts.filter((h: any) => selIds.includes(h.server_id) && h.agent_status === 'online');
if (online.length === 0) { toast.error('所选主机中没有在线 Agent'); return; }
const ids = online.map((h: any) => h.server_id);
const ok = await _confirm({ title: '批量升级 Agent', description: `确定升级选中的 ${ids.length} 台在线主机的 Agent 到最新版本?升级将在后台异步执行。`, confirmText: '升级', variant: 'default' });
if (!ok) return;
setBatchUpgrading(true);
try {
const res = await agentApi.batchUpgrade(ids);
toast.success(res?.message || `已提交:批量升级 ${ids.length} 台主机的 Agent,后台执行中,30秒后自动刷新`);
setTimeout(() => fetch(), 30000);
} catch { toast.error('提交失败'); }
finally { setBatchUpgrading(false); }
if (batchMode === 'install') {
if (!token) { toast.error('未登录'); return; }
// 安装:只安装无 Agent 的
const installIds = needInstall.map(h => h.server_id);
if (installIds.length === 0) { toast.warning('所选主机均已安装 Agent,无需安装'); return; }
const res = await serverApi.batchInstallAgent(installIds, token);
toast.success(`已提交:批量安装 ${res?.total ?? installIds.length} 台主机的 Agent,后台执行中,30秒后自动刷新`);
setSelected(new Set());
setTimeout(() => { fetch(); setOperating(new Set()); }, 30000);
} else {
// 升级:只升级在线的
const upgradeIds = needUpgrade.map(h => h.server_id);
if (upgradeIds.length === 0) { toast.warning('所选主机中没有在线 Agent,无法升级'); return; }
const res = await agentApi.batchUpgrade(upgradeIds);
toast.success(res?.message || `已提交:批量升级 ${upgradeIds.length} 台主机的 Agent,后台执行中,30秒后自动刷新`);
setSelected(new Set());
setTimeout(() => { fetch(); setOperating(new Set()); }, 30000);
}
setBatchOpen(false);
} catch { toast.error('提交失败'); setOperating(new Set()); }
finally { setBatchSubmitting(false); }
};
if (loading) return <div className="text-center py-20 text-[var(--dg-text-muted)]"><Activity className="h-6 w-6 mx-auto mb-2 animate-spin" /><p className="text-xs">加载中…</p></div>;
@@ -137,17 +141,9 @@ const AgentTab: React.FC = () => {
<Rocket className="w-3 h-3" />安装 Agent
</Button>
{selAny && (
<>
<Button size="sm" variant="outline" className="h-7 text-xs gap-1 border-blue-500/30 text-blue-400 hover:bg-blue-500/10" onClick={handleBatchInstall} disabled={batchInstalling}>
<Rocket className="w-3 h-3" />批量安装 ({selected.size})
</Button>
<Button size="sm" variant="outline" className="h-7 text-xs gap-1 border-red-500/30 text-red-400 hover:bg-red-500/10" onClick={handleBatchUninstall} disabled={batchUninstalling}>
<Trash2 className="w-3 h-3" />批量卸载 ({selected.size})
</Button>
<Button size="sm" variant="outline" className="h-7 text-xs gap-1 border-amber-500/30 text-amber-400 hover:bg-amber-500/10" onClick={handleBatchUpgrade} disabled={batchUpgrading}>
<Zap className="w-3 h-3" />批量升级 ({selected.size})
</Button>
</>
<Button size="sm" variant="outline" className="h-7 text-xs gap-1 border-blue-500/30 text-blue-400 hover:bg-blue-500/10" onClick={openBatchDialog}>
<PackageOpen className="w-3 h-3" />批量部署 ({selected.size})
</Button>
)}
<Button size="sm" variant="ghost" className="h-7 text-xs gap-1" onClick={fetch}><RefreshCw className="w-3 h-3" />刷新</Button>
</div>
@@ -156,6 +152,131 @@ const AgentTab: React.FC = () => {
onInstalled={() => { toast.success('Agent 安装成功'); setWizardOpen(false); fetch(); }}
onFailed={() => toast.error('安装失败')} />
{/* ────────── 批量部署 Agent 对话框 ────────── */}
<Dialog open={batchOpen} onOpenChange={(v) => { if (!v) { setBatchOpen(false); setBatchAck(false); } }}>
<DialogContent className="max-w-2xl max-h-[85vh] overflow-auto bg-slate-900 border-slate-700 text-slate-100">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-base">
<PackageOpen className="w-5 h-5 text-blue-400" />
批量部署 Agent
</DialogTitle>
<DialogDescription className="text-slate-400 text-xs">
选中 {selectedHosts.length} 台主机(需安装: {needInstall.length},待升级: {needUpgrade.length})
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* 模式选择 — 类似阿里云安装/升级切换 */}
<div className="flex items-center gap-2 p-1 rounded-lg bg-slate-800 border border-slate-700 w-fit">
{(['install', 'upgrade'] as const).map(m => (
<button
key={m}
disabled={m === 'install' ? needInstall.length === 0 : needUpgrade.length === 0}
onClick={() => setBatchMode(m)}
className={`px-4 py-1.5 rounded text-xs font-medium transition-colors disabled:opacity-30 disabled:cursor-not-allowed ${
batchMode === m ? 'bg-blue-600 text-white shadow-sm' : 'text-slate-400 hover:text-slate-200'
}`}
>
{m === 'install' ? `⚡ 安装 (${needInstall.length})` : `🔄 升级到最新版 (${needUpgrade.length})`}
</button>
))}
</div>
{/* 主机列表 */}
<div className="rounded-md border border-slate-700 overflow-hidden">
<table className="w-full text-xs">
<thead className="bg-slate-800">
<tr>
<th className="px-3 py-2 text-left font-medium text-slate-400">主机名</th>
<th className="px-3 py-2 text-left font-medium text-slate-400">IP</th>
<th className="px-3 py-2 text-left font-medium text-slate-400">状态</th>
<th className="px-3 py-2 text-left font-medium text-slate-400">当前版本</th>
<th className="px-3 py-2 text-left font-medium text-slate-400">操作</th>
</tr>
</thead>
<tbody>
{(batchMode === 'install' ? needInstall : needUpgrade).map(h => (
<tr key={h.server_id} className="border-t border-slate-800 hover:bg-slate-800/50">
<td className="px-3 py-2 font-medium text-slate-200">{h.hostname}</td>
<td className="px-3 py-2 font-mono text-slate-400">{h.ip}</td>
<td className="px-3 py-2">
{h.agent_status === 'online'
? <Badge className="text-[10px] bg-emerald-500/20 text-emerald-400">在线</Badge>
: <Badge className="text-[10px] bg-slate-500/20 text-slate-400">
{h.agent_status === 'not_installed' ? '未安装' : h.agent_status || '离线'}
</Badge>}
</td>
<td className="px-3 py-2 font-mono text-slate-400">{h.agent_version || '—'}</td>
<td className="px-3 py-2">
<span className="text-blue-400 text-[11px]">
{batchMode === 'install' ? '即将安装' : '即将升级'}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* ⚠️ 警告文案 — 参考阿里云批量安装/升级风格 */}
<div className="rounded-md border border-amber-500/30 bg-amber-500/5 p-3 space-y-2">
<div className="flex items-start gap-2">
<Info className="w-4 h-4 text-amber-400 mt-0.5 shrink-0" />
<div>
<p className="text-xs font-medium text-amber-300 mb-1">
{batchMode === 'install' ? '本次安装' : '本次升级'},主机监控探针将部署 AIOps Agent。AIOps Agent 是 AIOps 平台推出的新一代数据采集器,提供主机监控指标采集和可观测数据采集能力。
</p>
<p className="text-[11px] text-amber-400/80 leading-relaxed">
{batchMode === 'upgrade' && '升级前请注意以下事项:'}
{batchMode === 'install' && '安装前请注意以下事项:'}
</p>
<ul className="mt-1 space-y-0.5 text-[11px] text-slate-400 list-disc list-inside">
{batchMode === 'upgrade' && (
<>
<li>如果您的机器已经安装旧版 Agent,升级操作将停止旧版并替换为最新版本。</li>
<li>如果您的机器之前安装过 Agent 但已卸载,遗留有相关采集配置,本次升级可能导致配置重新生效。升级前请确保存量废弃配置已经移除。</li>
<li>如果您的机器采用自定义安装的 Agent(非 AIOps 提供的一键安装脚本),请不要直接使用批量升级。</li>
<li>批量升级前,请务必先单机灰度升级,确保升级成功且不影响业务后再操作。</li>
</>
)}
{batchMode === 'install' && (
<>
<li>安装将自动检测目标主机架构(amd64/arm64),下载对应版本的 Agent 二进制并启动服务。</li>
<li>请确保目标主机的 SSH 端口(默认 22)可访问,且已关联有效的 SSH 凭据。</li>
<li>安装过程约 30-60 秒,期间目标主机 CPU 负载会短暂升高。</li>
<li>若安装失败,请检查目标主机的防火墙规则和 SSH 服务状态后重试。</li>
</>
)}
<li>操作不可回滚,请在操作前确认已备份关键配置。</li>
<li>操作将在后台异步执行,您可以在「任务执行记录」中查看详细进度。</li>
</ul>
</div>
</div>
</div>
{/* 我已知悉 勾选 */}
<div className="flex items-center gap-2">
<Checkbox id="batch-ack" checked={batchAck} onCheckedChange={(c) => setBatchAck(!!c)} />
<label htmlFor="batch-ack" className="text-xs text-slate-300 cursor-pointer select-none">我已知悉以上风险和注意事项</label>
</div>
</div>
<DialogFooter className="gap-2">
<Button variant="outline" className="border-slate-700 text-slate-300 h-8 text-xs" onClick={() => { setBatchOpen(false); setBatchAck(false); }}>
取消
</Button>
<Button
className="bg-blue-600 hover:bg-blue-700 text-white h-8 text-xs gap-1"
disabled={batchSubmitting || !batchAck || (batchMode === 'install' ? needInstall.length === 0 : needUpgrade.length === 0)}
onClick={handleBatchSubmit}
>
{batchSubmitting && <Loader2 className="w-3 h-3 animate-spin" />}
{batchMode === 'install' ? '确认安装' : '确认升级'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<div className="overflow-x-auto rounded-lg border border-[var(--dg-border)]">
<table className="w-full text-xs">
<thead className="bg-[var(--dg-bg-tertiary)] border-b"><tr>
@@ -179,7 +300,16 @@ const AgentTab: React.FC = () => {
}} />
</td>
<td className="px-3 py-2.5 font-medium text-[var(--dg-text-primary)]">{h.hostname}</td>
<td className="px-3 py-2.5 font-mono text-[var(--dg-text-secondary)]">{h.agent_version || '—'}</td>
<td className="px-3 py-2.5 font-mono text-[var(--dg-text-secondary)]">
{operating.has(h.server_id) ? (
<span className="inline-flex items-center gap-1 text-blue-400">
<Loader2 className="w-3 h-3 animate-spin" />
<span className="text-[10px]">部署中</span>
</span>
) : (
h.agent_version || '—'
)}
</td>
<td className="px-3 py-2.5">{h.agent_status==='online'?<Badge className="text-[10px] bg-emerald-500/20 text-emerald-400"><Wifi className="w-3 h-3 mr-1"/>在线</Badge>:h.agent_status==='not_installed'?<Badge className="text-[10px] bg-amber-500/20 text-amber-400">未安装</Badge>:<Badge className="text-[10px] bg-slate-500/10 text-slate-400"><WifiOff className="w-3 h-3 mr-1"/>离线</Badge>}</td>
<td className="px-3 py-2.5">{bar(Number(h.cpu_usage||0), `${Number(h.cpu_usage||0).toFixed(1)}%`)}</td>
<td className="px-3 py-2.5">{bar(Number(h.mem_usage||0), `${Number(h.mem_usage||0).toFixed(1)}%`)}</td>