283 lines
10 KiB
JavaScript
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; |