feat(rdp): 新增 RDP 远程桌面/SMB/账单同步能力 + 清理编译产物
主要变更: - RDP 远程桌面:新增 backend/pkg/rdp(RDP 引擎 + credssp/display/rle/trace)、 backend/controller/rdp*(连接/凭证/文件/会话/实时)、frontend RDP 组件与 API - Windows SMB 文件传输:backend/pkg/winsmb - 云账单同步:backend/pkg/billingsync + billing_sync/aliyun_billing,前端账单详情 - Agent CLI Kubernetes 工具:kubernetes_action/get、pod_execute/logs - 服务器导入助手:server_import_helpers、ImportCredentialSettings - 国际化与组件重构(cloud/deploy/monitor/server/system 等多模块) - 清理编译产物二进制:移除 backend/server、backend/agent/bin/aiops-agent-* - .gitignore:新增 backend/agent/ 与 .cache/(避免 334MB go-build 缓存误入库) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
777d9e4e71
commit
76ec9387f0
202 files changed
+16555
-2543
No files matched your search
@@ -1,11 +1,10 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
// 监控大屏 — 标签页切换 Agent / Zabbix / Prometheus + 自定义仪表盘 // Logan.Li
|
||||
// 监控大屏 — 标签页切换 Agent / Zabbix / Prometheus // Logan.Li
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
|
||||
Monitor,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
@@ -29,7 +28,6 @@ import {
|
||||
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, DialogDescription,
|
||||
} from '@/components/ui/dialog';
|
||||
@@ -37,8 +35,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { toast } from 'sonner';
|
||||
import { useUserStore } from '@/store';
|
||||
import { dashboardApi, promDatasourceApi, zabbixDatasourceApi, agentApi, serverApi } from '@/api';
|
||||
import { GrafanaPanelContent } from './GrafanaPanelContent';
|
||||
import { promDatasourceApi, zabbixDatasourceApi, agentApi, serverApi } from '@/api';
|
||||
import { ZabbixDashboard } from './ZabbixDashboard';
|
||||
import { PrometheusDashboard } from './PrometheusDashboard';
|
||||
import { asList } from '@/lib/agent-utils';
|
||||
@@ -52,12 +49,11 @@ import { PageLoading } from '@/components/layout/PageLoading';
|
||||
import { PageEmpty } from '@/components/layout/PageEmpty';
|
||||
|
||||
// ═══════════ Tab 配置 ═══════════
|
||||
type TabId = 'agent' | 'zabbix' | 'prometheus' | 'dashboard';
|
||||
type TabId = 'agent' | 'zabbix' | 'prometheus';
|
||||
const getTabs = (t: (key: string) => string): { id: TabId; label: string; icon: React.ReactNode }[] => [
|
||||
{ id: 'agent', label: t('monitor.dashboard.tab.agent'), icon: <Server className="w-3.5 h-3.5" /> },
|
||||
{ id: 'zabbix', label: 'Zabbix', icon: <Activity className="w-3.5 h-3.5" /> },
|
||||
{ id: 'prometheus', label: 'Prometheus', icon: <Activity className="w-3.5 h-3.5" /> },
|
||||
{ id: 'dashboard', label: t('monitor.dashboard.tab.dashboard'), icon: <Monitor className="w-3.5 h-3.5" /> },
|
||||
];
|
||||
|
||||
// ═══════════ Agent 主机监控表格 ═══════════
|
||||
@@ -426,7 +422,11 @@ const AgentTab: React.FC = () => {
|
||||
<td className="px-2 py-2.5">
|
||||
<Checkbox checked={selected.has(String(h.server_id))} onCheckedChange={(c) => {
|
||||
const next = new Set(selected);
|
||||
c ? next.add(String(h.server_id)) : next.delete(String(h.server_id));
|
||||
if (c) {
|
||||
next.add(String(h.server_id));
|
||||
} else {
|
||||
next.delete(String(h.server_id));
|
||||
}
|
||||
setSelected(next);
|
||||
}} />
|
||||
</td>
|
||||
@@ -480,16 +480,65 @@ const AgentTab: React.FC = () => {
|
||||
};
|
||||
|
||||
// ═══════════ 数据源 Wrapper ═══════════
|
||||
interface MonitorDatasource {
|
||||
id: number | string;
|
||||
name?: string;
|
||||
url: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
is_default?: boolean;
|
||||
}
|
||||
|
||||
const normalizeDatasourceList = (r: any): MonitorDatasource[] => {
|
||||
const list = (r?.data || r || []) as MonitorDatasource[];
|
||||
return [...list].sort((a, b) => {
|
||||
if (!!a.is_default !== !!b.is_default) return a.is_default ? -1 : 1;
|
||||
return String(a.name || a.url).localeCompare(String(b.name || b.url));
|
||||
});
|
||||
};
|
||||
|
||||
const DatasourceToolbar: React.FC<{
|
||||
sources: MonitorDatasource[];
|
||||
activeId: string;
|
||||
onChange: (id: string) => void;
|
||||
onRefresh: () => void;
|
||||
refreshing?: boolean;
|
||||
}> = ({ sources, activeId, onChange, onRefresh, refreshing }) => (
|
||||
<div className="flex items-center gap-2 mb-3 flex-wrap">
|
||||
<Select value={activeId} onValueChange={onChange}>
|
||||
<SelectTrigger className="w-[280px] h-8 text-xs dg-input">
|
||||
<SelectValue placeholder="选择数据源" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="dg-input">
|
||||
{sources.map(ds => (
|
||||
<SelectItem key={String(ds.id)} value={String(ds.id)}>
|
||||
{ds.name || ds.url}{ds.is_default ? ' · 默认' : ''}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" size="sm" className="h-8 text-xs gap-1" onClick={onRefresh} disabled={refreshing}>
|
||||
<RefreshCw className={cn('w-3 h-3', refreshing && 'animate-spin')} />刷新数据源
|
||||
</Button>
|
||||
<span className="text-[11px] text-[var(--dg-text-muted)]">共 {sources.length} 个数据源,当前仅查询选中项</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ZabbixTab: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [sources, setSources] = useState<MonitorDatasource[]>([]);
|
||||
const [ds, setDs] = useState<any>(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));
|
||||
}, []);
|
||||
const fetchSources = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const list = normalizeDatasourceList(await zabbixDatasourceApi.list());
|
||||
setSources(list);
|
||||
setDs(current => list.find(d => String(d.id) === String(current?.id)) || list[0] || null);
|
||||
} catch { setSources([]); setDs(null); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
useEffect(() => { fetchSources(); }, []);
|
||||
if (loading) return <PageLoading label={t('monitor.dashboard.zabbix.loading')} />;
|
||||
if (!ds) return (
|
||||
<PageEmpty
|
||||
@@ -498,19 +547,29 @@ const ZabbixTab: React.FC = () => {
|
||||
action={<a href="/datasources" className="text-xs dg-text-blue hover:underline inline-flex items-center gap-1">{t('monitor.dashboard.zabbix.goConfig')} <ExternalLink className="w-3 h-3" /></a>}
|
||||
/>
|
||||
);
|
||||
return <ZabbixDashboard dsUrl={ds.url} dsUsername={ds.username} dsPassword={ds.password} />;
|
||||
return (
|
||||
<div>
|
||||
<DatasourceToolbar sources={sources} activeId={String(ds.id)} onChange={(id) => setDs(sources.find(item => String(item.id) === id) || ds)} onRefresh={fetchSources} refreshing={loading} />
|
||||
<ZabbixDashboard key={String(ds.id)} dsUrl={ds.url} dsUsername={ds.username} dsPassword={ds.password} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PrometheusTab: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [sources, setSources] = useState<MonitorDatasource[]>([]);
|
||||
const [ds, setDs] = useState<any>(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));
|
||||
}, []);
|
||||
const fetchSources = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const list = normalizeDatasourceList(await promDatasourceApi.list());
|
||||
setSources(list);
|
||||
setDs(current => list.find(d => String(d.id) === String(current?.id)) || list[0] || null);
|
||||
} catch { setSources([]); setDs(null); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
useEffect(() => { fetchSources(); }, []);
|
||||
if (loading) return <PageLoading label={t('monitor.dashboard.prometheus.loading')} />;
|
||||
if (!ds) return (
|
||||
<PageEmpty
|
||||
@@ -519,92 +578,10 @@ const PrometheusTab: React.FC = () => {
|
||||
action={<a href="/datasources" className="text-xs dg-text-blue hover:underline inline-flex items-center gap-1">{t('monitor.dashboard.zabbix.goConfig')} <ExternalLink className="w-3 h-3" /></a>}
|
||||
/>
|
||||
);
|
||||
return <PrometheusDashboard dsUrl={ds.url} />;
|
||||
};
|
||||
|
||||
// ═══════════ 自定义仪表盘 ═══════════
|
||||
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 { t } = useTranslation();
|
||||
const [dashboards, setDashboards] = useState<Dashboard[]>([]);
|
||||
const [active, setActive] = useState<Dashboard | null>(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 [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const fetch = async () => {
|
||||
setRefreshing(true);
|
||||
try {
|
||||
const res = await dashboardApi.getDashboards();
|
||||
const list = res.data || [];
|
||||
setDashboards(list);
|
||||
if (list.length > 0 && !active) setActive(list[0]);
|
||||
} catch (e: any) {
|
||||
toast.error(t('monitor.dashboard.custom.toastRefreshFailed', { msg: (e?.message || t('monitor.dashboard.agent.toast.unknownError')) }));
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
useEffect(() => { fetch(); }, []);
|
||||
|
||||
const create = async () => {
|
||||
if (!nd.name) { toast.error(t('monitor.dashboard.custom.toastNameRequired')); return; }
|
||||
try { await dashboardApi.createDashboard({ ...nd, panels: [] }); toast.success(t('monitor.dashboard.custom.toastCreateSuccess')); setCreateOpen(false); setNd({ name: '', description: '', datasource: 'prometheus', datasourceUrl: '' }); fetch(); } catch { toast.error(t('monitor.dashboard.custom.toastCreateFailed')); }
|
||||
};
|
||||
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(t('monitor.dashboard.custom.toastPanelAdded')); setAddOpen(false); setNp({ title: '', type: 'graph', query: '', width: 6, height: 4 }); fetch();
|
||||
} catch { toast.error(t('monitor.dashboard.custom.toastPanelFailed')); }
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={active?.id} onValueChange={id => { const d = dashboards.find(x => x.id === id); if (d) setActive(d); }}>
|
||||
<SelectTrigger className="w-[240px] h-8 text-xs"><SelectValue placeholder={t('monitor.dashboard.custom.selectDashboard')} /></SelectTrigger>
|
||||
<SelectContent>{dashboards.map(d => <SelectItem key={d.id} value={d.id}>{d.name}{d.isDefault ? ` ${t('monitor.dashboard.custom.defaultTag')}` : ''}</SelectItem>)}</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" size="sm" className="h-8 text-[10px]" onClick={fetch} disabled={refreshing}>
|
||||
<RefreshCw className={cn('w-3 h-3 mr-1', refreshing && 'animate-spin')} />{t('monitor.dashboard.refresh')}
|
||||
</Button>
|
||||
{active && <Button variant="outline" size="sm" className="h-8 text-[10px]" onClick={() => setAddOpen(true)}><Plus className="w-3 h-3 mr-1" />{t('monitor.dashboard.custom.addPanel')}</Button>}
|
||||
<Button variant="outline" size="sm" className="h-8 text-[10px]" onClick={() => setCreateOpen(true)}><Plus className="w-3 h-3 mr-1" />{t('monitor.dashboard.custom.create')}</Button>
|
||||
</div>
|
||||
|
||||
{active ? (
|
||||
active.panels.length === 0 ? (
|
||||
<PageEmpty icon={<BarChart3 className="w-10 h-10" />} title={t('monitor.dashboard.custom.emptyNoPanels')} className="py-16" />
|
||||
) : (
|
||||
<div className="grid grid-cols-12 gap-3">
|
||||
{active.panels.map(p => (
|
||||
<Card key={p.id} style={{ gridColumn: `span ${p.width}`, minHeight: `${p.height * 80}px` }}>
|
||||
<CardHeader className="py-2 px-3"><CardTitle className="text-[11px] font-medium">{p.title}</CardTitle></CardHeader>
|
||||
<CardContent className="p-3 pt-0"><GrafanaPanelContent panel={p} datasourceUrl={active.datasourceUrl} /></CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<PageEmpty icon={<Monitor className="w-10 h-10" />} title={t('monitor.dashboard.custom.emptyNoDashboards')} className="py-16" />
|
||||
)}
|
||||
|
||||
<Dialog open={createOpen} onOpenChange={setCreateOpen}><DialogContent><DialogHeader><DialogTitle>{t('monitor.dashboard.custom.createTitle')}</DialogTitle></DialogHeader>
|
||||
<div className="space-y-3"><div><Label>{t('monitor.dashboard.custom.name')}</Label><Input value={nd.name} onChange={e => setNd({ ...nd, name: e.target.value })} /></div>
|
||||
<div><Label>{t('monitor.dashboard.custom.datasource')}</Label><Select value={nd.datasource} onValueChange={v => setNd({ ...nd, datasource: v as any })}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent><SelectItem value="prometheus">Prometheus</SelectItem><SelectItem value="zabbix">Zabbix</SelectItem><SelectItem value="skywalking">SkyWalking</SelectItem><SelectItem value="custom">Custom</SelectItem></SelectContent></Select></div></div>
|
||||
<DialogFooter><Button variant="outline" onClick={() => setCreateOpen(false)}>{t('monitor.dashboard.custom.cancel')}</Button><Button onClick={create}>{t('monitor.dashboard.custom.createBtn')}</Button></DialogFooter></DialogContent></Dialog>
|
||||
|
||||
<Dialog open={addOpen} onOpenChange={setAddOpen}><DialogContent><DialogHeader><DialogTitle>{t('monitor.dashboard.custom.addPanelTitle')}</DialogTitle></DialogHeader>
|
||||
<div className="space-y-3"><div><Label>{t('monitor.dashboard.custom.panelTitle')}</Label><Input value={np.title} onChange={e => setNp({ ...np, title: e.target.value })} /></div>
|
||||
<div><Label>{t('monitor.dashboard.custom.panelType')}</Label><Select value={np.type} onValueChange={v => setNp({ ...np, type: v as any })}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent><SelectItem value="graph">{t('monitor.dashboard.custom.typeGraph')}</SelectItem><SelectItem value="stat">{t('monitor.dashboard.custom.typeStat')}</SelectItem><SelectItem value="gauge">{t('monitor.dashboard.custom.typeGauge')}</SelectItem><SelectItem value="table">{t('monitor.dashboard.custom.typeTable')}</SelectItem></SelectContent></Select></div>
|
||||
<div><Label>{t('monitor.dashboard.custom.panelQuery')}</Label><Input value={np.query} onChange={e => setNp({ ...np, query: e.target.value })} /></div></div>
|
||||
<DialogFooter><Button variant="outline" onClick={() => setAddOpen(false)}>{t('monitor.dashboard.custom.cancel')}</Button><Button onClick={addPanel}>{t('monitor.dashboard.custom.addBtn')}</Button></DialogFooter></DialogContent></Dialog>
|
||||
<div>
|
||||
<DatasourceToolbar sources={sources} activeId={String(ds.id)} onChange={(id) => setDs(sources.find(item => String(item.id) === id) || ds)} onRefresh={fetchSources} refreshing={loading} />
|
||||
<PrometheusDashboard key={String(ds.id)} dsUrl={ds.url} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -638,7 +615,6 @@ const GrafanaDashboard: React.FC = () => {
|
||||
{tab === 'agent' && <AgentTab />}
|
||||
{tab === 'zabbix' && <ZabbixTab />}
|
||||
{tab === 'prometheus' && <PrometheusTab />}
|
||||
{tab === 'dashboard' && <DashboardTab />}
|
||||
</PageShell>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user