merge: 合并远程 A2 告警引擎分布式安全更新,保留本地日志中心等最新改动

This commit is contained in:
Logan committed 2026-06-26 23:19:42 +08:00
commit 23e9e3da62
137 files changed
+947 -611

No files matched your search

@@ -17,16 +17,17 @@ import { AlertEventsTab } from './AlertEventsTab';
import { AlertRulesTab } from './AlertRulesTab';
import { AlertRuleDialog } from './AlertRuleDialog';
import { AlertChannelDialog } from './AlertChannelDialog';
import type { JsonData } from '@/types/common';
export function AlertManager() {
const [activeTab, setActiveTab] = useState('events');
const [alerts, setAlerts] = useState<any[]>([]);
const [rules, setRules] = useState<any[]>([]);
const [alerts, setAlerts] = useState<JsonData[]>([]);
const [rules, setRules] = useState<JsonData[]>([]);
const [loading, setLoading] = useState(true);
const [ruleDialogOpen, setRuleDialogOpen] = useState(false);
const [editingRule, setEditingRule] = useState<any>(null);
const [editingRule, setEditingRule] = useState<JsonData>(null);
const [channelDialogOpen, setChannelDialogOpen] = useState(false);
const [notifyChannels, setNotifyChannels] = useState<any[]>([]);
const [notifyChannels, setNotifyChannels] = useState<JsonData[]>([]);
const loadData = async () => {
try {
@@ -36,9 +37,9 @@ export function AlertManager() {
alertApi.getRules(),
alertApi.getNotifyChannels(),
]);
setAlerts((alertsData as Record<string, any>)?.list || []);
setRules((rulesData as Record<string, any>)?.list || (Array.isArray(rulesData) ? rulesData : []));
setNotifyChannels((channelsData as Record<string, any>)?.list || []);
setAlerts((alertsData as JsonData)?.list || []);
setRules((rulesData as JsonData)?.list || (Array.isArray(rulesData) ? rulesData : []));
setNotifyChannels((channelsData as JsonData)?.list || []);
} catch (error) {
console.error('Failed to load alert data:', error);
} finally {
@@ -55,7 +56,7 @@ export function AlertManager() {
setRuleDialogOpen(true);
};
const openEditRule = ( rule: any) => {
const openEditRule = (rule: JsonData) => {
setEditingRule(rule);
setRuleDialogOpen(true);
};
@@ -15,6 +15,7 @@ import {
import { Loader2 } from 'lucide-react';
import { alertApi } from '@/api';
import { parseChannels, getChannelIcon, getChannelTypeLabel } from './alertShared';
import type { JsonData } from '@/types/common';
const emptyRuleForm = {
name: '',
@@ -37,8 +38,8 @@ export function AlertRuleDialog({
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
editingRule: any;
notifyChannels: any[];
editingRule?: JsonData;
notifyChannels: JsonData[];
onSaved: () => void;
/** 新建时默认规则类型 // Logan.Li */
defaultType?: 'metric' | 'log' | 'event';
@@ -28,6 +28,7 @@ import { ContactDialog } from './ContactDialog';
import { ContactGroupDialog } from './ContactGroupDialog';
import SilenceRuleTab from './SilenceRuleTab';
import type { Contact, ContactGroup } from './alert-center-helpers';
import type { JsonData } from '@/types/common';
const AlertCenter: React.FC = () => {
const _confirm = useConfirm();
@@ -40,7 +41,7 @@ const AlertCenter: React.FC = () => {
const [silenceOpen, setSilenceOpen] = useState(false);
// 规则弹框
const [ruleDialogOpen, setRuleDialogOpen] = useState(false);
const [editingRule, setEditingRule] = useState<any>(null);
const [editingRule, setEditingRule] = useState<JsonData>(null);
// 联系人 & 联系组
const [contacts, setContacts] = useState<Contact[]>([]);
const [contactGroups, setContactGroups] = useState<ContactGroup[]>([]);
@@ -81,14 +82,14 @@ const AlertCenter: React.FC = () => {
const fetchContacts = async () => {
try {
const res: Record<string, any> = await alertApi.getContacts();
const res: JsonData = await alertApi.getContacts();
setContacts((res?.data?.list || res?.list || []) as Contact[]);
} catch { /* silent */ }
};
const fetchContactGroups = async () => {
try {
const res: Record<string, any> = await alertApi.getContactGroups();
const res: JsonData = await alertApi.getContactGroups();
setContactGroups((res?.data?.list || res?.list || []) as ContactGroup[]);
} catch { /* silent */ }
};
@@ -11,6 +11,7 @@ import {
type NotificationChannel, getChannelIcon, getChannelTypeLabel,
} from './alert-center-helpers';
import { AlertChannelDialog } from './AlertChannelDialog';
import type { JsonData } from '@/types/common';
export const AlertChannelsTab: React.FC<{ refreshKey: number }> = ({ refreshKey }) => {
const _confirm = useConfirm();
@@ -24,7 +25,7 @@ export const AlertChannelsTab: React.FC<{ refreshKey: number }> = ({ refreshKey
setLoading(true);
try {
const res = await alertApi.getNotifyChannels();
const list = (res as Record<string, any>)?.list || [];
const list = (res as JsonData)?.list || [];
setChannels(list);
} catch {
toast.error('获取通知渠道失败');
@@ -60,7 +61,7 @@ export const AlertChannelsTab: React.FC<{ refreshKey: number }> = ({ refreshKey
const handleTest = async (id: number) => {
try {
const res = await alertApi.testNotifyChannel(id);
const status = (res as Record<string, any>)?.status;
const status = (res as JsonData)?.status;
if (status === 'success') {
toast.success('测试通知已发送');
} else {
@@ -11,6 +11,7 @@ import {
} from '@/components/ui/select';
import { toast } from 'sonner';
import { alertCenterApi } from '@/api';
import type { JsonData } from '@/types/common';
// 静默类型中文标签
const SILENCE_TYPE_LABELS: Record<string, string> = {
@@ -49,7 +50,7 @@ export function SilenceDialog({
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
editingSilence: any;
editingSilence?: JsonData;
onSaved: () => void;
}) {
const [saving, setSaving] = useState(false);
@@ -9,6 +9,7 @@ import { toast } from 'sonner';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { alertCenterApi } from '@/api';
import { SilenceDialog } from './SilenceDialog';
import type { JsonData } from '@/types/common';
const SILENCE_TYPE_LABELS: Record<string, string> = {
onetime: '一次性',
@@ -30,15 +31,15 @@ function formatTime(t: string | Date): string {
const SilenceRuleTab: React.FC = () => {
const confirm = useConfirm();
const [silences, setSilences] = useState<any[]>([]);
const [silences, setSilences] = useState<JsonData[]>([]);
const [loading, setLoading] = useState(false);
const [dialogOpen, setDialogOpen] = useState(false);
const [editing, setEditing] = useState<any>(null);
const [editing, setEditing] = useState<JsonData>(null);
const fetchSilences = useCallback(async () => {
setLoading(true);
try {
const res: Record<string, any> = await alertCenterApi.getSilences();
const res: JsonData = await alertCenterApi.getSilences();
const list = res?.list || res?.data?.list || [];
setSilences(list);
} catch {
@@ -57,12 +58,12 @@ const SilenceRuleTab: React.FC = () => {
setDialogOpen(true);
};
const handleEdit = (silence: any) => {
const handleEdit = (silence: JsonData) => {
setEditing(silence);
setDialogOpen(true);
};
const handleDelete = async (silence: any) => {
const handleDelete = async (silence: JsonData) => {
const ok = await confirm({
title: '删除静默规则',
description: `确定删除「${silence.name}」?删除后该规则匹配的告警将恢复通知。`,
@@ -79,7 +80,7 @@ const SilenceRuleTab: React.FC = () => {
}
};
const handleToggle = async (silence: any) => {
const handleToggle = async (silence: JsonData) => {
try {
await alertCenterApi.toggleSilence(silence.id);
toast.success(silence.status === 1 ? '已禁用' : '已启用');
@@ -89,7 +90,7 @@ const SilenceRuleTab: React.FC = () => {
}
};
const isCurrentlyActive = (s: any): boolean => {
const isCurrentlyActive = (s: JsonData): boolean => {
if (s.is_active !== undefined) return s.is_active;
if (s.status !== 1) return false;
const now = new Date();
@@ -5,6 +5,7 @@ import {
Mail, MessageSquare, Phone, Bell,
} from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import type { JsonData } from '@/types/common';
/** 前端 Alert 接口 */
export interface Alert {
@@ -76,7 +77,7 @@ export interface ContactGroup {
}
// Logan.Li — 后端字段映射到前端 Alert 接口
export function mapAlert( a: any): Alert {
export function mapAlert(a: JsonData): Alert {
return {
id: String(a.id),
title: a.rule_name || a.title || `告警 #${a.id}`,
@@ -93,7 +93,7 @@ const AgentTab: React.FC = () => {
finally { setBatchUninstalling(false); }
};
const totalPages = Math.max(1, Math.ceil(hosts.length / pageSize));
const fetch = async () => { try { setHosts(asList(await agentApi.getLatest())); } catch {} finally { setLoading(false); } };
const fetch = async () => { try { setHosts(asList(await agentApi.getLatest()) as typeof hosts); } catch {} finally { setLoading(false); } };
useEffect(() => { fetch(); const t = setInterval(fetch, 30000); return () => clearInterval(t); }, []);
const toggle = (f: SortField) => { if (sf === f) setSd(d => d === 'asc' ? 'desc' : 'asc'); else { setSf(f); setSd('desc'); } };
const sorted = [...hosts].sort((a, b) => {
@@ -419,7 +419,7 @@ const PrometheusTab: React.FC = () => {
// ═══════════ 自定义仪表盘 ═══════════
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; }
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<Dashboard[]>([]);
@@ -29,6 +29,7 @@ import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue
} from '@/components/ui/select';
import { serverApi } from '@/api';
import type { JsonData } from '@/types/common';
type SortField = 'hostname' | 'ip' | 'agent_version' | 'agent_status' | 'cpu_usage' | 'memory_usage' | 'disk_usage' | 'region';
type SortDir = 'asc' | 'desc';
@@ -48,7 +49,7 @@ function getUsageBar(val: number) {
export default function HostMonitorDashboard() {
const navigate = useNavigate();
const [hosts, setHosts] = useState<any[]>([]);
const [hosts, setHosts] = useState<JsonData[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
@@ -67,16 +68,16 @@ export default function HostMonitorDashboard() {
if (isRefresh) setRefreshing(true);
else setLoading(true);
try {
const params: Record<string, any> = { page, page_size: pageSize };
const params: JsonData = { page, page_size: pageSize };
if (search.trim()) params.keyword = search.trim();
const res = await serverApi.getServers(params);
const list = (res as Record<string, any>)?.list || [];
const list = (res as JsonData)?.list || [];
setHosts(list);
setTotal((res as Record<string, any>)?.total || 0);
setTotal((res as JsonData)?.total || 0);
// 统计
const online = list.filter(( h: any) => h.agent_status === 'online').length;
setStats({ online, offline: list.length - online, total: (res as Record<string, any>)?.total || 0 });
const online = list.filter((h: JsonData) => h.agent_status === 'online').length;
setStats({ online, offline: list.length - online, total: (res as JsonData)?.total || 0 });
} catch {
setHosts([]);
setTotal(0);
@@ -89,8 +90,8 @@ export default function HostMonitorDashboard() {
useEffect(() => { loadData(); }, [loadData]);
// 排序后的数据
const sorted = [...hosts].sort(( a: any, b: any) => {
let va: Record<string, any>, vb: any;
const sorted = [...hosts].sort((a: JsonData, b: JsonData) => {
let va: JsonData | undefined, vb: JsonData | undefined;
if (sortField === 'region') {
va = (a.region?.name || a.region?.Name || '').toLowerCase();
vb = (b.region?.name || b.region?.Name || '').toLowerCase();
@@ -178,9 +179,9 @@ export default function HostMonitorDashboard() {
</CardContent>
</Card>
{(() => {
const onlines = hosts.filter(( h: any) => h.agent_status === 'online');
const avgCpu = onlines.length > 0 ? onlines.reduce((s: number, h: any) => s + (h.cpu_usage || 0), 0) / onlines.length : 0;
const avgMem = onlines.length > 0 ? onlines.reduce((s: number, h: any) => s + (h.memory_usage || 0), 0) / onlines.length : 0;
const onlines = hosts.filter((h: JsonData) => h.agent_status === 'online');
const avgCpu = onlines.length > 0 ? onlines.reduce((s: number, h: JsonData) => s + (h.cpu_usage || 0), 0) / onlines.length : 0;
const avgMem = onlines.length > 0 ? onlines.reduce((s: number, h: JsonData) => s + (h.memory_usage || 0), 0) / onlines.length : 0;
const cpuColor = avgCpu > 85 ? 'text-red-400' : avgCpu > 60 ? 'text-amber-400' : 'text-emerald-400';
const memColor = avgMem > 85 ? 'text-red-400' : avgMem > 60 ? 'text-amber-400' : 'text-emerald-400';
return (<>
@@ -207,7 +208,7 @@ export default function HostMonitorDashboard() {
</div>
{/* 资源条状图 — Grafana Bargauge 风格 (参照 K8s Node CPU/Memory Ratio) */}
{hosts.filter(( h: any) => h.agent_status === 'online').length > 0 && (
{hosts.filter((h: JsonData) => h.agent_status === 'online').length > 0 && (
<div className="grid grid-cols-2 gap-3">
{/* CPU 排行 */}
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
@@ -220,10 +221,10 @@ export default function HostMonitorDashboard() {
</CardHeader>
<CardContent className="space-y-2">
{[...hosts]
.filter(( h: any) => h.agent_status === 'online')
.sort(( a: any, b: any) => (b.cpu_usage || 0) - (a.cpu_usage || 0))
.filter((h: JsonData) => h.agent_status === 'online')
.sort((a: JsonData, b: JsonData) => (b.cpu_usage || 0) - (a.cpu_usage || 0))
.slice(0, 5)
.map(( h: any) => {
.map((h: JsonData) => {
const val = h.cpu_usage || 0;
return (
<div key={h.id} className="flex items-center gap-2">
@@ -261,10 +262,10 @@ export default function HostMonitorDashboard() {
</CardHeader>
<CardContent className="space-y-2">
{[...hosts]
.filter(( h: any) => h.agent_status === 'online')
.sort(( a: any, b: any) => (b.memory_usage || 0) - (a.memory_usage || 0))
.filter((h: JsonData) => h.agent_status === 'online')
.sort((a: JsonData, b: JsonData) => (b.memory_usage || 0) - (a.memory_usage || 0))
.slice(0, 5)
.map(( h: any) => {
.map((h: JsonData) => {
const val = h.memory_usage || 0;
return (
<div key={h.id} className="flex items-center gap-2">
@@ -363,7 +364,7 @@ export default function HostMonitorDashboard() {
</tr>
</thead>
<tbody>
{sorted.map(( h: any) => (
{sorted.map((h: JsonData) => (
<tr key={h.id} className="border-b border-[var(--dg-border)]/50 hover:bg-[var(--dg-bg-tertiary)]/50 transition-colors">
{/* 主机名/ID */}
<td className="px-3 py-2">
@@ -7,6 +7,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { StatStrip } from '@/components/ui/stat-strip';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { prometheusProxyApi } from '@/api';
import type { JsonData } from '@/types/common';
interface Props { dsUrl?: string; }
@@ -36,17 +37,17 @@ const fmtBps = (b: number) => {
return `${b.toFixed(1)} B/s`;
};
function parsePrune( result: any[]): any[] {
return (result || []).map(( r: any) => {
const m: Record<string, any> = { ...(r.metric || {}) };
function parsePrune( result: JsonData[]): JsonData[] {
return (result || []).map((r: JsonData) => {
const m: JsonData = { ...(r.metric || {}) };
if (r.value) m.value = r.value[1];
return m;
});
}
const Box = ( p: any) => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>;
const Box = (p: JsonData) => <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>;
const K8sPanel: React.FC<{ d: Record<string, any>}> = ({ d }) => (
const K8sPanel: React.FC<{ d: JsonData}> = ({ d }) => (
<div className="space-y-4">
<StatStrip
items={[
@@ -58,27 +59,27 @@ const K8sPanel: React.FC<{ d: Record<string, any>}> = ({ d }) => (
/>
<div className="rounded-lg bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] p-3 max-h-60 overflow-y-auto">
<div className="text-xs font-medium mb-2 flex items-center gap-1"><Cpu className="w-3 h-3 text-purple-400" />节点 CPU / 内存利用率</div>
{(d.nodeCPU || []).map(( n: any, i: number) => {
const mem = (d.nodeMem || []).find(( m: any) => m.node === n.node);
{(d.nodeCPU || []).map((n: JsonData, i: number) => {
const mem = (d.nodeMem || []).find((m: JsonData) => m.node === n.node);
return <div key={i} className="flex items-center gap-3 text-[11px] mb-1.5"><span className="w-28 truncate" title={n.node}>{n.node}</span><MiniBar val={Number(n.value||0)} label={`${Number(n.value||0).toFixed(1)}%`} /><MiniBar val={Number(mem?.value||0)} label={`${Number(mem?.value||0).toFixed(1)}%`} color="bg-blue-500" /></div>;
})}
</div>
</div>
);
const NodePanel: React.FC<{ d: Record<string, any>}> = ({ d }) => (
const NodePanel: React.FC<{ d: JsonData}> = ({ d }) => (
<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)]"><tr><th className="px-3 py-2 text-left">节点</th><th className="px-3 py-2 text-left">状态</th><th className="px-3 py-2 text-left">CPU</th><th className="px-3 py-2 text-left">内存</th><th className="px-3 py-2 text-left">磁盘</th><th className="px-3 py-2 text-left">流入</th><th className="px-3 py-2 text-left">流出</th></tr></thead>
<tbody>
{(d.nodes || []).map(( n: any, i: number) => {
{(d.nodes || []).map((n: JsonData, i: number) => {
const inst = n.instance || n.exported_instance || '';
const cpu = (d.cpuUsage||[]).find(( x: any)=>x.instance===inst);
const mem = (d.memUsage||[]).find(( x: any)=>x.instance===inst);
const disk = (d.diskUsage||[]).find(( x: any)=>x.instance===inst);
const up = (d.up||[]).find(( x: any)=>x.instance===inst);
const rx = (d.netRx||[]).find(( x: any)=>x.instance===inst);
const tx = (d.netTx||[]).find(( x: any)=>x.instance===inst);
const cpu = (d.cpuUsage||[]).find((x: JsonData)=>x.instance===inst);
const mem = (d.memUsage||[]).find((x: JsonData)=>x.instance===inst);
const disk = (d.diskUsage||[]).find((x: JsonData)=>x.instance===inst);
const up = (d.up||[]).find((x: JsonData)=>x.instance===inst);
const rx = (d.netRx||[]).find((x: JsonData)=>x.instance===inst);
const tx = (d.netTx||[]).find((x: JsonData)=>x.instance===inst);
return <tr key={i} className="border-b border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/50">
<td className="px-3 py-2 font-medium">{n.nodename||inst}</td>
<td className="px-3 py-2"><Badge className={`text-[10px] ${up?.value===1?'bg-emerald-500/20 text-emerald-400':'bg-red-500/20 text-red-400'}`}>{up?.value===1?'UP':'DOWN'}</Badge></td>
@@ -94,7 +95,7 @@ const NodePanel: React.FC<{ d: Record<string, any>}> = ({ d }) => (
</div>
);
const PodPanel: React.FC<{ d: Record<string, any>}> = ({ d }) => (
const PodPanel: React.FC<{ d: JsonData}> = ({ d }) => (
<div className="space-y-4">
<StatStrip
items={[
@@ -102,12 +103,12 @@ const PodPanel: React.FC<{ d: Record<string, any>}> = ({ d }) => (
{ label: 'Pending', value: d.pending?.[0]?.value || 0, color: 'amber' },
]}
/>
<div className="rounded-lg bg-[var(--dg-bg-primary)] p-3 max-h-40 overflow-y-auto"><div className="text-xs font-medium mb-2">CPU Top 10</div>{(d.cpuTop||[]).map(( p: any,i:number)=><div key={i} className="flex items-center gap-2 text-[11px] mb-1"><span className="w-10 text-right text-[var(--dg-text-muted)]">{i+1}</span><span className="w-32 truncate" title={p.pod}>{p.pod}</span><MiniBar val={Number(p.value||0)*100} label={`${Number(p.value||0).toFixed(2)}c`} color="bg-purple-500" /></div>)}</div>
<div className="rounded-lg bg-[var(--dg-bg-primary)] p-3 max-h-40 overflow-y-auto"><div className="text-xs font-medium mb-2">内存 Top 10</div>{(d.memTop||[]).map(( p: any,i:number)=><div key={i} className="flex items-center gap-2 text-[11px] mb-1"><span className="w-10 text-right text-[var(--dg-text-muted)]">{i+1}</span><span className="w-32 truncate" title={p.pod}>{p.pod}</span><span className="ml-auto">{fmtBytes(Number(p.value||0))}</span></div>)}</div>
<div className="rounded-lg bg-[var(--dg-bg-primary)] p-3 max-h-40 overflow-y-auto"><div className="text-xs font-medium mb-2">CPU Top 10</div>{(d.cpuTop||[]).map((p: JsonData, i:number)=><div key={i} className="flex items-center gap-2 text-[11px] mb-1"><span className="w-10 text-right text-[var(--dg-text-muted)]">{i+1}</span><span className="w-32 truncate" title={p.pod}>{p.pod}</span><MiniBar val={Number(p.value||0)*100} label={`${Number(p.value||0).toFixed(2)}c`} color="bg-purple-500" /></div>)}</div>
<div className="rounded-lg bg-[var(--dg-bg-primary)] p-3 max-h-40 overflow-y-auto"><div className="text-xs font-medium mb-2">内存 Top 10</div>{(d.memTop||[]).map((p: JsonData, i:number)=><div key={i} className="flex items-center gap-2 text-[11px] mb-1"><span className="w-10 text-right text-[var(--dg-text-muted)]">{i+1}</span><span className="w-32 truncate" title={p.pod}>{p.pod}</span><span className="ml-auto">{fmtBytes(Number(p.value||0))}</span></div>)}</div>
</div>
);
const JvmPanel: React.FC<{ d: Record<string, any>}> = ({ d }) => {
const JvmPanel: React.FC<{ d: JsonData}> = ({ d }) => {
const h = Number(d.heapUsed?.[0]?.value||0);
const uptime = Number(d.uptime?.[0]?.value||0);
return (
@@ -127,10 +128,10 @@ export const PrometheusDashboard: React.FC<Props> = ({ dsUrl }) => {
const [targets, setTargets] = useState(0);
const [error, setError] = useState('');
const [tab, setTab] = useState('kubernetes');
const [k8sD, setK8sD] = useState<any>({});
const [nodeD, setNodeD] = useState<any>({});
const [podD, setPodD] = useState<any>({});
const [jvmD, setJvmD] = useState<any>({});
const [k8sD, setK8sD] = useState<JsonData>({});
const [nodeD, setNodeD] = useState<JsonData>({});
const [podD, setPodD] = useState<JsonData>({});
const [jvmD, setJvmD] = useState<JsonData>({});
useEffect(() => {
if (dsUrl) doConnect(dsUrl);
@@ -139,20 +140,20 @@ export const PrometheusDashboard: React.FC<Props> = ({ dsUrl }) => {
const doConnect = async (u: string) => {
setLoading(true); setError('');
try {
const data: Record<string, any> = await prometheusProxyApi.connect(u);
const data: JsonData = await prometheusProxyApi.connect(u);
setConnected(true); setTargets(data.targets||0);
loadAll(u);
} catch (e) { setConnected(false); setError((e as Record<string, any>).message||'连接失败'); }
} catch (e) { setConnected(false); setError((e as JsonData).message||'连接失败'); }
finally { setLoading(false); }
};
const loadAll = async (u: string) => {
for (const t of ['kubernetes','node','pod','jvm']) {
try {
const data: Record<string, any> = await prometheusProxyApi.dashboard(u, t);
const items: Record<string, any> = {};
const data: JsonData = await prometheusProxyApi.dashboard(u, t);
const items: JsonData = {};
for (const [k, v] of Object.entries(data||{}))
items[k] = Array.isArray(v) ? v : ((v as Record<string, any>)?.data?.result ? parsePrune((v as Record<string, any>).data.result) : v);
items[k] = Array.isArray(v) ? v : ((v as JsonData)?.data?.result ? parsePrune((v as JsonData).data.result) : v);
if (t==='kubernetes') setK8sD(items); if (t==='node') setNodeD(items);
if (t==='pod') setPodD(items); if (t==='jvm') setJvmD(items);
} catch {}
@@ -17,6 +17,7 @@ import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { zabbixProxyApi } from '@/api';
import type { JsonData } from '@/types/common';
interface ZabbixHost {
hostid: string; hostname: string; name: string; ip: string; available: string;
@@ -45,11 +46,11 @@ export const ZabbixDashboard: React.FC<Props> = ({ dsUrl, dsUsername, dsPassword
const doConnect = async (u: string, un: string, pw: string) => {
setLoading(true); setError('');
try {
const data: Record<string, any> = await zabbixProxyApi.connect(u, un, pw);
const data: JsonData = await zabbixProxyApi.connect(u, un, pw);
setHosts(data.hosts || []);
setVersion(data.version || '');
setConnected(true);
} catch (e) { setConnected(false); setError((e as Record<string, any>).message || '连接失败'); }
} catch (e) { setConnected(false); setError((e as JsonData).message || '连接失败'); }
finally { setLoading(false); }
};
@@ -59,7 +60,7 @@ export const ZabbixDashboard: React.FC<Props> = ({ dsUrl, dsUsername, dsPassword
else { setSortField(f); setSortDir('desc'); }
};
const sorted = [...hosts].sort((a, b) => {
const va = (a as Record<string, any>)[sortField] ?? '', vb = (b as Record<string, any>)[sortField] ?? '';
const va = (a as JsonData)[sortField] ?? '', vb = (b as JsonData)[sortField] ?? '';
if (typeof va === 'number') return sortDir === 'asc' ? va - vb : vb - va;
return sortDir === 'asc' ? String(va).localeCompare(String(vb)) : String(vb).localeCompare(String(va));
});
@@ -31,6 +31,7 @@ import {
import { toast } from 'sonner';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { zabbixDatasourceApi } from '@/api';
import type { JsonData } from '@/types/common';
interface ZabbixDS {
id: number;
@@ -74,7 +75,7 @@ export default function ZabbixDatasourceManager() {
const fetchList = useCallback(async () => {
try {
const res: Record<string, any> = await zabbixDatasourceApi.list();
const res: JsonData = await zabbixDatasourceApi.list();
setSources(res?.data || res || []);
} catch {
toast.error('获取数据源列表失败');
@@ -109,7 +110,7 @@ export default function ZabbixDatasourceManager() {
setDialogOpen(true);
};
const updateForm = (field: string, value: any) => {
const updateForm = (field: string, value: string | number | boolean) => {
setForm(prev => ({ ...prev, [field]: value }));
};
@@ -164,7 +165,7 @@ export default function ZabbixDatasourceManager() {
setTesting(true);
setTestResult(null);
try {
const res: Record<string, any> = await zabbixDatasourceApi.testConnection({
const res: JsonData = await zabbixDatasourceApi.testConnection({
url: form.url.trim(),
username: form.username.trim(),
password: form.password || '',
@@ -37,6 +37,7 @@ import { Separator } from '@/components/ui/separator';
import { ScrollArea } from '@/components/ui/scroll-area';
import { toast } from 'sonner';
import { traceApi } from '@/api';
import type { JsonData } from '@/types/common';
interface Trace {
id: string;
@@ -502,7 +503,7 @@ const TraceManager: React.FC = () => {
<span className="w-20 text-right">耗时</span>
</div>
{/* Span树渲染 */}
{(selectedTrace as Record<string, any>).spans && buildSpanTree((selectedTrace as Record<string, any>).spans).map(span => (
{(selectedTrace as JsonData).spans && buildSpanTree((selectedTrace as JsonData).spans).map(span => (
<SpanTreeNode key={span.spanId} span={span} level={0} totalDuration={selectedTrace.duration} />
))}
</div>