import React, { useEffect, useState, useRef, useCallback } from 'react'; import { io } from 'socket.io-client'; import LineChartComponent from './Components/LineChartComponent'; import { TimeRangeSelector } from './Components/TimeRangeSelector'; import { ConnectionStatusIndicator } from './Components/ConnectionStatusIndicator'; import { CurrentRangeDisplay } from './Components/CurrentRangeDisplay'; import { TIME_RANGES, COLORS } from './Components/constants'; import axios from 'axios'; const PrometheusChart = ({ metricName }) => { const [chartData, setChartData] = useState(null); const [selectedRange, setSelectedRange] = useState(TIME_RANGES[0]); const [startDate, setStartDate] = useState(new Date()); const [endDate, setEndDate] = useState(new Date()); const [useCustomRange, setUseCustomRange] = useState(false); const [connectionStatus, setConnectionStatus] = useState('disconnected'); const [selectedGraphRange, setSelectedGraphRange] = useState(null); const [filteredData, setFilteredData] = useState(null); const [isSelectingRange, setIsSelectingRange] = useState(false); const [lastCustomRange, setLastCustomRange] = useState(null); const intervalRef = useRef(null); const socketRef = useRef(null); const debounceRef = useRef(null); const formatTime = useCallback((timestamp, rangeSeconds) => { const ts = typeof timestamp === 'number' ? timestamp : Date.now(); const date = new Date(ts); // Определяем формат в зависимости от диапазона const showFullDate = rangeSeconds > 86400; // больше суток const timeOptions = { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }; const dateOptions = showFullDate ? { month: '2-digit', day: '2-digit', ...timeOptions } : timeOptions; return { display: date.toLocaleString('ru-RU', dateOptions), fullDisplay: date.toLocaleString('ru-RU', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }), timestamp: ts }; }, []); const calculateStep = useCallback((start, end) => { const range = end - start; if (range <= 60) return 1; // 1 мин if (range <= 300) return 5; // 5 мин if (range <= 1800) return 15; // 30 мин if (range <= 3600) return 30; // 1 час if (range <= 10800) return 60; // 3 часа if (range <= 21600) return 120; // 6 часов if (range <= 43200) return 300; // 12 часов if (range <= 86400) return 600; // 24 часа return 1800; // > 24 часов }, []); const fetchData = useCallback(() => { if (isSelectingRange) return; const now = Math.floor(Date.now() / 1000); const start = now - selectedRange.value; const end = now; const step = calculateStep(start, end); if (socketRef.current?.connected) { socketRef.current.emit('get-metrics', { metric: metricName, start, end, step, _t: Date.now() }); } }, [metricName, selectedRange.value, isSelectingRange]); const processMetricsData = useCallback((response) => { console.log('Processing metrics data:', response); if (response.metric !== metricName) return; const dataArray = Array.isArray(response.data) ? response.data : [response.data]; if (!dataArray.length) return; setChartData(prev => { const newData = { ...(prev || {}) }; const rangeSeconds = useCustomRange ? (endDate.getTime() - startDate.getTime()) / 1000 : selectedRange.value; dataArray.forEach(item => { const instance = item.instance || 'default'; if (!newData[instance]) newData[instance] = []; // Унифицированная конвертация timestamp let timestamp; if (typeof item.timestamp === 'number') { // Определяем, в секундах или миллисекундах пришел timestamp timestamp = item.timestamp > 1e12 ? item.timestamp : item.timestamp * 1000; } else { timestamp = Date.now(); } const value = parseFloat(item.value); const formattedTime = formatTime(timestamp, rangeSeconds); newData[instance].push({ time: formattedTime.display, fullTime: formattedTime.fullDisplay, value: value, timestamp: timestamp }); }); // Сортируем и ограничиваем данные Object.keys(newData).forEach(instance => { newData[instance] = newData[instance] .sort((a, b) => a.timestamp - b.timestamp) .slice(-1000); }); return newData; }); }, [metricName, selectedRange.value, formatTime, useCustomRange, startDate, endDate]); const setupWebSocket = useCallback(() => { if (socketRef.current) { // Если соединение уже существует, возвращаем его if (socketRef.current.connected) return socketRef.current; // Если соединение в процессе переподключения, тоже возвращаем if (socketRef.current.reconnecting) return socketRef.current; } const socket = io('http://192.168.2.39:3000/api/metrics-ws', { transports: ['websocket'], reconnection: true, reconnectionAttempts: Infinity, reconnectionDelay: 1000, reconnectionDelayMax: 5000, }); socketRef.current = socket; socket.on('connect', () => { console.log('WebSocket connected'); setConnectionStatus('connected'); fetchData(); }); socket.on('disconnect', (reason) => { console.log('WebSocket disconnected:', reason); setConnectionStatus('disconnected'); if (reason === 'io server disconnect') socket.connect(); }); socket.on('connect_error', (error) => { console.error('WebSocket connection error:', error); setConnectionStatus('error'); setTimeout(() => socket.connect(), 1000); }); socket.on('metrics-data', (response) => { console.log('Received raw metrics data:', response); processMetricsData(response); }); socket.on('metrics-error', (error) => { console.error('Metrics error:', error); setConnectionStatus('error'); }); return socket; }, []); const fetchCustomRangeData = useCallback(async () => { const start = Math.floor(startDate.getTime() / 1000); const end = Math.floor(endDate.getTime() / 1000); const rangeSeconds = end - start; try { const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/metrics`, { params: { metric: metricName, start, end, step: calculateStep(start, end) } }); if (response.data?.length) { // Преобразуем данные перед передачей в processMetricsData const processedData = response.data.map(item => ({ ...item, timestamp: item.timestamp, // оставляем в секундах - processMetricsData конвертирует value: item.value.toString() })); processMetricsData({ metric: metricName, data: processedData }); } } catch (error) { console.error('Ошибка при получении кастомных данных:', error); } }, [metricName, startDate, endDate, calculateStep, processMetricsData]); const handleRangeChange = useCallback((event) => { // Очищаем текущий интервал if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } const selectedValue = event.target.value; const range = TIME_RANGES.find(r => r.value === parseInt(selectedValue, 10)); setSelectedRange(range); setUseCustomRange(false); setChartData(null); setSelectedGraphRange(null); setFilteredData(null); const now = new Date(); setEndDate(now); setStartDate(new Date(now.getTime() - range.value * 1000)); // Переподключение сокета if (!socketRef.current?.connected) { socketRef.current?.connect(); } }, []); const handleCustomRangeChange = useCallback(() => { // Отключаем WebSocket соединение if (socketRef.current?.connected) { socketRef.current.disconnect(); setConnectionStatus('disconnected'); } // Очищаем интервал обновления if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } setUseCustomRange(true); setChartData(null); setSelectedGraphRange(null); setFilteredData(null); fetchCustomRangeData(); }, [fetchCustomRangeData]); const handleResetZoom = useCallback(() => { setSelectedGraphRange(null); setFilteredData(null); setIsSelectingRange(false); if (useCustomRange) { fetchCustomRangeData(); } else { if (!socketRef.current?.connected) { socketRef.current?.connect(); } fetchData(); } if (lastCustomRange) { handleRangeSelect(lastCustomRange); return; } }, [fetchData, fetchCustomRangeData, useCustomRange]); const interpolateData = useCallback((data, targetPointCount) => { if (!data || data.length < 2) return data; if (data.length >= targetPointCount) return data; const interpolated = []; const step = (data.length - 1) / (targetPointCount - 1); for (let i = 0; i < targetPointCount; i++) { const index = i * step; const lowerIndex = Math.floor(index); const upperIndex = Math.ceil(index); if (lowerIndex === upperIndex) { interpolated.push(data[lowerIndex]); continue; } const fraction = index - lowerIndex; const interpolatedPoint = {}; Object.keys(data[lowerIndex]).forEach(key => { if (key === 'timestamp') { interpolatedPoint[key] = data[lowerIndex][key] + fraction * (data[upperIndex][key] - data[lowerIndex][key]); // Добавляем отображаемое время const { display, fullDisplay } = formatTime(interpolatedPoint[key], (endDate - startDate) / 1000); interpolatedPoint.time = display; interpolatedPoint.fullTime = fullDisplay; } else if (typeof data[lowerIndex][key] === 'number') { interpolatedPoint[key] = data[lowerIndex][key] + fraction * (data[upperIndex][key] - data[lowerIndex][key]); } else { interpolatedPoint[key] = data[lowerIndex][key]; } }); interpolated.push(interpolatedPoint); } return interpolated; }, []); const handleRangeSelect = useCallback((range) => { setLastCustomRange(range); if (!range || !chartData) return; setIsSelectingRange(true); setSelectedGraphRange(range); // Отключаем автоматические обновления if (socketRef.current?.connected) { socketRef.current.disconnect(); } if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } // Получаем все точки и сортируем по времени const allPoints = Object.values(chartData).flat(); const sortedPoints = allPoints.sort((a, b) => a.timestamp - b.timestamp); // Вычисляем абсолютные индексы const startIndex = Math.floor(range.startIndex * (sortedPoints.length - 1)); const endIndex = Math.floor(range.endIndex * (sortedPoints.length - 1)); // Фильтруем точки по выбранному диапазону const filtered = sortedPoints.slice(startIndex, endIndex + 1); // Применяем интерполяцию только если точек меньше 100 const interpolated = filtered.length < 100 ? interpolateData(filtered, Math.min(100, filtered.length * 3)) : filtered; setFilteredData(interpolated); setIsSelectingRange(false); }, [chartData, interpolateData, formatTime]); useEffect(() => { const socket = setupWebSocket(); return () => { clearInterval(intervalRef.current); socket.disconnect(); }; }, [setupWebSocket]); // Обновим useEffect для кастомного диапазона useEffect(() => { if (useCustomRange && !isSelectingRange) { // Очищаем предыдущий таймер if (debounceRef.current) { clearTimeout(debounceRef.current); } // Устанавливаем новый таймер с задержкой 500 мс debounceRef.current = setTimeout(() => { fetchCustomRangeData(); }, 500); } return () => { if (debounceRef.current) { clearTimeout(debounceRef.current); } }; }, [useCustomRange, isSelectingRange, startDate, endDate, fetchCustomRangeData]); useEffect(() => { if (useCustomRange || isSelectingRange) return; const fetchDataWrapper = () => { try { fetchData(); } catch (error) { console.error('Error in interval fetch:', error); } }; // Очищаем предыдущий интервал if (intervalRef.current) { clearInterval(intervalRef.current); } // Запускаем сразу и затем по интервалу fetchDataWrapper(); intervalRef.current = setInterval(fetchDataWrapper, selectedRange.interval); return () => { if (intervalRef.current) { clearInterval(intervalRef.current); } }; }, [fetchData, selectedRange.interval, useCustomRange, isSelectingRange]); useEffect(() => { if (!selectedGraphRange || !chartData) { setFilteredData(null); return; } const allPoints = Object.values(chartData).flat(); const sortedPoints = allPoints.sort((a, b) => a.timestamp - b.timestamp); const startIndex = Math.floor(selectedGraphRange.startIndex * (sortedPoints.length - 1)); const endIndex = Math.floor(selectedGraphRange.endIndex * (sortedPoints.length - 1)); const filtered = sortedPoints.slice(startIndex, endIndex + 1); const interpolated = filtered.length > 100 ? interpolateData(filtered, 100) : filtered; setFilteredData(interpolated); }, [selectedGraphRange, chartData, interpolateData]); if (chartData === null) { return