import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { EChartsOption } from 'echarts'; import { Link } from 'react-router-dom'; import { Button, Modal, Select, Table, Tag, type TableColumn } from '@/components/ui'; import { monitorApi, names, ruleSource, states, time, title, type Alert, type Metric, type Page, type Snapshot, } from './monitorApi'; const TrendChart = lazy(() => import('@/components/ui/Chart').then((m) => ({ default: m.Chart }))); const closeReasons: Record = { recovered: '已连续恢复', data_corrected: '迟到数据修正', disabled: '规则已停用', rule_changed: '规则版本变化', enrollment_removed: '已移出监控', }; export function Rate({ value }: { value: Metric }) { return (
{value.rate === null ? '—' : `${value.rate.toFixed(2)}%`} {value.success.toLocaleString()} / {value.mature.toLocaleString()} 成熟 {value.observing} 条观察中{value.insufficient ? ' · 样本不足' : ''} {value.bad ? ' · 低于下限' : ''}
); } export function MonitorHistory({ row, onClose, alert }: { row: Snapshot; onClose: () => void; alert?: Alert }) { const [range, setRange] = useState('2h'), [items, setItems] = useState([]), [error, setError] = useState(''); useEffect(() => { let live = true; monitorApi .history(row, range) .then((r) => { if (live) { setItems(r); setError(''); } }) .catch((e) => { if (live) setError(e.message); }); return () => { live = false; }; }, [row, range]); const chartOption = useMemo( () => ({ tooltip: { trigger: 'axis', confine: true }, legend: { type: 'scroll', bottom: 0 }, grid: { left: 45, right: 20, top: 20, bottom: 65 }, xAxis: { type: 'category', data: items.map((item) => new Date(item.evaluationAt).toLocaleTimeString('zh-CN', { timeZone: 'Asia/Shanghai', hour: '2-digit', minute: '2-digit', }), ), }, yAxis: { type: 'value', min: 0, max: 100, axisLabel: { formatter: '{value}%' } }, series: row.metrics.metrics.flatMap((m, i) => [ { name: `${m.seconds}秒到达率`, type: 'line' as const, connectNulls: false, data: items.map((item) => item.metrics.metrics[i]?.rate ?? null), showSymbol: false, }, { name: `${m.seconds}秒下限`, type: 'line' as const, step: 'end' as const, connectNulls: false, data: items.map((item) => (item.rule?.config.enabled ? (item.metrics.metrics[i]?.threshold ?? null) : null)), showSymbol: false, lineStyle: { type: 'dashed' as const }, }, ]), }), [items, row.metrics.metrics], ); return ( 关闭}>

{title(row.dimensions)}

{alert && (

告警开始:{time(alert.openedAt)} · 最近评估:{time(alert.lastEvaluatedAt)} · {states[alert.state]}{' '} {closeReasons[alert.closeReason ?? ''] ?? ''}

最差快照:{time(alert.worst.evaluationAt)} ·{' '} {alert.worst.metrics.metrics .map( (m) => `${m.seconds}秒 ${m.rate === null ? '—' : m.rate.toFixed(2) + '%'}(${m.success}/${m.mature})`, ) .join(';')}

)} 查看该窗口样本(沿用短信详情权限,样本保留72小时)

统计窗口:{time(row.windowFrom)} 至 {time(row.evaluationAt)}(不含结束时刻)

观察截止:{time(row.observedUntil)} · {row.stage === 'final' ? '定稿' : '初评'} · 修订 {row.revision}

{ruleSource(row.rule)}{' '} {row.rule ? `v${row.rule.version},最低成熟 ${row.rule.config.minSamples} 条,下限 ${row.rule.config.thresholds.map((v) => (v === null ? '未启用' : `${v}%`)).join(' / ')}` : '等待配置阈值'}

不可评估 {row.metrics.unassessable} 条。{row.completeness.reason}

{ setReadStatus(event.target.value); setPage(1); }} />