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 (
setMode(e.target.value)} buttonStyle="solid" style={{ marginBottom: 10 }} > Режим реального времени Исторические данные {mode === 'historical' && ( )} {mode === 'realtime' && ( {isLiveUpdating ? 'Обновление в реальном времени' : 'Режим реального времени остановлен'} )}
{device && Устройство: {device}} {source_id && Модуль: {source_id.split('$')[1]}} setShowLogs(!showLogs)} sx={{ position: 'absolute', right: 16, top: 16, zIndex: 1000, bgcolor: 'background.paper', boxShadow: 1 }} > {isLoading ? (
) : error ? (
Ошибка: {error}
) : rawData.length === 0 ? (
Нет данных для метрики: {metricName}
) : ( <> {showLogs && } )}
); }; export default SystemChart;