trust-module-frontend/src/Charts/SystemChart.jsx

283 lines
10 KiB
JavaScript

import React, { useState, useEffect, useMemo } from 'react';
import LineChartComponent from './LineChartComponent';
import DateRangeSelector from '../Charts2/Components/DateRangeSelector';
import metricsService from '../Charts2/Components/metricsService';
import { Button, Radio, message, Tag, Spin } from 'antd';
import moment from 'moment';
import StatusLogTable from '../Charts2/Components/StatusLogTable';
import { Box, IconButton, Tooltip as MuiTooltip } from '@mui/material';
import { ListAlt } from '@mui/icons-material';
const SystemChart = ({ metricInfo, chartHeight = 580 }) => {
const {
name: metricName,
filters = {},
title = metricName,
description,
context = {},
ranges = [],
multipleLines = false,
lineKey = 'device'
} = metricInfo || {};
const { device, source_id } = context;
const [rawData, setRawData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const [metricMeta, setMetricMeta] = useState({});
const [mode, setMode] = useState('realtime');
const [startDate, setStartDate] = useState(moment().subtract(1, 'hour').toDate());
const [endDate, setEndDate] = useState(moment().toDate());
const [isLiveUpdating, setIsLiveUpdating] = useState(false);
const [showLogs, setShowLogs] = useState(false);
const [statusLogs, setStatusLogs] = useState([]);
const MAX_POINTS = 1000;
const TIME_WINDOW_MS = 3600 * 1000;
const subscriptionKey = useMemo(() => {
const filterParts = [];
if (device) filterParts.push(`device=${encodeURIComponent(device)}`);
if (source_id) filterParts.push(`source_id=${encodeURIComponent(source_id)}`);
return `${metricName}${filterParts.length ? `?${filterParts.join('&')}` : ''}`;
}, [metricName, device, source_id]);
const formatMetricData = (dataArray) => {
return dataArray.map(item => ({
...item,
timestamp: item.timestamp,
value: parseFloat(item.value),
name: item.__name__ || metricName,
status: parseInt(item.status) || 0,
device: item.device?.trim() || null,
source_id: item.source_id || null,
description: item.description || description,
lineId: item[lineKey] || 'default'
}));
};
const calculateStep = (start, end) => {
const duration = end.getTime() - start.getTime();
return Math.max(Math.floor(duration / (MAX_POINTS * 1000)), 1);
};
const fetchHistoricalData = async (start, end) => {
setIsLoading(true);
setError(null);
try {
const extendedFilters = {
...filters,
...(device && { device: device.toString() }),
...(source_id && { source_id: source_id.toString() })
};
const step = calculateStep(start, end);
const data = await metricsService.fetchMetricsRange(
metricName,
Math.floor(start.getTime() / 1000),
Math.floor(end.getTime() / 1000),
step,
extendedFilters
);
const formattedData = formatMetricData(data);
setRawData(formattedData);
if (formattedData.length > 0) {
setMetricMeta({
type: data[0]?.type,
description: data[0]?.description || description,
instance: data[0]?.instance,
job: data[0]?.job
});
}
} catch (err) {
console.error(`Error loading historical data for ${metricName}:`, err);
setError(err.message);
message.error(`Failed to load historical data: ${err.message}`);
} finally {
setIsLoading(false);
}
};
const startRealtimeUpdates = () => {
setIsLiveUpdating(true);
setIsLoading(true);
const end = new Date();
const start = new Date(end.getTime() - TIME_WINDOW_MS);
fetchHistoricalData(start, end).finally(() => setIsLoading(false));
return metricsService.subscribeToMetric(
subscriptionKey,
(newData) => {
setRawData(prev => {
const now = Date.now();
const cutoffTime = now - TIME_WINDOW_MS;
const formattedNewData = formatMetricData(newData)
.filter(point => point.timestamp >= cutoffTime);
const filteredPrev = prev.filter(point => point.timestamp >= cutoffTime);
// Объединяем данные, удаляем дубликаты
const merged = [...filteredPrev, ...formattedNewData]
.filter((v, i, a) =>
a.findIndex(t =>
t.timestamp === v.timestamp &&
t[lineKey] === v[lineKey]
) === i
);
return merged;
});
},
5000, // Интервал обновления 5 секунд
{
...filters,
...(device && { device }),
...(source_id && { source_id })
}
);
};
const stopRealtimeUpdates = () => {
setIsLiveUpdating(false);
metricsService.unsubscribeFromMetric(subscriptionKey);
};
const handleCustomRangeApply = () => {
if (startDate && endDate) {
fetchHistoricalData(startDate, endDate);
}
};
// Обновляем логи статусов
useEffect(() => {
if (rawData.length > 0) {
const logs = [];
const devices = [...new Set(rawData.map(item => item[lineKey]))];
devices.forEach(dev => {
const deviceData = rawData
.filter(item => item[lineKey] === dev)
.sort((a, b) => a.timestamp - b.timestamp);
if (deviceData.length > 0) {
logs.push(deviceData[0]); // Первая точка
for (let i = 1; i < deviceData.length; i++) {
if (deviceData[i].status !== deviceData[i - 1].status) {
logs.push(deviceData[i]);
}
}
}
});
setStatusLogs(logs.sort((a, b) => b.timestamp - a.timestamp));
}
}, [rawData, lineKey]);
useEffect(() => {
let unsubscribe;
if (mode === 'realtime') {
unsubscribe = startRealtimeUpdates();
} else {
stopRealtimeUpdates();
fetchHistoricalData(startDate, endDate);
}
return () => {
if (unsubscribe) unsubscribe();
stopRealtimeUpdates();
};
}, [mode, metricName, device, source_id]);
const metaInfo = [
metricMeta.instance && `Instance: ${metricMeta.instance}`,
metricMeta.job && `Job: ${metricMeta.job}`,
metricMeta.type && `Type: ${metricMeta.type}`
].filter(Boolean).join(' | ');
return (
<div style={{ position: 'relative' }}>
<div style={{ marginBottom: 16 }}>
<Radio.Group
value={mode}
onChange={(e) => setMode(e.target.value)}
buttonStyle="solid"
style={{ marginBottom: 10 }}
>
<Radio.Button value="realtime">Режим реального времени</Radio.Button>
<Radio.Button value="historical">Исторические данные</Radio.Button>
</Radio.Group>
{mode === 'historical' && (
<DateRangeSelector
startDate={startDate}
endDate={endDate}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
onApply={handleCustomRangeApply}
/>
)}
{mode === 'realtime' && (
<Tag color={isLiveUpdating ? 'green' : 'red'}>
{isLiveUpdating ? 'Обновление в реальном времени' : 'Режим реального времени остановлен'}
</Tag>
)}
</div>
{device && <Tag color="geekblue">Устройство: {device}</Tag>}
{source_id && <Tag color="purple">Модуль: {source_id.split('$')[1]}</Tag>}
<Box position="relative">
<MuiTooltip title={showLogs ? "Скрыть логи" : "Показать логи"}>
<IconButton
onClick={() => setShowLogs(!showLogs)}
sx={{
position: 'absolute',
right: 16,
top: 16,
zIndex: 1000,
bgcolor: 'background.paper',
boxShadow: 1
}}
>
<ListAlt />
</IconButton>
</MuiTooltip>
{isLoading ? (
<div style={{ height: chartHeight, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Spin size="large" tip="Загрузка данных..." />
</div>
) : error ? (
<div style={{ color: 'red', padding: 20 }}>Ошибка: {error}</div>
) : rawData.length === 0 ? (
<div style={{ padding: 20 }}>Нет данных для метрики: {metricName}</div>
) : (
<>
<LineChartComponent
data={rawData}
title={title}
description={description}
multipleLines={multipleLines}
lineKey={lineKey}
metaInfo={metaInfo}
height={chartHeight}
ranges={ranges}
/>
{showLogs && <StatusLogTable logs={statusLogs} />}
</>
)}
</Box>
</div>
);
};
export default SystemChart;