merge: 合并远程 A2 告警引擎分布式安全更新,保留本地日志中心等最新改动
This commit is contained in:
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>
|
||||
|
||||
Reference in new issue
Block a user