feat: add configurable infrastructure alerts

This commit is contained in:
hectorzhao
2026-08-14 17:51:42 +08:00
parent 1ef4380422
commit 6ccc102830
23 changed files with 506 additions and 39 deletions
@@ -321,3 +321,16 @@
@media (prefers-reduced-motion: reduce) {
.is-spinning { animation: none; }
}
.system-monitoring-service-actions { align-items: center; display: flex; gap: 12px; }
.system-monitoring-threshold-dialog { display: grid; gap: 12px; max-height: 62vh; overflow: auto; padding-right: 4px; }
.system-monitoring-threshold-note { align-items: flex-start; background: #eff6ff; border-radius: 10px; color: #475569; display: flex; font-size: 13px; gap: 9px; padding: 12px 14px; }
.system-monitoring-threshold-row { align-items: end; border-bottom: 1px solid #eef2f7; display: grid; gap: 14px; grid-template-columns: minmax(180px, 1fr) 150px 150px; padding: 12px 0; }
.system-monitoring-threshold-row > div:first-child { align-self: center; display: grid; gap: 4px; }
.system-monitoring-threshold-row small { color: #64748b; }
@media (max-width: 760px) {
.system-monitoring-service-actions { align-items: flex-start; flex-direction: column; }
.system-monitoring-threshold-row { grid-template-columns: 1fr 1fr; }
.system-monitoring-threshold-row > div:first-child { grid-column: 1 / -1; }
}
@@ -12,16 +12,19 @@ import {
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, Chart, Table, Tag, type TableColumn } from '@/components/ui';
import { Breadcrumb, Button, Chart, Input, Modal, Table, Tag, type TableColumn } from '@/components/ui';
import './AdminSystemMonitoringPage.css';
const RANGE_OPTIONS: Array<{ value: InfrastructureMonitoringRange; label: string }> = [
@@ -166,6 +169,11 @@ export function AdminSystemMonitoringPage() {
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 requestSequence = useRef(0);
const pendingRequests = useRef(0);
@@ -189,8 +197,38 @@ export function AdminSystemMonitoringPage() {
}
}, [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]);
useEffect(() => {
void loadData(true);
void loadSettings();
const intervalId = window.setInterval(() => {
if (document.visibilityState === 'visible') void loadData();
}, 30_000);
@@ -203,7 +241,7 @@ export function AdminSystemMonitoringPage() {
window.clearInterval(intervalId);
document.removeEventListener('visibilitychange', handleVisibility);
};
}, [loadData]);
}, [loadData, loadSettings]);
const status = STATUS_COPY[overview?.summary.overallStatus ?? 'unknown'];
const cpuOption = useMemo(() => makeTrendOption({
@@ -232,7 +270,7 @@ export function AdminSystemMonitoringPage() {
<div>
<Breadcrumb items={['系统管理', '系统监控']} />
<div className="system-monitoring-title-row">
<p></p>
<p> Prometheus </p>
<Tag tone={status.tone}>{status.label}</Tag>
</div>
</div>
@@ -283,29 +321,6 @@ export function AdminSystemMonitoringPage() {
<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>
<section className="surface system-monitoring-service-metrics">
<header>
<div><Database size={18} /><strong></strong></div>
<span>ID或SQL文本</span>
</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>
<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>
@@ -332,10 +347,40 @@ export function AdminSystemMonitoringPage() {
</aside>
</div>
<section className="surface system-monitoring-alerts">
<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>
<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>
);
}