431 lines
25 KiB
TypeScript
431 lines
25 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
import type { EChartsOption } from 'echarts';
|
|
import {
|
|
Activity,
|
|
AlertTriangle,
|
|
CheckCircle2,
|
|
Clock3,
|
|
Cpu,
|
|
Database,
|
|
HardDrive,
|
|
MemoryStick,
|
|
Network,
|
|
RefreshCw,
|
|
Server,
|
|
Settings2,
|
|
ShieldAlert,
|
|
} from 'lucide-react';
|
|
import {
|
|
adminApi,
|
|
type InfrastructureAlert,
|
|
type InfrastructureAlertSettings,
|
|
type InfrastructureAlertThresholds,
|
|
type InfrastructureMetricPoint,
|
|
type InfrastructureMonitoringOverview,
|
|
type InfrastructureMonitoringRange,
|
|
} from '@/api/adminApi';
|
|
import { Breadcrumb, Button, Input, Modal, Table, Tag, type TableColumn } from '@/components/ui';
|
|
import { Chart } from '@/components/ui/Chart';
|
|
import './AdminSystemMonitoringPage.css';
|
|
|
|
const RANGE_OPTIONS: Array<{ value: InfrastructureMonitoringRange; label: string }> = [
|
|
{ value: '1h', label: '近1小时' },
|
|
{ value: '24h', label: '近24小时' },
|
|
{ value: '7d', label: '近7天' },
|
|
];
|
|
|
|
const STATUS_COPY = {
|
|
healthy: { label: '运行正常', tone: 'success' as const },
|
|
warning: { label: '需要关注', tone: 'warning' as const },
|
|
critical: { label: '严重告警', tone: 'danger' as const },
|
|
unknown: { label: '状态未知', tone: 'neutral' as const },
|
|
};
|
|
|
|
function formatPercent(value: number | null) {
|
|
return value === null ? '—' : `${value.toFixed(1)}%`;
|
|
}
|
|
|
|
function formatBytes(value: number | null) {
|
|
if (value === null) return '—';
|
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
let amount = value;
|
|
let index = 0;
|
|
while (amount >= 1024 && index < units.length - 1) {
|
|
amount /= 1024;
|
|
index += 1;
|
|
}
|
|
return `${amount.toFixed(index >= 3 ? 1 : 0)} ${units[index]}`;
|
|
}
|
|
|
|
function formatRate(value: number | null) {
|
|
return value === null ? '—' : `${formatBytes(value)}/s`;
|
|
}
|
|
|
|
function totalNetworkRate(receive: number | null | undefined, transmit: number | null | undefined) {
|
|
if (receive === null || receive === undefined || transmit === null || transmit === undefined) return null;
|
|
return receive + transmit;
|
|
}
|
|
|
|
function formatUptime(value: number | null) {
|
|
if (value === null) return '—';
|
|
const days = Math.floor(value / 86400);
|
|
const hours = Math.floor((value % 86400) / 3600);
|
|
return days > 0 ? `${days}天 ${hours}小时` : `${hours}小时`;
|
|
}
|
|
|
|
function formatServiceMetric(value: number | null, unit: 'percent' | 'seconds' | 'count' | 'per_second' | 'bytes') {
|
|
if (value === null) return '—';
|
|
if (unit === 'percent') return `${value.toFixed(2)}%`;
|
|
if (unit === 'seconds') return value < 1 ? `${Math.round(value * 1000)} ms` : `${value.toFixed(1)} s`;
|
|
if (unit === 'per_second') return `${value.toFixed(value < 10 ? 2 : 1)}/s`;
|
|
if (unit === 'bytes') return formatBytes(value);
|
|
return Math.round(value).toLocaleString('zh-CN');
|
|
}
|
|
|
|
function formatTime(value: string | null) {
|
|
if (!value) return '暂无采样';
|
|
return new Intl.DateTimeFormat('zh-CN', {
|
|
month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false,
|
|
}).format(new Date(value));
|
|
}
|
|
|
|
function formatDuration(startedAt: string) {
|
|
const milliseconds = Date.now() - Date.parse(startedAt);
|
|
if (!Number.isFinite(milliseconds) || milliseconds < 0) return '—';
|
|
const minutes = Math.floor(milliseconds / 60_000);
|
|
if (minutes < 60) return `${Math.max(minutes, 1)}分钟`;
|
|
const hours = Math.floor(minutes / 60);
|
|
return hours < 24 ? `${hours}小时 ${minutes % 60}分钟` : `${Math.floor(hours / 24)}天 ${hours % 24}小时`;
|
|
}
|
|
|
|
function timeLabels(points: InfrastructureMetricPoint[], range: InfrastructureMonitoringRange) {
|
|
return points.map((point) => new Intl.DateTimeFormat('zh-CN', range === '7d'
|
|
? { month: '2-digit', day: '2-digit', hour: '2-digit', hour12: false }
|
|
: { hour: '2-digit', minute: '2-digit', hour12: false })
|
|
.format(new Date(point.timestamp)));
|
|
}
|
|
|
|
function makeTrendOption(params: {
|
|
range: InfrastructureMonitoringRange;
|
|
series: Array<{ name: string; points: InfrastructureMetricPoint[]; color: string }>;
|
|
suffix: string;
|
|
maximum?: number;
|
|
}): EChartsOption {
|
|
const timestamps = [...new Set(params.series.flatMap((series) => series.points.map((point) => point.timestamp)))].sort();
|
|
return {
|
|
animationDuration: 280,
|
|
color: params.series.map((item) => item.color),
|
|
grid: { left: 8, right: 16, top: 34, bottom: 4, containLabel: true },
|
|
legend: params.series.length > 1 ? { type: 'scroll', top: 0, left: 0, right: 0, textStyle: { color: '#6b7280', fontSize: 12 } } : undefined,
|
|
tooltip: {
|
|
trigger: 'axis',
|
|
valueFormatter: (value) => `${Number(value).toFixed(1)}${params.suffix}`,
|
|
},
|
|
xAxis: {
|
|
type: 'category',
|
|
boundaryGap: false,
|
|
data: timeLabels(timestamps.map((timestamp) => ({ timestamp, value: 0 })), params.range),
|
|
axisLine: { lineStyle: { color: '#e5e7eb' } },
|
|
axisTick: { show: false },
|
|
axisLabel: { color: '#9ca3af', hideOverlap: true, margin: 12 },
|
|
},
|
|
yAxis: {
|
|
type: 'value', min: 0, max: params.maximum,
|
|
axisLabel: { color: '#9ca3af', formatter: `{value}${params.suffix}` },
|
|
splitLine: { lineStyle: { color: '#eef0f3' } },
|
|
},
|
|
series: params.series.map((item) => {
|
|
const values = new Map(item.points.map((point) => [point.timestamp, point.value]));
|
|
return {
|
|
name: item.name,
|
|
data: timestamps.map((timestamp) => values.get(timestamp) ?? null),
|
|
type: 'line',
|
|
smooth: true,
|
|
showSymbol: false,
|
|
lineStyle: { width: 2.5 },
|
|
areaStyle: { opacity: 0.07 },
|
|
};
|
|
}),
|
|
};
|
|
}
|
|
|
|
function severityTag(severity: InfrastructureAlert['severity']) {
|
|
if (severity === 'critical') return <Tag tone="danger">严重</Tag>;
|
|
if (severity === 'warning') return <Tag tone="warning">警告</Tag>;
|
|
return <Tag tone="info">提示</Tag>;
|
|
}
|
|
|
|
function makeAlertColumns(onMarkRead: (alert: InfrastructureAlert) => void, readingFingerprint: string): Array<TableColumn<InfrastructureAlert>> { return [
|
|
{ key: 'severity', title: '级别', width: '82px', render: (record) => severityTag(record.severity) },
|
|
{
|
|
key: 'alert', title: '告警', width: '280px', render: (record) => (
|
|
<div className="system-monitoring-alert-copy"><strong>{record.name}</strong><span>{record.summary}</span></div>
|
|
),
|
|
},
|
|
{ key: 'service', title: '服务 / 实例', width: '190px', render: (record) => record.service || record.instance || '主机资源' },
|
|
{ key: 'value', title: '当前值 / 阈值', width: '150px', render: (record) => `${record.currentValue || '—'} / ${record.threshold || '—'}` },
|
|
{ key: 'startedAt', title: '开始时间', width: '150px', render: (record) => formatTime(record.startedAt) },
|
|
{ key: 'duration', title: '持续时间', width: '120px', render: (record) => formatDuration(record.startedAt) },
|
|
{
|
|
key: 'actions', title: '操作', width: '112px', render: (record) => record.acknowledged
|
|
? <Tag tone="neutral">已读</Tag>
|
|
: <Button disabled={readingFingerprint === record.fingerprint} icon={<CheckCircle2 size={14} />} onClick={() => onMarkRead(record)} size="sm" variant="ghost">{readingFingerprint === record.fingerprint ? '处理中' : '标记已读'}</Button>,
|
|
},
|
|
]; }
|
|
|
|
export function AdminSystemMonitoringPage() {
|
|
const [range, setRange] = useState<InfrastructureMonitoringRange>('24h');
|
|
const [overview, setOverview] = useState<InfrastructureMonitoringOverview | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState('');
|
|
const [settings, setSettings] = useState<InfrastructureAlertSettings | null>(null);
|
|
const [draftThresholds, setDraftThresholds] = useState<InfrastructureAlertThresholds>({});
|
|
const [showSettings, setShowSettings] = useState(false);
|
|
const [settingsError, setSettingsError] = useState('');
|
|
const [savingSettings, setSavingSettings] = useState(false);
|
|
const [readingFingerprint, setReadingFingerprint] = useState('');
|
|
const [readError, setReadError] = useState('');
|
|
const requestSequence = useRef(0);
|
|
const pendingRequests = useRef(0);
|
|
|
|
const loadData = useCallback(async (supersede = false) => {
|
|
if (!supersede && pendingRequests.current > 0) return;
|
|
pendingRequests.current += 1;
|
|
const sequence = ++requestSequence.current;
|
|
setLoading(true);
|
|
try {
|
|
const result = await adminApi.getInfrastructureMonitoringOverview(range);
|
|
if (sequence !== requestSequence.current) return;
|
|
setOverview(result);
|
|
setError(result.available ? '' : result.error || '监控数据当前不可用');
|
|
} catch (reason) {
|
|
if (sequence !== requestSequence.current) return;
|
|
setOverview(null);
|
|
setError(reason instanceof Error ? reason.message : '监控数据加载失败');
|
|
} finally {
|
|
if (sequence === requestSequence.current) setLoading(false);
|
|
pendingRequests.current -= 1;
|
|
}
|
|
}, [range]);
|
|
|
|
const loadSettings = useCallback(async () => {
|
|
try {
|
|
const result = await adminApi.getInfrastructureAlertThresholds();
|
|
setSettings(result);
|
|
setDraftThresholds(result.thresholds);
|
|
setSettingsError('');
|
|
} catch (reason) {
|
|
setSettingsError(reason instanceof Error ? reason.message : '告警阈值加载失败');
|
|
}
|
|
}, []);
|
|
|
|
const saveSettings = useCallback(async () => {
|
|
if (!settings) return;
|
|
setSavingSettings(true);
|
|
setSettingsError('');
|
|
try {
|
|
const result = await adminApi.updateInfrastructureAlertThresholds({ configVersion: settings.configVersion, thresholds: draftThresholds });
|
|
setSettings(result);
|
|
setDraftThresholds(result.thresholds);
|
|
setShowSettings(false);
|
|
window.dispatchEvent(new Event('cmpp-infrastructure-alert-count-refresh'));
|
|
await loadData(true);
|
|
} catch (reason) {
|
|
setSettingsError(reason instanceof Error ? reason.message : '告警阈值保存失败');
|
|
} finally {
|
|
setSavingSettings(false);
|
|
}
|
|
}, [draftThresholds, loadData, settings]);
|
|
|
|
const markAlertRead = useCallback(async (alert: InfrastructureAlert) => {
|
|
setReadingFingerprint(alert.fingerprint);
|
|
setReadError('');
|
|
try {
|
|
const result = await adminApi.markInfrastructureAlertRead(alert.fingerprint, alert.startedAt);
|
|
setOverview((current) => current ? {
|
|
...current,
|
|
alerts: current.alerts.map((item) => item.fingerprint === result.fingerprint && Date.parse(item.startedAt) === Date.parse(result.activeAt)
|
|
? { ...item, acknowledged: true, acknowledgedAt: result.acknowledgedAt }
|
|
: item),
|
|
} : current);
|
|
window.dispatchEvent(new Event('cmpp-infrastructure-alert-count-refresh'));
|
|
} catch (reason) {
|
|
setReadError(reason instanceof Error ? reason.message : '活动告警标记已读失败');
|
|
} finally {
|
|
setReadingFingerprint('');
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
void loadData(true);
|
|
void loadSettings();
|
|
const intervalId = window.setInterval(() => {
|
|
if (document.visibilityState === 'visible') void loadData();
|
|
}, 30_000);
|
|
const handleVisibility = () => {
|
|
if (document.visibilityState === 'visible') void loadData();
|
|
};
|
|
document.addEventListener('visibilitychange', handleVisibility);
|
|
return () => {
|
|
requestSequence.current += 1;
|
|
window.clearInterval(intervalId);
|
|
document.removeEventListener('visibilitychange', handleVisibility);
|
|
};
|
|
}, [loadData, loadSettings]);
|
|
|
|
const status = STATUS_COPY[overview?.summary.overallStatus ?? 'unknown'];
|
|
const cpuOption = useMemo(() => makeTrendOption({
|
|
range, maximum: 100, suffix: '%', series: [{ name: 'CPU', points: overview?.trends.cpuUsagePercent ?? [], color: '#2563eb' }],
|
|
}), [overview?.trends.cpuUsagePercent, range]);
|
|
const memoryOption = useMemo(() => makeTrendOption({
|
|
range, maximum: 100, suffix: '%', series: [{ name: '内存', points: overview?.trends.memoryUsagePercent ?? [], color: '#7c3aed' }],
|
|
}), [overview?.trends.memoryUsagePercent, range]);
|
|
const diskOption = useMemo(() => makeTrendOption({
|
|
range, maximum: 100, suffix: '%', series: (overview?.disks ?? []).map((disk, index) => ({
|
|
name: `${disk.mountpoint} · ${disk.device} · ${disk.instance}`,
|
|
points: disk.trend,
|
|
color: ['#d97706', '#2563eb', '#0f766e', '#7c3aed', '#dc2626', '#0891b2'][index % 6],
|
|
})),
|
|
}), [overview?.disks, range]);
|
|
const networkOption = useMemo(() => makeTrendOption({
|
|
range, suffix: ' B/s', series: [
|
|
{ name: '接收', points: overview?.trends.networkReceiveBytesPerSecond ?? [], color: '#0f766e' },
|
|
{ name: '发送', points: overview?.trends.networkTransmitBytesPerSecond ?? [], color: '#2563eb' },
|
|
],
|
|
}), [overview?.trends.networkReceiveBytesPerSecond, overview?.trends.networkTransmitBytesPerSecond, range]);
|
|
|
|
const metrics = overview?.metrics;
|
|
const serviceHealthy = overview?.summary.serviceHealthy ?? 0;
|
|
const serviceTotal = overview?.summary.serviceTotal ?? 6;
|
|
const alertColumns = useMemo(() => makeAlertColumns((alert) => { void markAlertRead(alert); }, readingFingerprint), [markAlertRead, readingFingerprint]);
|
|
|
|
return (
|
|
<section className="page-stack admin-system-monitoring-page">
|
|
<div className="page-heading system-monitoring-heading">
|
|
<div>
|
|
<Breadcrumb items={['系统管理', '系统监控']} />
|
|
<div className="system-monitoring-title-row">
|
|
<p>服务器资源、核心服务与活动告警,数据由 Prometheus 采集与计算</p>
|
|
<Tag tone={status.tone}>{status.label}</Tag>
|
|
</div>
|
|
</div>
|
|
<div className="system-monitoring-controls">
|
|
<div className="system-monitoring-range" aria-label="监控时间范围" role="group">
|
|
{RANGE_OPTIONS.map((option) => (
|
|
<button
|
|
aria-pressed={range === option.value}
|
|
className={range === option.value ? 'is-active' : ''}
|
|
key={option.value}
|
|
onClick={() => setRange(option.value)}
|
|
type="button"
|
|
>{option.label}</button>
|
|
))}
|
|
</div>
|
|
<Button disabled={loading} icon={<RefreshCw className={loading ? 'is-spinning' : ''} size={16} />} onClick={() => void loadData()} variant="ghost">
|
|
{loading ? '刷新中' : '刷新'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{error ? (
|
|
<div className="system-monitoring-unavailable" role="alert">
|
|
<ShieldAlert size={20} />
|
|
<div><strong>监控数据不可用</strong><span>{error}。页面不会展示历史缓存值。</span></div>
|
|
</div>
|
|
) : null}
|
|
|
|
<div className="system-monitoring-health surface">
|
|
<div className="system-monitoring-health__copy">
|
|
<div className={`system-monitoring-health__mark is-${overview?.summary.overallStatus ?? 'unknown'}`}>
|
|
{overview?.summary.overallStatus === 'healthy' ? <CheckCircle2 size={24} /> : <AlertTriangle size={24} />}
|
|
</div>
|
|
<span>平台基础设施</span>
|
|
<strong>{status.label}</strong>
|
|
<small>最新采样 {formatTime(overview?.lastSampleAt ?? null)}</small>
|
|
</div>
|
|
<div className="system-monitoring-health__fact"><span>核心服务</span><strong>{serviceHealthy}/{serviceTotal}</strong><small>正常运行</small></div>
|
|
<div className="system-monitoring-health__fact"><span>活动告警</span><strong>{overview?.summary.activeAlerts ?? 0}</strong><small>{overview?.summary.criticalAlerts ?? 0} 严重 · {overview?.summary.warningAlerts ?? 0} 警告</small></div>
|
|
<div className="system-monitoring-health__fact"><span>系统负载</span><strong>{metrics?.load1 === null || metrics?.load1 === undefined ? '—' : metrics.load1.toFixed(2)}</strong><small>最近1分钟</small></div>
|
|
<div className="system-monitoring-health__fact"><span>持续运行</span><strong>{formatUptime(metrics?.uptimeSeconds ?? null)}</strong><small>主机启动后</small></div>
|
|
</div>
|
|
|
|
<div className="system-monitoring-metrics">
|
|
<article className="surface system-monitoring-metric"><div className="system-monitoring-metric__icon is-blue"><Cpu size={19} /></div><div><span>CPU 使用率</span><strong>{formatPercent(metrics?.cpuUsagePercent ?? null)}</strong><small>5分钟平均</small></div></article>
|
|
<article className="surface system-monitoring-metric"><div className="system-monitoring-metric__icon is-violet"><MemoryStick size={19} /></div><div><span>内存使用率</span><strong>{formatPercent(metrics?.memoryUsagePercent ?? null)}</strong><small>{formatBytes(metrics?.memoryAvailableBytes ?? null)} 可用 / {formatBytes(metrics?.memoryTotalBytes ?? null)}</small></div></article>
|
|
{(overview?.disks ?? []).map((disk) => <article className="surface system-monitoring-metric" key={disk.id}>
|
|
<div className="system-monitoring-metric__icon is-amber"><HardDrive size={19} /></div>
|
|
<div><span>{disk.mountpoint === '/' ? '系统盘' : '磁盘'} {disk.mountpoint}</span><strong>{formatPercent(disk.usagePercent)}</strong><small title={`${disk.device} · ${disk.filesystem} · ${disk.instance}`}>{disk.device} · {disk.filesystem}</small><small>{formatBytes(disk.availableBytes)} 可用 / {formatBytes(disk.totalBytes)}</small></div>
|
|
</article>)}
|
|
{!overview?.disks?.length ? <article className="surface system-monitoring-metric"><HardDrive size={19} /><div><span>磁盘</span><strong>暂无数据</strong></div></article> : null}
|
|
<article className="surface system-monitoring-metric"><div className="system-monitoring-metric__icon is-green"><Network size={19} /></div><div><span>网络吞吐</span><strong>{formatRate(totalNetworkRate(metrics?.networkReceiveBytesPerSecond, metrics?.networkTransmitBytesPerSecond))}</strong><small>接收 {formatRate(metrics?.networkReceiveBytesPerSecond ?? null)} · 发送 {formatRate(metrics?.networkTransmitBytesPerSecond ?? null)}</small></div></article>
|
|
</div>
|
|
|
|
<div className="system-monitoring-main-grid">
|
|
<div className="system-monitoring-chart-stack">
|
|
<article className="surface system-monitoring-chart-card"><header><div><Cpu size={17} /><strong>CPU 趋势</strong></div><span>{formatPercent(metrics?.cpuUsagePercent ?? null)}</span></header>{overview?.trends.cpuUsagePercent.length ? <Chart height={230} option={cpuOption} /> : <EmptyChart />}</article>
|
|
<article className="surface system-monitoring-chart-card"><header><div><MemoryStick size={17} /><strong>内存趋势</strong></div><span>{formatPercent(metrics?.memoryUsagePercent ?? null)}</span></header>{overview?.trends.memoryUsagePercent.length ? <Chart height={230} option={memoryOption} /> : <EmptyChart />}</article>
|
|
<article className="surface system-monitoring-chart-card"><header><div><HardDrive size={17} /><strong>全部磁盘趋势</strong></div><span>{overview?.disks?.length ?? 0} 个挂载点</span></header>{overview?.disks?.some((disk) => disk.trend.length) ? <Chart height={230} option={diskOption} /> : <EmptyChart />}</article>
|
|
<article className="surface system-monitoring-chart-card"><header><div><Activity size={17} /><strong>网络趋势</strong></div><span>{formatRate(totalNetworkRate(metrics?.networkReceiveBytesPerSecond, metrics?.networkTransmitBytesPerSecond))}</span></header>{overview?.trends.networkReceiveBytesPerSecond.length ? <Chart height={230} option={networkOption} /> : <EmptyChart />}</article>
|
|
</div>
|
|
|
|
<aside className="surface system-monitoring-services">
|
|
<header><div><Server size={18} /><strong>核心服务</strong></div><Tag tone={serviceHealthy === serviceTotal && overview?.available ? 'success' : 'neutral'}>{serviceHealthy}/{serviceTotal} 正常</Tag></header>
|
|
<div className="system-monitoring-service-list">
|
|
{(overview?.services ?? []).map((service) => (
|
|
<div className="system-monitoring-service" key={service.key}>
|
|
<span className={`system-monitoring-service__dot is-${service.status}`} />
|
|
<div><strong>{service.name}</strong><small>{service.unit}</small></div>
|
|
<span>{service.status === 'healthy' ? '正常' : service.status === 'unhealthy' ? '异常' : '未知'}</span>
|
|
</div>
|
|
))}
|
|
{!overview?.services.length ? [
|
|
['api', 'API服务'], ['gateway', 'Gateway服务'], ['postgresql', 'PostgreSQL'], ['redis', 'Redis'], ['minio', 'MinIO'], ['nginx', 'Nginx'],
|
|
].map(([key, name]) => <div className="system-monitoring-service" key={key}><span className="system-monitoring-service__dot is-unknown" /><div><strong>{name}</strong><small>等待真实采集</small></div><span>未知</span></div>) : null}
|
|
</div>
|
|
<div className="system-monitoring-collector-note"><Database size={16} /><span>指标由 Prometheus 采集,业务数据库不写入高频时序数据。</span></div>
|
|
</aside>
|
|
</div>
|
|
|
|
<section className="surface system-monitoring-service-metrics">
|
|
<header>
|
|
<div><Database size={18} /><strong>服务关键指标</strong></div>
|
|
<div className="system-monitoring-service-actions"><span>固定低基数聚合,不含手机号、短信ID或SQL文本</span><Button icon={<Settings2 size={15} />} onClick={() => setShowSettings(true)} variant="ghost">告警阈值设置</Button></div>
|
|
</header>
|
|
<div className="system-monitoring-service-metric-grid">
|
|
{(overview?.serviceMetrics ?? []).map((group) => (
|
|
<article key={group.key}>
|
|
<div className="system-monitoring-service-metric-title"><strong>{group.name}</strong><Tag tone={group.available ? 'success' : 'neutral'}>{group.available ? '已采集' : '待采集'}</Tag></div>
|
|
{group.metrics.length ? group.metrics.map((metric) => <div className="system-monitoring-service-metric-row" key={metric.key}><span>{metric.label}</span><strong>{formatServiceMetric(metric.value, metric.unit)}</strong></div>) : <div className="system-monitoring-service-metric-empty">已监控服务可用性,待原生容量指标接入</div>}
|
|
</article>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
<section className="surface system-monitoring-alerts" id="active-alerts">
|
|
<header><div><AlertTriangle size={18} /><strong>活动告警</strong><Tag tone={overview?.summary.activeAlerts ? 'warning' : 'success'}>{overview?.summary.activeAlerts ?? 0}</Tag></div><span><Clock3 size={14} /> 刷新于 {formatTime(overview?.collectedAt ?? null)}</span></header>
|
|
{readError ? <div className="system-monitoring-unavailable" role="alert"><AlertTriangle size={18} /><div><strong>标记已读失败</strong><span>{readError}</span></div></div> : null}
|
|
<Table columns={alertColumns} data={overview?.alerts ?? []} emptyText={overview?.available ? '当前没有活动告警' : '监控不可用,无法读取活动告警'} pagination={false} rowKey="fingerprint" />
|
|
</section>
|
|
|
|
<Modal footer={<><Button onClick={() => setShowSettings(false)} variant="ghost">取消</Button><Button disabled={savingSettings || !settings} onClick={() => void saveSettings()}>{savingSettings ? '验证并应用中' : '保存并应用'}</Button></>} onClose={() => setShowSettings(false)} open={showSettings} title="Prometheus 告警阈值设置">
|
|
<div className="system-monitoring-threshold-dialog">
|
|
<div className="system-monitoring-threshold-note"><ShieldAlert size={17} /><span>仅允许修改固定指标的数值阈值。保存时先由 promtool 校验,再原子替换规则并热加载 Prometheus。</span></div>
|
|
{settings?.definitions.map((definition) => (
|
|
<div className="system-monitoring-threshold-row" key={definition.key}>
|
|
<div><strong>{definition.label}</strong><small>单位:{definition.unit}</small></div>
|
|
<Input label="警告阈值" max={definition.max} min={definition.min} onChange={(event) => setDraftThresholds((current) => ({ ...current, [definition.key]: { ...current[definition.key], warning: Number(event.target.value) } }))} step={definition.step} type="number" value={draftThresholds[definition.key]?.warning ?? ''} />
|
|
<Input label="严重阈值" max={definition.max} min={definition.min} onChange={(event) => setDraftThresholds((current) => ({ ...current, [definition.key]: { ...current[definition.key], critical: Number(event.target.value) } }))} step={definition.step} type="number" value={draftThresholds[definition.key]?.critical ?? ''} />
|
|
</div>
|
|
))}
|
|
{settings?.applyStatus === 'failed' ? <div className="system-monitoring-unavailable"><AlertTriangle size={18} /><div><strong>上次应用失败</strong><span>{settings.lastError}</span></div></div> : null}
|
|
{settingsError ? <div className="system-monitoring-unavailable"><AlertTriangle size={18} /><div><strong>阈值配置不可用</strong><span>{settingsError}</span></div></div> : null}
|
|
</div>
|
|
</Modal>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function EmptyChart() {
|
|
return <div className="system-monitoring-chart-empty"><Activity size={22} /><span>暂无真实趋势指标</span></div>;
|
|
}
|