Files
EventHubFrontAdmin/src/pages/monitoring/MonitoringPage.tsx
T
aleksey de379e28bf
CI / test (push) Successful in 1m8s
CI / push-image (push) Successful in 6m9s
CI / ci-done (push) Successful in 0s
Deploy stage (admin) / deploy-stage-admin (push) Successful in 4m46s
fix(charts): единый стиль AreaChart с градиентом
Общий AreaTrendChart для мониторинга и daily-графиков, StatisticSparklineCard на дашборде.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 17:55:24 +03:00

222 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useMemo, useState } from 'react';
import { Card, Col, Row, Select, Table, Tabs } from 'antd';
import dayjs from 'dayjs';
import { useMetricsStore, NodeMetric } from '../../store/metricsStore';
import { monitoringApi } from '../../api/monitoringApi';
import AreaTrendChart from '../../components/AreaTrendChart';
const NODE_COLORS = ['#1677ff', '#52c41a', '#fa8c16', '#eb2f96', '#722ed1', '#13c2c2'];
const TIME_RANGES = [
{ label: '5 мин', value: 5 },
{ label: '15 мин', value: 15 },
{ label: '30 мин', value: 30 },
{ label: '1 час', value: 60 },
];
const bytesToMb = (value?: number) => (value ?? 0) / 1048576;
const formatTime = (ts: string) => dayjs(ts).format('HH:mm:ss');
const buildChartData = (metrics: NodeMetric[]) =>
metrics.map((m) => ({
time: formatTime(m.timestamp),
cpu: m.cpu_utilization ?? 0,
memoryMb: bytesToMb(m.memory_total),
memoryProcessesMb: bytesToMb(m.memory_processes),
memoryAtomMb: bytesToMb(m.memory_atom),
memoryBinaryMb: bytesToMb(m.memory_binary),
memoryEtsMb: bytesToMb(m.memory_ets),
memoryAvailableMb: bytesToMb(m.memory_available),
ws: m.ws_connections ?? 0,
runQueue: m.run_queue ?? 0,
processCount: m.process_count ?? 0,
activeSessions: m.active_sessions ?? 0,
ioIn: m.io_in ?? 0,
ioOut: m.io_out ?? 0,
mnesiaCommits: m.mnesia_commits ?? 0,
mnesiaFailures: m.mnesia_failures ?? 0,
tableSizes: m.table_sizes ?? {},
}));
const MetricChart: React.FC<{
data: ReturnType<typeof buildChartData>;
lines: Array<{ key: string; color: string; name: string }>;
}> = ({ data, lines }) => (
<AreaTrendChart data={data} xKey="time" series={lines} />
);
const MonitoringPage: React.FC = () => {
const allHistory = useMetricsStore((s) => s.allHistory);
const current = useMetricsStore((s) => s.current);
const setAllHistory = useMetricsStore((s) => s.setAllHistory);
const filterVisibleHistory = useMetricsStore((s) => s.filterVisibleHistory);
const visibleHistory = useMetricsStore((s) => s.visibleHistory);
const [minutes, setMinutes] = useState(15);
const [loadingHistory, setLoadingHistory] = useState(false);
useEffect(() => {
let cancelled = false;
const loadHistory = async () => {
setLoadingHistory(true);
try {
const to = dayjs().toISOString();
const from = dayjs().subtract(minutes, 'minute').toISOString();
const metrics = await monitoringApi.getNodeMetrics(from, to);
if (!cancelled && metrics.length > 0) {
setAllHistory(metrics);
}
} catch {
// live WS дополняет историю; REST недоступен — не блокируем страницу
} finally {
if (!cancelled) {
setLoadingHistory(false);
}
}
};
loadHistory();
return () => {
cancelled = true;
};
}, [minutes, setAllHistory]);
useEffect(() => {
filterVisibleHistory(minutes);
}, [minutes, allHistory.length, filterVisibleHistory]);
const allNodes = useMemo(() => {
const nodes = new Set<string>();
[...allHistory, ...(current ? [current] : [])].forEach((m) => nodes.add(m.node));
return Array.from(nodes).sort();
}, [allHistory, current]);
const metricsByNode = useMemo(() => {
const source = visibleHistory.length > 0 ? visibleHistory : allHistory;
const map = new Map<string, NodeMetric[]>();
source.forEach((m) => {
const list = map.get(m.node) || [];
list.push(m);
map.set(m.node, list);
});
return map;
}, [visibleHistory, allHistory]);
const tabItems = allNodes.map((node, idx) => {
const nodeMetrics = metricsByNode.get(node) || [];
const chartData = buildChartData(nodeMetrics);
const latestTableSizes = chartData.at(-1)?.tableSizes ?? {};
const color = NODE_COLORS[idx % NODE_COLORS.length];
return {
key: node,
label: <span style={{ color }}>{node}</span>,
children: (
<Row gutter={[16, 16]}>
<Col span={24}>
<Card title="CPU и память (МБ)">
<MetricChart
data={chartData}
lines={[
{ key: 'cpu', color, name: 'CPU %' },
{ key: 'memoryMb', color: '#1890ff', name: 'Память всего' },
{ key: 'memoryAvailableMb', color: '#13c2c2', name: 'Память доступно' },
]}
/>
</Card>
</Col>
<Col span={24}>
<Card title="Память по типам (МБ)">
<MetricChart
data={chartData}
lines={[
{ key: 'memoryProcessesMb', color: '#1677ff', name: 'Processes' },
{ key: 'memoryAtomMb', color: '#52c41a', name: 'Atom' },
{ key: 'memoryBinaryMb', color: '#fa8c16', name: 'Binary' },
{ key: 'memoryEtsMb', color: '#eb2f96', name: 'ETS' },
]}
/>
</Card>
</Col>
<Col xs={24} lg={12}>
<Card title="WebSocket и очередь">
<MetricChart
data={chartData}
lines={[
{ key: 'ws', color: '#52c41a', name: 'WS' },
{ key: 'runQueue', color: '#fa8c16', name: 'Run queue' },
{ key: 'processCount', color: '#722ed1', name: 'Processes' },
{ key: 'activeSessions', color: '#1677ff', name: 'Sessions' },
]}
/>
</Card>
</Col>
<Col xs={24} lg={12}>
<Card title="Mnesia">
<MetricChart
data={chartData}
lines={[
{ key: 'mnesiaCommits', color: '#722ed1', name: 'Commits' },
{ key: 'mnesiaFailures', color: '#ff4d4f', name: 'Failures' },
]}
/>
</Card>
</Col>
<Col span={24}>
<Card title="I/O">
<MetricChart
data={chartData}
lines={[
{ key: 'ioIn', color: '#1677ff', name: 'IO in' },
{ key: 'ioOut', color: '#52c41a', name: 'IO out' },
]}
/>
</Card>
</Col>
<Col span={24}>
<Card title="Размеры таблиц Mnesia (последняя точка)">
<Table
size="small"
pagination={false}
rowKey="table"
dataSource={Object.entries(latestTableSizes).map(([table, size]) => ({
table,
size,
}))}
columns={[
{ title: 'Таблица', dataIndex: 'table', key: 'table' },
{ title: 'Записей', dataIndex: 'size', key: 'size' },
]}
/>
</Card>
</Col>
</Row>
),
};
});
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<h2 style={{ margin: 0 }}>Мониторинг нод</h2>
<Select
value={minutes}
onChange={setMinutes}
options={TIME_RANGES}
style={{ width: 120 }}
/>
</div>
{allNodes.length === 0 ? (
<Card loading={loadingHistory}>
{loadingHistory ? 'Загрузка метрик…' : 'Ожидание метрик по WebSocket…'}
</Card>
) : (
<Tabs items={tabItems} />
)}
</div>
);
};
export default MonitoringPage;