diff --git a/src/Charts/LineChartComponent.jsx b/src/Charts/LineChartComponent.jsx index ee9a4e3..88e0e29 100644 --- a/src/Charts/LineChartComponent.jsx +++ b/src/Charts/LineChartComponent.jsx @@ -16,13 +16,13 @@ const formatXAxis = (tickItem) => { }; const formatTooltip = (value, name, props) => { - return [`${value.toFixed(2)}`, ` ${name}`]; + return [`${value.toFixed(2)}`, `Устройство ${name}`]; }; const LineChartComponent = ({ data = [], - multipleLines = false, - lineKey = 'device', + multipleLines = true, // По умолчанию включаем множественные линии + lineKey = 'device', // Ключ для разделения линий title, description, height = 400, @@ -30,27 +30,25 @@ const LineChartComponent = ({ }) => { if (!data || data.length === 0) return
Нет данных для отображения
; - // Создаем массив уникальных линий - const lineKeys = [...new Set(data.map(item => item[lineKey] || 'default'))]; + // Создаем массив уникальных устройств + const devices = [...new Set(data.map(item => item.device))]; - // Преобразуем данные в формат, удобный для Recharts - const chartData = data.reduce((acc, item) => { - const timestamp = item.timestamp; - const existingPoint = acc.find(p => p.timestamp === timestamp); + // Группируем данные по timestamp для правильного отображения + const timestamps = [...new Set(data.map(item => item.timestamp))].sort(); - if (existingPoint) { - return acc.map(p => - p.timestamp === timestamp - ? { ...p, [item[lineKey] || 'default']: item.value } - : p + const chartData = timestamps.map(timestamp => { + const point = { timestamp }; + + // Для каждого устройства находим значение в этот timestamp + devices.forEach(device => { + const deviceData = data.find(item => + item.timestamp === timestamp && item.device === device ); - } + point[`device_${device}`] = deviceData ? deviceData.value : null; + }); - return [...acc, { - timestamp, - [item[lineKey] || 'default']: item.value - }]; - }, []).sort((a, b) => a.timestamp - b.timestamp); + return point; + }); return (
@@ -63,39 +61,27 @@ const LineChartComponent = ({ dataKey="timestamp" tickFormatter={formatXAxis} /> - + format(new Date(label), 'yyyy-MM-dd HH:mm:ss')} /> - {multipleLines ? ( - lineKeys.map(key => ( - - )) - ) : ( + {devices.map(device => ( - )} + ))} {/* Добавляем диапазоны если они есть */} {ranges.map((range, idx) => ( diff --git a/src/Charts/SystemChart.jsx b/src/Charts/SystemChart.jsx index 478d7cb..decba5b 100644 --- a/src/Charts/SystemChart.jsx +++ b/src/Charts/SystemChart.jsx @@ -1,8 +1,8 @@ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect } 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 { Button, Radio, message, Tag } from 'antd'; import moment from 'moment'; import StatusLogTable from '../Charts2/Components/StatusLogTable'; import { Box, IconButton, Tooltip as MuiTooltip } from '@mui/material'; @@ -15,14 +15,12 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => { title = metricName, description, context = {}, - ranges = [], - multipleLines = false, - lineKey = 'device' + ranges = [] } = metricInfo || {}; const { device, source_id } = context; - const [rawData, setRawData] = useState([]); + const [chartData, setChartData] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [metricMeta, setMetricMeta] = useState({}); @@ -32,43 +30,96 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => { const [isLiveUpdating, setIsLiveUpdating] = useState(false); const [showLogs, setShowLogs] = useState(false); const [statusLogs, setStatusLogs] = useState([]); - - const MAX_POINTS = 1000; + const MAX_POINTS = 50; const TIME_WINDOW_MS = 3600 * 1000; - const subscriptionKey = useMemo(() => { + + // Эта функция может больше не понадобиться, так как + // сервис сам генерирует ключи, но оставьте для совместимости + const getSubscriptionKey = () => { 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 = (responseData) => { - const dataArray = Array.isArray(responseData) ? responseData : responseData.data; + const getStatusFromRanges = (value, ranges) => { + if (!ranges || ranges.length === 0) return 1; + for (const r of ranges) { + if (value >= r.min && value <= r.max) { + return r.status; + } + } + return 1; + }; + const formatMetricData = (dataArray) => { if (!Array.isArray(dataArray)) { - console.error('Expected array but got:', responseData); + console.error('Expected array in formatMetricData, got:', typeof dataArray); return []; } - 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' - })); + return dataArray.map(item => { + if (item.timestamp === undefined || item.value === undefined) { + console.warn('Invalid metric item:', item); + return null; + } + + return { + ...item, + timestamp: Number(item.timestamp), + value: parseFloat(item.value), + status: getStatusFromRanges(parseFloat(item.value), ranges), + name: item.__name__ || metricName, + device: item.device?.trim() || null, + source_id: item.source_id || null, + description: item.description || description + }; + }).filter(Boolean) + .sort((a, b) => a.timestamp - b.timestamp); }; - const calculateStep = (start, end) => { - const duration = end.getTime() - start.getTime(); - return Math.max(Math.floor(duration / (MAX_POINTS * 1000)), 1); + const calculateStep = (startTime, endTime, maxPoints = 10000) => { + const durationSeconds = (endTime.getTime() - startTime.getTime()) / 1000; + return Math.max(Math.ceil(durationSeconds / maxPoints), 1); }; + const downsampleData = (data, maxPoints = MAX_POINTS) => { + if (data.length <= maxPoints) return [...data]; + + const sortedData = [...data].sort((a, b) => a.timestamp - b.timestamp); + const step = Math.max(1, Math.floor(sortedData.length / maxPoints)); + + const result = []; + for (let i = 0; i < sortedData.length; i += step) { + if (result.length >= maxPoints) break; + result.push(sortedData[i]); + } + + if (result.length > 0) { + const lastOriginalPoint = sortedData[sortedData.length - 1]; + if (result[result.length - 1].timestamp !== lastOriginalPoint.timestamp) { + result[result.length - 1] = lastOriginalPoint; + } + } + + return result; + }; + + + useEffect(() => { + if (chartData.length > 0) { + const newLogs = chartData.reduce((acc, point, index) => { + if (index === 0 || point.status !== chartData[index - 1].status) { + return [...acc, point]; + } + return acc; + }, []); + setStatusLogs(newLogs); + } + }, [chartData]); + + const fetchHistoricalData = async (start, end) => { setIsLoading(true); setError(null); @@ -82,19 +133,23 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => { const step = calculateStep(start, end); + // Используем новый метод для исторических данных const data = await metricsService.fetchMetricsRange( metricName, - Math.floor(start.getTime() / 1000), - Math.floor(end.getTime() / 1000), + start.getTime(), // Теперь передаем timestamp в миллисекундах + end.getTime(), step, extendedFilters ); - const responseData = Array.isArray(data) ? data : data.data; - const formattedData = formatMetricData(responseData); - setRawData(formattedData); + const formattedData = formatMetricData(data) + .sort((a, b) => a.timestamp - b.timestamp); - if (formattedData.length > 0) { + const limitedData = formattedData.length > MAX_POINTS + ? downsampleData(formattedData, MAX_POINTS) + : formattedData; + + if (limitedData.length > 0) { setMetricMeta({ type: data[0]?.type, description: data[0]?.description || description, @@ -102,6 +157,8 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => { job: data[0]?.job }); } + + setChartData(limitedData); } catch (err) { console.error(`Error loading historical data for ${metricName}:`, err); setError(err.message); @@ -117,42 +174,55 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => { const end = new Date(); const start = new Date(end.getTime() - TIME_WINDOW_MS); - const cutoffTime = Date.now() - TIME_WINDOW_MS; + fetchHistoricalData(start, end).finally(() => setIsLoading(false)); + // Изменяем параметры подписки return metricsService.subscribeToMetric( - subscriptionKey, - (newData) => { - setRawData(prev => { - const actualData = Array.isArray(newData) ? newData : newData.data; - const formattedNewData = formatMetricData(actualData) + metricName, // Теперь передаем просто имя метрики + { ...filters, device, source_id }, // Фильры отдельным параметром + (update) => { // Колбэк получает объект с данными + console.log('Received WS update:', update); + + if (!update || !Array.isArray(update.data)) { + console.error('Invalid update format:', update); + return; + } + + setChartData(prev => { + const now = Date.now(); + const cutoffTime = now - TIME_WINDOW_MS; + + const formattedNew = formatMetricData(update.data) .filter(point => point.timestamp >= cutoffTime); - const filteredPrev = prev.filter(point => point.timestamp >= cutoffTime); + const filteredPrev = prev.filter(point => + point.timestamp >= cutoffTime + ); - const merged = [...filteredPrev, ...formattedNewData] + const merged = [...filteredPrev, ...formattedNew] .filter((v, i, a) => - a.findIndex(t => - t.timestamp === v.timestamp && - t[lineKey] === v[lineKey] - ) === i - ); + a.findIndex(t => t.timestamp === v.timestamp) === i + ) + .sort((a, b) => a.timestamp - b.timestamp); - return merged; + return merged.length > MAX_POINTS + ? merged.slice(-MAX_POINTS) + : merged; }); }, - 5000, - { - ...filters, - ...(device && { device }), - ...(source_id && { source_id }) - } + 5000 // Интервал обновления (можно настроить) ); }; + const stopRealtimeUpdates = () => { setIsLiveUpdating(false); - metricsService.unsubscribeFromMetric(subscriptionKey); + // Теперь отписываемся по метрике и фильтрам + metricsService.unsubscribeFromMetric( + metricName, + { ...filters, device, source_id } + ); }; const handleCustomRangeApply = () => { @@ -162,45 +232,29 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => { }; useEffect(() => { - if (rawData.length > 0) { - const logs = []; - const devices = [...new Set(rawData.map(item => item[lineKey]))]; + console.log('Metric changed:', { metricName, device, source_id, filters }); - 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); - } + const init = async () => { + if (mode === 'realtime') { + unsubscribe = startRealtimeUpdates(); + } else { + await fetchHistoricalData(startDate, endDate); + } + }; + + init(); return () => { - if (unsubscribe) unsubscribe(); - stopRealtimeUpdates(); + if (unsubscribe) { + unsubscribe(); // Вызываем функцию отписки + } + if (mode === 'realtime') { + stopRealtimeUpdates(); // Дополнительная очистка + } }; - }, [mode, metricName, device, source_id]); + }, [mode, metricName, device, source_id, JSON.stringify(filters)]); // Добавляем JSON.stringify для фильтров const metaInfo = [ metricMeta.instance && `Instance: ${metricMeta.instance}`, @@ -209,7 +263,7 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => { ].filter(Boolean).join(' | '); return ( -
+
{ /> )} - {mode === 'realtime' && ( - - {isLiveUpdating ? 'Обновление в реальном времени' : 'Режим реального времени остановлен'} - + {mode === 'realtime' && isLiveUpdating && ( + )}
@@ -259,26 +318,28 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => { {isLoading ? ( -
- -
+
Загрузка графика...
) : error ? ( -
Ошибка: {error}
- ) : rawData.length === 0 ? ( -
Нет данных для метрики: {metricName}
+
Ошибка: {error}
+ ) : chartData.length === 0 ? ( +
Нет данных для метрики: {metricName}
) : ( <> - {showLogs && } + {showLogs && ( + + )} )} @@ -286,4 +347,4 @@ const SystemChart = ({ metricInfo, chartHeight = 580 }) => { ); }; -export default SystemChart; \ No newline at end of file +export default SystemChart; diff --git a/src/Charts2/PrometheusChart.jsx b/src/Charts2/PrometheusChart.jsx index 8240209..4ff71de 100644 --- a/src/Charts2/PrometheusChart.jsx +++ b/src/Charts2/PrometheusChart.jsx @@ -43,6 +43,16 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => { return `${metricName}${filterParts.length ? `?${filterParts.join('&')}` : ''}`; }; + const getStatusFromRanges = (value, ranges) => { + if (!ranges || ranges.length === 0) return 1; + for (const r of ranges) { + if (value >= r.min && value <= r.max) { + return r.status; + } + } + return 1; + }; + const formatMetricData = (dataArray) => { if (!Array.isArray(dataArray)) { console.error('Expected array in formatMetricData, got:', typeof dataArray); @@ -59,7 +69,7 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => { ...item, timestamp: Number(item.timestamp), value: parseFloat(item.value), - status: parseInt(item.status || '0'), + status: getStatusFromRanges(parseFloat(item.value), ranges), name: item.__name__ || metricName, device: item.device?.trim() || null, source_id: item.source_id || null,