= 80 ? 'bg-orange-500' : 'bg-emerald-500'}`} style={{ width: `${Math.min(v, 100)}%` }} />
= 80 ? 'text-orange-400' : ''}`}>{l}
);
const selAll = selected.size === hosts.length && hosts.length > 0;
const selAny = selected.size > 0;
const selIds = [...selected].map(Number).filter(n => !isNaN(n));
// 选中主机按安装/在线分组
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;
const ids = selectedHosts.map(h => h.server_id);
setOperating(new Set(ids));
try {
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
;
if (!hosts.length) return
暂无服务器资产
请先在「服务器管理」中添加主机,然后在此安装 Agent
;
return (
{/* 工具栏 */}
{selAny &&
已选 {selected.size} 项}
{selAny && (
<>
>
)}
{ toast.success('Agent 安装成功'); setWizardOpen(false); fetch(); }}
onFailed={() => toast.error('安装失败')} />
{/* ────────── 批量部署 Agent 对话框 ────────── */}
|
setSelected(c ? new Set(hosts.map(h => String(h.server_id))) : new Set())} />
|
{['hostname','agent_version','agent_status','cpu_usage','mem_usage','disk_usage','region','ip'].map(f => (
toggle(f as SortField)}>
{{hostname:'主机名',agent_version:'Agent版本',agent_status:'状态',cpu_usage:'CPU',mem_usage:'内存',disk_usage:'磁盘',region:'区域',ip:'IP'}[f]}
|
))}
操作 |
{paged.map((h, i) => (
|
{
const next = new Set(selected);
c ? next.add(String(h.server_id)) : next.delete(String(h.server_id));
setSelected(next);
}} />
|
{h.hostname} |
{operating.has(h.server_id) ? (
部署中
) : (
h.agent_version || '—'
)}
|
{h.agent_status==='online'?在线:h.agent_status==='not_installed'?未安装:离线} |
{bar(Number(h.cpu_usage||0), `${Number(h.cpu_usage||0).toFixed(1)}%`)} |
{bar(Number(h.mem_usage||0), `${Number(h.mem_usage||0).toFixed(1)}%`)} |
{bar(Number(h.disk_usage||0), `${Number(h.disk_usage||0).toFixed(1)}%`)} |
{h.region||'—'} |
{h.ip||'—'} |
|
))}
{hosts.length > pageSize && (
共 {hosts.length} 条
{page} / {totalPages}
)}
);
};
// ═══════════ 数据源 Wrapper ═══════════
const ZabbixTab: React.FC = () => {
const [ds, setDs] = useState
(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
zabbixDatasourceApi.list().then((r: any) => {
const list = r?.data || r || [];
setDs(list.find((d: any) => d.is_default) || list[0] || null);
}).catch(() => {}).finally(() => setLoading(false));
}, []);
if (loading) return ;
if (!ds) return ;
return ;
};
const PrometheusTab: React.FC = () => {
const [ds, setDs] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
promDatasourceApi.list().then((r: any) => {
const list = r?.data || r || [];
setDs(list.find((d: any) => d.is_default) || list[0] || null);
}).catch(() => {}).finally(() => setLoading(false));
}, []);
if (loading) return ;
if (!ds) return ;
return ;
};
// ═══════════ 自定义仪表盘 ═══════════
interface Dashboard { id: string; name: string; description: string; datasource: string; datasourceUrl: string; panels: Panel[]; createdAt: string; updatedAt: string; createdBy: string; isDefault: boolean; }
export interface Panel { id: string; title: string; type: string; query: string; datasource: string; width: number; height: number; x: number; y: number; options: any; }
const DashboardTab: React.FC = () => {
const [dashboards, setDashboards] = useState([]);
const [active, setActive] = useState(null);
const [createOpen, setCreateOpen] = useState(false);
const [addOpen, setAddOpen] = useState(false);
const [nd, setNd] = useState({ name: '', description: '', datasource: 'prometheus' as const, datasourceUrl: '' });
const [np, setNp] = useState({ title: '', type: 'graph' as const, query: '', width: 6, height: 4 });
const fetch = async () => {
try { const res = await dashboardApi.getDashboards(); const list = res.data || []; setDashboards(list); if (list.length > 0 && !active) setActive(list[0]); } catch {}
};
useEffect(() => { fetch(); }, []);
const create = async () => {
if (!nd.name) { toast.error('请输入名称'); return; }
try { await dashboardApi.createDashboard({ ...nd, panels: [] }); toast.success('创建成功'); setCreateOpen(false); setNd({ name: '', description: '', datasource: 'prometheus', datasourceUrl: '' }); fetch(); } catch { toast.error('创建失败'); }
};
const addPanel = async () => {
if (!np.title || !np.query || !active) return;
try {
const p: Panel = { id: Date.now().toString(), ...np, datasource: active.datasource, x: 0, y: 0, options: { legend: true, tooltip: true, refreshInterval: 30 } };
await dashboardApi.updateDashboard(active.id, { ...active, panels: [...active.panels, p] });
toast.success('面板添加成功'); setAddOpen(false); setNp({ title: '', type: 'graph', query: '', width: 6, height: 4 }); fetch();
} catch { toast.error('添加失败'); }
};
return (
{active &&
}
{active ? (
active.panels.length === 0 ? (
) : (
{active.panels.map(p => (
{p.title}
))}
)
) : (
)}
);
};
// ═══════════ 主组件 ═══════════
const GrafanaDashboard: React.FC = () => {
const [tab, setTab] = useState('agent');
const [fullscreen, setFullscreen] = useState(false);
return (
{/* Tab 切换栏 */}
{TABS.map(t => (
))}
{/* Tab 内容 */}
{tab === 'agent' &&
}
{tab === 'zabbix' &&
}
{tab === 'prometheus' &&
}
{tab === 'dashboard' &&
}
);
};
export default GrafanaDashboard;