feat: 合并批量安装/升级为统一弹窗 — 类阿里云风格
- 批量安装/升级合并为一个「批量部署」按钮,弹窗内切换安装/升级模式 - 弹窗展示选中主机列表、当前版本、操作目标 - 增加专业警告文案(AIOps语境,区分安装/升级注意事项) - 「我已知悉」勾选确认后才可提交 - Agent版本列:提交后显示loading转圈「部署中」状态 - 移除批量卸载按钮
This commit is contained in:
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>
|
||||
|
||||
Reference in new issue
Block a user