/* eslint-disable @typescript-eslint/no-explicit-any */ // 面板内容渲染 — 执行 PromQL 查询并渲染真实数据 // Logan.Li import { useState, useEffect, useCallback } from 'react'; import { TrendingUp, Loader2, AlertCircle } from 'lucide-react'; import { LineChart, Line, ResponsiveContainer, XAxis, YAxis, Tooltip, CartesianGrid } from 'recharts'; import type { Panel } from './GrafanaDashboard'; import { prometheusProxyApi, promDatasourceApi } from '@/api'; // 默认 Prometheus URL 缓存 let _defaultPromURL: string | null = null; async function getDefaultPromURL(dashboardURL?: string): Promise { // 1. 优先使用仪表盘指定的 URL if (dashboardURL) return dashboardURL; // 2. 缓存命中 if (_defaultPromURL) return _defaultPromURL; // 3. 查询默认 Prometheus 数据源 try { const res = await promDatasourceApi.list(); const list = (res as any[]) ?? []; const def = list.find((d: any) => d.is_default) || list[0]; if (def?.url) { _defaultPromURL = def.url; return def.url; } } catch { // ignore } return ''; } // 解析 Prometheus range query 结果 function parseRangeResult(promResult: any): { time: string; value: number }[] { try { const result = promResult?.data?.result; if (!Array.isArray(result) || result.length === 0) return []; // 取第一条时间序列(多序列时合并取均值) const series = result[0]; const values = series?.values; if (!Array.isArray(values)) return []; return values.map((v: any) => { const ts = v[0] as number; const val = parseFloat(v[1] as string) || 0; const d = new Date(ts * 1000); return { time: `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`, value: Math.round(val * 100) / 100, }; }); } catch { return []; } } // 解析 Prometheus instant query 结果 function parseInstantResult(promResult: any): { labels: Record; value: number }[] { try { const result = promResult?.data?.result; if (!Array.isArray(result)) return []; return result.map((r: any) => ({ labels: r.metric ?? {}, value: parseFloat(Array.isArray(r.value) ? r.value[1] : '0') || 0, })); } catch { return []; } } /** 渲染面板内容 */ export function GrafanaPanelContent({ panel, datasourceUrl }: { panel: Panel; datasourceUrl?: string }) { const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [chartData, setChartData] = useState<{ time: string; value: number }[]>([]); const [instantData, setInstantData] = useState<{ labels: Record; value: number }[]>([]); const fetchData = useCallback(async () => { if (!panel.query) return; setLoading(true); setError(''); try { const url = await getDefaultPromURL(datasourceUrl); if (!url) { setError('未配置 Prometheus 数据源'); setLoading(false); return; } if (panel.type === 'graph') { // 范围查询:最近 1 小时,1 分钟步长 const now = Math.floor(Date.now() / 1000); const start = now - 3600; const res = await prometheusProxyApi.queryRange({ url, query: panel.query, start: String(start), end: String(now), step: '1m', }); const parsed = parseRangeResult(res); setChartData(parsed.length > 0 ? parsed : []); if (parsed.length === 0) setError('无数据'); } else { // stat / gauge / table 用即时查询 const res = await prometheusProxyApi.query(url, panel.query); const parsed = parseInstantResult(res); setInstantData(parsed); if (parsed.length === 0) setError('无数据'); } } catch (e: any) { setError(e?.message ? String(e.message).slice(0, 80) : '查询失败'); } finally { setLoading(false); } }, [panel.query, panel.type, datasourceUrl]); useEffect(() => { fetchData(); // 自动刷新(每 30s) const id = setInterval(fetchData, 30000); return () => clearInterval(id); }, [fetchData]); // Loading 状态 if (loading && chartData.length === 0 && instantData.length === 0) { return (
); } // Error 状态 if (error && chartData.length === 0 && instantData.length === 0) { return (
{error}
); } switch (panel.type) { case 'stat': { const val = instantData[0]?.value ?? 0; const unit = (panel.options as any)?.unit ?? ''; return (
{val > 1000 ? (val / 1000).toFixed(1) + 'K' : val.toFixed(2)} {unit && {unit}}
实时
); } case 'gauge': { const val = instantData[0]?.value ?? 0; const pct = Math.min(val, 100); return (
80 ? '#ef4444' : pct > 60 ? '#f59e0b' : '#3b82f6'} strokeWidth="10" strokeDasharray={`${(pct / 100) * 251} 251`} transform="rotate(-90 50 50)" style={{ transition: 'stroke-dasharray 0.5s ease' }} />
{pct.toFixed(1)}%
{panel.title}
); } case 'table': { if (instantData.length === 0) return
无数据
; return (
{Object.keys(instantData[0]?.labels ?? {}).slice(0, 3).map((k) => ( ))} {instantData.slice(0, 20).map((row, i) => ( {Object.entries(row.labels).slice(0, 3).map(([k, v]) => ( ))} ))}
{k}value
{v}{row.value.toFixed(2)}
); } case 'graph': default: { if (chartData.length === 0) { return
无数据
; } return (
); } } }