diff --git a/package.json b/package.json index b7873e5..74c5e26 100755 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "@mui/icons-material": "^6.4.8", "reactflow": "^11.11.4", "vite-plugin-svgr": "^4.3.0", - "react-scripts": "^5.0.1" + "react-scripts": "^5.0.1", + "socket.io-client": "^4.8.1" }, "devDependencies": { "@eslint/js": "^9.17.0", diff --git a/src/Charts/PrometheusChart.jsx b/src/Charts/PrometheusChart.jsx index 5fb0b22..cd537da 100755 --- a/src/Charts/PrometheusChart.jsx +++ b/src/Charts/PrometheusChart.jsx @@ -1,8 +1,8 @@ import React, { useEffect, useState, useRef } from 'react'; -import axios from 'axios'; import DatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; import LineChartComponent from './Components/LineChartComponent'; +import { io } from 'socket.io-client'; const MAX_POINTS = 20; const COLORS = ['#3e95cd', '#8e5ea2', '#3cba9f', '#e8c3b9', '#c45850']; @@ -30,11 +30,142 @@ const PrometheusChart = ({ metricName }) => { const [startDate, setStartDate] = useState(new Date()); const [endDate, setEndDate] = useState(new Date()); const [useCustomRange, setUseCustomRange] = useState(false); - const [selectedGraphRange, setSelectedGraphRange] = useState(null); // Выбранный диапазон - const [filteredData, setFilteredData] = useState(null); // Отфильтрованные данные + const [selectedGraphRange, setSelectedGraphRange] = useState(null); + const [filteredData, setFilteredData] = useState(null); + const [connectionStatus, setConnectionStatus] = useState('disconnected'); const intervalRef = useRef(null); + const socketRef = useRef(null); + + const setupWebSocket = () => { + const socket = io('http://192.168.2.39:3000/metrics-ws', { + path: '/socket.io', + transports: ['websocket'], + reconnection: true, + reconnectionAttempts: 5, + reconnectionDelay: 1000, + }); + + socketRef.current = socket; + + socket.on('connect', () => { + setConnectionStatus('connected'); + fetchData(); + }); + + socket.on('disconnect', () => { + setConnectionStatus('disconnected'); + }); + + socket.on('connect_error', (err) => { + setConnectionStatus('error'); + }); + + socket.on('metrics-data', (response) => { + processMetricsData(response); + }); + + return socket; + }; + + const calculateStep = (start, end) => { + const range = end - start; + if (range <= 3600) return 5; + if (range <= 21600) return 30; + if (range <= 86400) return 120; + return 300; + }; + + const fetchData = () => { + try { + const now = Math.floor(Date.now() / 1000); + let start = useCustomRange + ? Math.floor(startDate.getTime() / 1000) + : now - selectedRange.value; + let end = useCustomRange + ? Math.floor(endDate.getTime() / 1000) + : now; + + // Проверка на корректность диапазона + if (start >= end) { + console.error('Invalid time range: start >= end'); + return; + } + + const step = calculateStep(start, end); + + if (socketRef.current?.connected) { + socketRef.current.emit('get-metrics', { + metric: metricName, + start, + end, + step + }); + } else { + console.error('WebSocket is not connected'); + } + } catch (error) { + console.error('Error in fetchData:', error); + } + }; + + const processMetricsData = (response) => { + const { metric, data } = response; + if (metric !== metricName) return; + + let metrics = Array.isArray(data) ? data : []; + let start, end; + + if (metrics.length === 0) { + console.warn('Received empty metrics data'); + return; + } + + if (useCustomRange) { + start = Math.floor(startDate.getTime() / 1000); + end = Math.floor(endDate.getTime() / 1000); + } else { + end = Math.floor(Date.now() / 1000); + start = end - selectedRange.value; + } + + const step = calculateStep(start, end); + const range = end - start; + + // 1. Генерируем ВСЕ ожидаемые временные точки + const timePoints = []; + for (let t = start; t <= end; t += step) { + const date = new Date(t * 1000); + const formattedTime = range > 86400 + ? date.toLocaleString([], { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) + : date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); + timePoints.push(formattedTime); + } + + // 2. Создаем карту "время -> значение" для каждого инстанса + const timeValueMap = {}; + metrics.forEach(m => { + const date = new Date(m.timestamp); + const formattedTime = range > 86400 + ? date.toLocaleString([], { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) + : date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); + + const key = m.instance; + if (!timeValueMap[key]) timeValueMap[key] = {}; + timeValueMap[key][formattedTime] = m.value; + }); + + // 3. Строим финальные данные, гарантируя все временные точки + const newChartData = {}; + Object.keys(timeValueMap).forEach(instance => { + newChartData[instance] = timePoints.map(time => ({ + time, + value: timeValueMap[instance][time] ?? null // null если данных нет + })); + }); + + setChartData({ ...newChartData }); // Форсируем обновление + }; - // Функция для интерполяции данных const interpolateData = (data, minPoints = 15) => { if (data.length >= minPoints) return data; @@ -44,19 +175,15 @@ const PrometheusChart = ({ metricName }) => { const currentPoint = data[i]; const nextPoint = data[i + 1]; - - // Вычисляем разницу во времени между точками const currentTime = new Date(currentPoint.time).getTime(); const nextTime = new Date(nextPoint.time).getTime(); const timeDiff = nextTime - currentTime; - // Добавляем промежуточные точки const steps = Math.ceil((minPoints - data.length) / (data.length - 1)); for (let j = 1; j <= steps; j++) { const interpolatedTime = new Date(currentTime + (timeDiff * j) / (steps + 1)).toLocaleString(); const interpolatedPoint = { time: interpolatedTime }; - // Интерполируем значения для каждой метрики Object.keys(currentPoint).forEach(key => { if (key !== 'time') { const currentValue = currentPoint[key]; @@ -69,122 +196,85 @@ const PrometheusChart = ({ metricName }) => { } } - interpolatedData.push(data[data.length - 1]); // Добавляем последнюю точку - return interpolatedData.slice(0, minPoints); // Обрезаем до minPoints + interpolatedData.push(data[data.length - 1]); + return interpolatedData.slice(0, minPoints); }; - const fetchData = async () => { - try { - let start, end; - - if (useCustomRange) { - start = Math.floor(startDate.getTime() / 1000); - end = Math.floor(endDate.getTime() / 1000); - } else { - end = Math.floor(Date.now() / 1000); - start = end - selectedRange.value; - } - - let step; - const range = end - start; - if (range <= 3600) step = 5; - else if (range <= 21600) step = 30; - else if (range <= 86400) step = 120; - else step = 300; - - const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/metrics`, { - params: { - metric: metricName, - start, - end, - step - } - }); - - const result = response.data; - let metrics = Array.isArray(result) ? result : result.data || []; - - if (!Array.isArray(metrics)) { - metrics = []; - } - - const timePoints = []; - for (let t = start; t <= end; t += step) { - const date = new Date(t * 1000); - const formattedTime = range > 86400 - ? date.toLocaleString([], { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) - : date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); - - timePoints.push(formattedTime); - } - - const updatedData = {}; - metrics.forEach(m => { - const date = new Date(m.timestamp); - const formattedTime = range > 86400 - ? date.toLocaleString([], { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) - : date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); - - const key = m.instance; - if (!updatedData[key]) updatedData[key] = {}; - updatedData[key][formattedTime] = m.value; - }); - - const chartData = {}; - Object.keys(updatedData).forEach(key => { - chartData[key] = timePoints.map(time => ({ - time, - value: updatedData[key][time] ?? null, - })); - }); - - setChartData(chartData); - } catch (error) { - console.error('Ошибка при загрузке метрик:', error); - } - }; - - useEffect(() => { - fetchData(); - - intervalRef.current = setInterval(() => { - fetchData(); - }, selectedRange.interval); - - return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - } - }; - }, [metricName, selectedRange, useCustomRange, startDate, endDate]); - const handleRangeChange = (event) => { const selectedValue = event.target.value; - const range = TIME_RANGES.find(range => range.value === parseInt(selectedValue, 10)); + const range = TIME_RANGES.find(r => r.value === parseInt(selectedValue, 10)); - // Принудительно сбрасываем состояние - setSelectedGraphRange(null); + // Сбрасываем данные и состояние + setChartData({}); setFilteredData(null); + setSelectedGraphRange(null); - // Обновляем диапазон - setSelectedRange({ ...range }); // Создаем новый объект, чтобы React увидел изменение + // Обновляем диапазон и даты + setSelectedRange(range); setUseCustomRange(false); + const now = new Date(); + setEndDate(now); + setStartDate(new Date(now.getTime() - range.value * 1000)); + // Принудительно запрашиваем новые данные + // Используем setTimeout для гарантированного обновления состояния перед запросом + setTimeout(() => { + fetchData(); + }, 0); }; const handleCustomRangeChange = () => { + // Сбрасываем данные и состояние + setChartData({}); + setFilteredData(null); + setSelectedGraphRange(null); + setUseCustomRange(true); - setSelectedGraphRange(null); // Сбрасываем выбранный диапазон - setFilteredData(null); // Сбрасываем отфильтрованные данные + + // Принудительно запрашиваем новые данные + setTimeout(() => { + fetchData(); + }, 0); }; const handleResetZoom = () => { setSelectedGraphRange(null); setFilteredData(null); - fetchData(); // Принудительно обновляем данные + fetchData(); }; + useEffect(() => { + const socket = setupWebSocket(); + + // Первоначальная загрузка данных + fetchData(); + + // Настраиваем интервал обновления + const intervalId = setInterval(() => { + fetchData(); + }, selectedRange.interval); + + intervalRef.current = intervalId; + + return () => { + clearInterval(intervalRef.current); + socket.disconnect(); + }; + }, [metricName, selectedRange.value]); + + useEffect(() => { + // При изменении диапазона или дат перезапускаем интервал + if (socketRef.current?.connected) { + clearInterval(intervalRef.current); + fetchData(); + + intervalRef.current = setInterval(() => { + fetchData(); + }, selectedRange.interval); + } + }, [selectedRange, useCustomRange, startDate, endDate, metricName]); + useEffect(() => { if (selectedGraphRange) { const { startIndex, endIndex } = selectedGraphRange; @@ -203,12 +293,10 @@ const PrometheusChart = ({ metricName }) => { }); const filtered = data.slice(startIndex, endIndex + 1); - - // Интерполируем данные, если точек меньше 15 const interpolated = interpolateData(filtered, 15); - setFilteredData(interpolated); // Сохраняем интерполированные данные + setFilteredData(interpolated); } else { - setFilteredData(null); // Сбрасываем фильтрацию, если диапазон не выбран + setFilteredData(null); } }, [selectedGraphRange, chartData]); @@ -233,8 +321,23 @@ const PrometheusChart = ({ metricName }) => { backgroundColor: '#fff', borderRadius: '8px', padding: '20px', - marginBottom: '20px' + marginBottom: '20px', + position: 'relative' }}> +
+ {connectionStatus === 'connected' ? 'Online' : + connectionStatus === 'error' ? 'Connection Error' : 'Offline'} +
{/* Заголовок графика */}

diff --git a/src/Style/SidebarMenu.css b/src/Style/SidebarMenu.css deleted file mode 100755 index e69de29..0000000