diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 138af34..aea4323 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -33,6 +33,7 @@ import TraceManager from '@/components/monitor/trace/TraceManager'; import HostMonitorDashboard from '@/components/monitor/dashboard/HostMonitorDashboard'; import { MonitorCenter } from '@/pages/MonitorCenter'; import { MetricsExplorer } from '@/pages/MetricsExplorer'; +import PrometheusDatasourceManager from '@/components/monitor/datasource/PrometheusDatasourceManager'; const queryClient = new QueryClient({ defaultOptions: { @@ -74,6 +75,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index e39bb29..0d9d6a3 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -91,6 +91,7 @@ const menuItems: MenuItem[] = [ { id: 'metrics', name: '指标监控', icon: Cpu, path: '/metrics' }, { id: 'trace', name: '链路追踪', icon: GitBranch, path: '/trace' }, { id: 'alert-center', name: '告警服务', icon: Bell, path: '/alert-center' }, + { id: 'datasources', name: '数据源', icon: Database, path: '/datasources' }, ], }, { diff --git a/frontend/src/components/monitor/datasource/PrometheusDatasourceManager.tsx b/frontend/src/components/monitor/datasource/PrometheusDatasourceManager.tsx new file mode 100644 index 0000000..af7334e --- /dev/null +++ b/frontend/src/components/monitor/datasource/PrometheusDatasourceManager.tsx @@ -0,0 +1,829 @@ +// Prometheus 数据源管理 — 参考 Grafana 界面设计 // Logan.Li +import { useState, useEffect, useCallback } from 'react'; +import { + Database, Plus, Edit, Trash2, Star, RefreshCw, CheckCircle, XCircle, + Shield, Globe, Clock, Wrench, Settings, Key, Link, X, ChevronDown, ChevronUp, + Plug, AlertTriangle +} 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 { Label } from '@/components/ui/label'; +import { Switch } from '@/components/ui/switch'; +import { Separator } from '@/components/ui/separator'; +import { + Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter +} from '@/components/ui/dialog'; +import { + Select, SelectContent, SelectItem, SelectTrigger, SelectValue +} from '@/components/ui/select'; +import { toast } from 'sonner'; +import { useConfirm } from '@/components/ui/confirm-dialog'; +import { promDatasourceApi } from '@/services/api'; + +interface PromDS { + id: number; + name: string; + url: string; + access: string; + basic_auth: boolean; + basic_auth_user: string; + basic_auth_pass: string; + with_credentials: boolean; + tls_client_auth: boolean; + with_ca_cert: boolean; + ca_cert: string; + skip_tls_verify: boolean; + custom_headers: string; + enable_alerting: boolean; + alertmanager_ds: string; + scrape_interval: number; + query_timeout: number; + http_method: string; + disable_metrics_lookup: boolean; + custom_query_params: string; + is_default: boolean; + status: number; + created_at: string; + updated_at: string; +} + +interface HeaderEntry { + header: string; + value: string; +} + +const defaultForm = { + name: '', + url: '', + access: 'server', + http_method: 'POST', + basic_auth: false, + basic_auth_user: '', + basic_auth_pass: '', + with_credentials: false, + tls_client_auth: false, + with_ca_cert: false, + ca_cert: '', + skip_tls_verify: false, + custom_headers_json: '', + enable_alerting: true, + alertmanager_ds: '', + scrape_interval: 15, + query_timeout: 60, + disable_metrics_lookup: false, + custom_query_params: '', + is_default: false, +}; + +export default function PrometheusDatasourceManager() { + const _confirm = useConfirm(); + const [sources, setSources] = useState([]); + const [loading, setLoading] = useState(true); + const [dialogOpen, setDialogOpen] = useState(false); + const [editingId, setEditingId] = useState(null); + const [form, setForm] = useState({ ...defaultForm }); + + // 自定义 HTTP Headers(动态键值对) + const [headers, setHeaders] = useState([]); + // 折叠面板 + const [showAuth, setShowAuth] = useState(false); + const [showTLS, setShowTLS] = useState(false); + const [showAlerting, setShowAlerting] = useState(false); + const [showMisc, setShowMisc] = useState(false); + + // 连接测试 + const [testing, setTesting] = useState(false); + const [testResult, setTestResult] = useState<{ success: boolean; message: string; target_count?: number } | null>(null); + + const fetchList = useCallback(async () => { + try { + const res: any = await promDatasourceApi.list(); + setSources(res?.data || res || []); + } catch { + toast.error('获取数据源列表失败'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { fetchList(); }, [fetchList]); + + // ==================== 表单操作 ==================== + + const openCreate = () => { + setEditingId(null); + setForm({ ...defaultForm }); + setHeaders([]); + setTestResult(null); + setShowAuth(false); + setShowTLS(false); + setShowAlerting(false); + setShowMisc(false); + setDialogOpen(true); + }; + + const openEdit = (ds: PromDS) => { + setEditingId(ds.id); + setForm({ + name: ds.name, + url: ds.url, + access: ds.access || 'server', + http_method: ds.http_method || 'POST', + basic_auth: ds.basic_auth || false, + basic_auth_user: ds.basic_auth_user || '', + basic_auth_pass: ds.basic_auth_pass || '', + with_credentials: ds.with_credentials || false, + tls_client_auth: ds.tls_client_auth || false, + with_ca_cert: ds.with_ca_cert || false, + ca_cert: ds.ca_cert || '', + skip_tls_verify: ds.skip_tls_verify || false, + custom_headers_json: ds.custom_headers || '', + enable_alerting: ds.enable_alerting !== false, + alertmanager_ds: ds.alertmanager_ds || '', + scrape_interval: ds.scrape_interval || 15, + query_timeout: ds.query_timeout || 60, + disable_metrics_lookup: ds.disable_metrics_lookup || false, + custom_query_params: ds.custom_query_params || '', + is_default: ds.is_default || false, + }); + // 解析 custom_headers JSON + try { + const parsed = JSON.parse(ds.custom_headers || '[]'); + setHeaders(Array.isArray(parsed) ? parsed : []); + } catch { + setHeaders([]); + } + setTestResult(null); + setDialogOpen(true); + }; + + const updateForm = (field: string, value: any) => { + setForm(prev => ({ ...prev, [field]: value })); + }; + + // ==================== HTTP Headers ==================== + + const addHeader = () => { + setHeaders(prev => [...prev, { header: '', value: '' }]); + }; + + const removeHeader = (idx: number) => { + setHeaders(prev => prev.filter((_, i) => i !== idx)); + }; + + const updateHeader = (idx: number, field: 'header' | 'value', val: string) => { + setHeaders(prev => { + const next = [...prev]; + next[idx] = { ...next[idx], [field]: val }; + return next; + }); + }; + + // ==================== CRUD ==================== + + const handleSave = async () => { + if (!form.name.trim()) { toast.error('请输入数据源名称'); return; } + if (!form.url.trim()) { toast.error('请输入 Prometheus URL'); return; } + + const payload: any = { + name: form.name.trim(), + url: form.url.trim(), + access: form.access, + http_method: form.http_method, + basic_auth: form.basic_auth, + basic_auth_user: form.basic_auth_user, + basic_auth_pass: form.basic_auth_pass, + with_credentials: form.with_credentials, + tls_client_auth: form.tls_client_auth, + with_ca_cert: form.with_ca_cert, + ca_cert: form.ca_cert, + skip_tls_verify: form.skip_tls_verify, + custom_headers: JSON.stringify(headers), + enable_alerting: form.enable_alerting, + alertmanager_ds: form.alertmanager_ds, + scrape_interval: form.scrape_interval, + query_timeout: form.query_timeout, + disable_metrics_lookup: form.disable_metrics_lookup, + custom_query_params: form.custom_query_params, + is_default: form.is_default, + }; + + try { + if (editingId) { + await promDatasourceApi.update(editingId, payload); + toast.success('数据源更新成功'); + } else { + await promDatasourceApi.create(payload); + toast.success('数据源创建成功'); + } + setDialogOpen(false); + fetchList(); + } catch (err: any) { + toast.error(err?.message || '操作失败'); + } + }; + + const handleDelete = async (ds: PromDS) => { + const ok = await _confirm({ + title: '删除数据源', + description: `确定删除数据源「${ds.name}」吗?此操作不可撤销。`, + variant: 'destructive', + }); + if (!ok) return; + try { + await promDatasourceApi.delete(ds.id); + toast.success('已删除'); + fetchList(); + } catch { + toast.error('删除失败'); + } + }; + + const handleSetDefault = async (ds: PromDS) => { + try { + await promDatasourceApi.setDefault(ds.id); + toast.success(`已将「${ds.name}」设为默认数据源`); + fetchList(); + } catch { + toast.error('设置失败'); + } + }; + + // ==================== 连接测试 ==================== + + const handleTestConnection = async () => { + if (!form.url.trim()) { toast.error('请先输入 URL'); return; } + setTesting(true); + setTestResult(null); + try { + const res: any = await promDatasourceApi.testConnection({ + url: form.url.trim(), + basic_auth: form.basic_auth, + basic_auth_user: form.basic_auth_user, + basic_auth_pass: form.basic_auth_pass, + skip_tls_verify: form.skip_tls_verify, + }); + const data = res?.data || res; + setTestResult(data); + } catch (err: any) { + setTestResult({ success: false, message: err?.message || '测试失败' }); + } finally { + setTesting(false); + } + }; + + // ==================== 渲染 ==================== + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* 页头 */} +
+
+

数据源管理

+

+ Prometheus 数据源配置,用于指标监控与监控大屏 +

+
+ +
+ + {/* 数据源列表 */} + + + + + 已配置的数据源 + {sources.length} + + + + {sources.length === 0 ? ( +
+ +

暂无数据源

+

点击「添加数据源」开始配置

+
+ ) : ( +
+ + + + + + + + + + + + {sources.map(ds => ( + + + + + + + + ))} + +
名称URL默认状态操作
+
+ Prometheus { (e.target as HTMLImageElement).style.display = 'none'; }} + /> +
+ {ds.name} + {ds.basic_auth && } +
+
+
+ + {ds.url} + + + {ds.is_default ? ( + + 默认 + + ) : ( + -- + )} + + {ds.status === 1 ? ( + + 启用 + + ) : ( + 禁用 + )} + +
+ + {!ds.is_default && ( + + )} + + +
+
+
+ )} +
+
+ + {/* ==================== 添加/编辑 Dialog ==================== */} + + + + + + {editingId ? '编辑数据源' : '添加数据源'} + + + Prometheus 数据源配置 · 参考 Grafana 数据源管理 + + + +
+ {/* === Name & Default === */} +
+
+ + updateForm('name', e.target.value)} + className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" + /> +
+
+
+ updateForm('is_default', v)} + /> + +
+
+
+ + {/* === HTTP 设置 === */} +
+

+ HTTP +

+
+ +
+ + updateForm('url', e.target.value)} + className="flex-1 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] font-mono text-sm" + /> +
+
+
+ Access: {form.access === 'server' ? 'Server (default)' : 'Browser'} +
+
+ + {/* === Auth 折叠 === */} +
+ + {showAuth && ( +
+ {/* Basic Auth */} +
+
+ updateForm('basic_auth', v)} + /> + +
+ {form.basic_auth && ( +
+ updateForm('basic_auth_user', e.target.value)} + className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-sm" + /> + updateForm('basic_auth_pass', e.target.value)} + className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-sm" + /> +
+ )} +
+ + {/* With Credentials */} +
+ updateForm('with_credentials', v)} + /> + +
+ + {/* TLS Client Auth */} +
+
+ updateForm('tls_client_auth', v)} + /> + +
+ {form.tls_client_auth && ( +
+
+ updateForm('with_ca_cert', v)} + /> + +
+ {form.with_ca_cert && ( +