diff --git a/src/Charts/Components/ConnectionStatusIndicator.jsx b/src/Charts/Components/ConnectionStatusIndicator.jsx new file mode 100644 index 0000000..941fed2 --- /dev/null +++ b/src/Charts/Components/ConnectionStatusIndicator.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +export const ConnectionStatusIndicator = ({ connectionStatus }) => { + return ( +
+ {connectionStatus === 'connected' ? 'Online' : + connectionStatus === 'error' ? 'Connection Error' : 'Offline'} +
+ ); +}; \ No newline at end of file diff --git a/src/Charts/Components/CurrentRangeDisplay.jsx b/src/Charts/Components/CurrentRangeDisplay.jsx new file mode 100644 index 0000000..41cfb4c --- /dev/null +++ b/src/Charts/Components/CurrentRangeDisplay.jsx @@ -0,0 +1,17 @@ +import React from 'react'; + +export const CurrentRangeDisplay = ({ useCustomRange, startDate, endDate, selectedRange }) => { + return ( +
+ Текущий диапазон: {useCustomRange + ? `${startDate.toLocaleString()} - ${endDate.toLocaleString()}` + : selectedRange.label} +
+ ); +}; \ No newline at end of file diff --git a/src/Charts/Components/LineChartComponent.jsx b/src/Charts/Components/LineChartComponent.jsx index 898e12f..53d859f 100755 --- a/src/Charts/Components/LineChartComponent.jsx +++ b/src/Charts/Components/LineChartComponent.jsx @@ -1,7 +1,13 @@ import React, { useState, useRef, useEffect } from 'react'; import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Line, ResponsiveContainer, ReferenceArea } from 'recharts'; -const LineChartComponent = ({ chartData, metricName, colors, description, onRangeSelect, filteredData }) => { +const LineChartComponent = ({ + chartData, + metricName, + colors, + onRangeSelect, + filteredData +}) => { const [selectionArea, setSelectionArea] = useState(null); const [isSelecting, setIsSelecting] = useState(false); const chartRef = useRef(null); @@ -19,11 +25,14 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang point[key] = instanceData ? instanceData.value : null; }); return point; + }).sort((a, b) => { + const timeA = chartData[Object.keys(chartData)[0]].find(d => d.time === a.time)?.timestamp; + const timeB = chartData[Object.keys(chartData)[0]].find(d => d.time === b.time)?.timestamp; + return timeA - timeB; }); const displayData = filteredData || data; - // Блокировка выделения текста при перетаскивании useEffect(() => { const handleSelectStart = (e) => { if (isSelecting) { @@ -57,10 +66,12 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang const startIndex = data.findIndex(point => point.time === selectionArea.start); const endIndex = data.findIndex(point => point.time === selectionArea.end); - onRangeSelect({ - startIndex: Math.min(startIndex, endIndex), - endIndex: Math.max(startIndex, endIndex) - }); + if (startIndex >= 0 && endIndex >= 0) { + onRangeSelect({ + startIndex: Math.min(startIndex, endIndex), + endIndex: Math.max(startIndex, endIndex) + }); + } setSelectionArea(null); }; @@ -68,17 +79,17 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang const CustomTooltip = ({ active, payload, label }) => { if (active && payload && payload.length) { return ( -
-

{`Время: ${label}`}

+

{`${label}`}

{payload.map((item, index) => (

- {`${item.value}`} + {`Значение: ${item.value}`}

))}
@@ -87,9 +98,13 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang return null; }; + if (!data.length) { + return
Нет данных для отображения
; + } + return (
@@ -102,20 +117,7 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang `} -
-
- - + - - } - cursor={{ stroke: '#ccc', strokeWidth: 1 }} - /> - {Object.keys(chartData).map((key, index) => ( + + } /> + {Object.keys(chartData).map((instance, index) => ( ))} - {selectionArea?.start && selectionArea?.end && ( { + return ( +
+ {/* Стандартные диапазоны */} +
+
+ + +
+ + +
+ + {/* Кастомный диапазон */} +
+
+ Или укажите свой диапазон: +
+
+
+ setStartDate(date)} + showTimeSelect + timeFormat="HH:mm" + timeIntervals={15} + dateFormat="yyyy-MM-dd HH:mm" + placeholderText="Начальная дата" + customInput={ + + } + /> +
+
+ setEndDate(date)} + showTimeSelect + timeFormat="HH:mm" + timeIntervals={15} + dateFormat="yyyy-MM-dd HH:mm" + placeholderText="Конечная дата" + customInput={ + + } + /> +
+ +
+
+
+ ); +}; \ No newline at end of file diff --git a/src/Charts/Components/constants.jsx b/src/Charts/Components/constants.jsx new file mode 100644 index 0000000..68f92d9 --- /dev/null +++ b/src/Charts/Components/constants.jsx @@ -0,0 +1,20 @@ +export const TIME_RANGES = [ + { label: '1 минута', value: 60, interval: 3000 }, + { label: '5 минут', value: 300, interval: 15000 }, + { label: '30 минут', value: 1800, interval: 90000 }, + { label: '1 час', value: 3600, interval: 180000 }, + { label: '3 часа', value: 10800, interval: 540000 }, + { label: '6 часов', value: 21600, interval: 1080000 }, + { label: '12 часов', value: 43200, interval: 2160000 }, + { label: '24 часа', value: 86400, interval: 4320000 }, + { label: '2 дня', value: 172800, interval: 8640000 }, + { label: '7 дней', value: 604800, interval: 30240000 }, + { label: '30 дней', value: 2592000, interval: 129600000 }, + { label: '90 дней', value: 7776000, interval: 388800000 }, + { label: '6 месяцев', value: 15552000, interval: 777600000 }, + { label: '9 месяцев', value: 23328000, interval: 1166400000 }, + { label: '1 год', value: 31536000, interval: 1576800000 }, +]; + +export const COLORS = ['#3e95cd', '#8e5ea2', '#3cba9f', '#e8c3b9', '#c45850']; +export const MAX_POINTS = 20; \ No newline at end of file diff --git a/src/Charts/PrometheusChart.jsx b/src/Charts/PrometheusChart.jsx index cd537da..632767d 100755 --- a/src/Charts/PrometheusChart.jsx +++ b/src/Charts/PrometheusChart.jsx @@ -1,81 +1,102 @@ -import React, { useEffect, useState, useRef } from 'react'; -import DatePicker from 'react-datepicker'; -import 'react-datepicker/dist/react-datepicker.css'; -import LineChartComponent from './Components/LineChartComponent'; +import React, { useEffect, useState, useRef, useCallback } from 'react'; import { io } from 'socket.io-client'; - -const MAX_POINTS = 20; -const COLORS = ['#3e95cd', '#8e5ea2', '#3cba9f', '#e8c3b9', '#c45850']; -const TIME_RANGES = [ - { label: '1 минута', value: 60, interval: 3000 }, - { label: '5 минут', value: 300, interval: 15000 }, - { label: '30 минут', value: 1800, interval: 90000 }, - { label: '1 час', value: 3600, interval: 180000 }, - { label: '3 часа', value: 10800, interval: 540000 }, - { label: '6 часов', value: 21600, interval: 1080000 }, - { label: '12 часов', value: 43200, interval: 2160000 }, - { label: '24 часа', value: 86400, interval: 4320000 }, - { label: '2 дня', value: 172800, interval: 8640000 }, - { label: '7 дней', value: 604800, interval: 30240000 }, - { label: '30 дней', value: 2592000, interval: 129600000 }, - { label: '90 дней', value: 7776000, interval: 388800000 }, - { label: '6 месяцев', value: 15552000, interval: 777600000 }, - { label: '9 месяцев', value: 23328000, interval: 1166400000 }, - { label: '1 год', value: 31536000, interval: 1576800000 }, -]; +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'; const PrometheusChart = ({ metricName }) => { - const [chartData, setChartData] = useState({}); + 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 [connectionStatus, setConnectionStatus] = useState('disconnected'); const intervalRef = useRef(null); const socketRef = useRef(null); - const setupWebSocket = () => { + const formatTime = useCallback((timestamp, rangeSeconds) => { + const date = new Date(timestamp); + if (rangeSeconds > 86400) { + return { + display: date.toLocaleString([], { + day: '2-digit', + month: '2-digit', + year: '2-digit', + hour: '2-digit', + minute: '2-digit' + }), + timestamp: timestamp + }; + } + return { + display: date.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }), + timestamp: timestamp + }; + }, []); + + const setupWebSocket = useCallback(() => { + if (socketRef.current?.connected) return socketRef.current; + + if (socketRef.current) socketRef.current.disconnect(); + const socket = io('http://192.168.2.39:3000/metrics-ws', { - path: '/socket.io', transports: ['websocket'], reconnection: true, - reconnectionAttempts: 5, + reconnectionAttempts: Infinity, reconnectionDelay: 1000, + reconnectionDelayMax: 5000, }); socketRef.current = socket; socket.on('connect', () => { + console.log('WebSocket connected'); setConnectionStatus('connected'); fetchData(); }); - socket.on('disconnect', () => { + socket.on('disconnect', (reason) => { + console.log('WebSocket disconnected:', reason); setConnectionStatus('disconnected'); + if (reason === 'io server disconnect') socket.connect(); }); - socket.on('connect_error', (err) => { + 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); }); - return socket; - }; + socket.on('metrics-error', (error) => { + console.error('Metrics error:', error); + setConnectionStatus('error'); + }); - const calculateStep = (start, end) => { + return socket; + }, []); + + const calculateStep = useCallback((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 = () => { + const fetchData = useCallback(() => { try { const now = Math.floor(Date.now() / 1000); let start = useCustomRange @@ -85,13 +106,13 @@ const PrometheusChart = ({ metricName }) => { ? Math.floor(endDate.getTime() / 1000) : now; - // Проверка на корректность диапазона if (start >= end) { console.error('Invalid time range: start >= end'); return; } const step = calculateStep(start, end); + console.log(`Fetching data for ${metricName}`, { start, end, step }); if (socketRef.current?.connected) { socketRef.current.emit('get-metrics', { @@ -102,219 +123,163 @@ const PrometheusChart = ({ metricName }) => { }); } else { console.error('WebSocket is not connected'); + setupWebSocket(); } } catch (error) { console.error('Error in fetchData:', error); } - }; + }, [metricName, selectedRange, useCustomRange, startDate, endDate, calculateStep, setupWebSocket]); - const processMetricsData = (response) => { + const processMetricsData = useCallback((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'); + if (!Array.isArray(data)) { + console.error('Invalid data format:', 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; - } + console.log('Processing metrics data:', data); - const step = calculateStep(start, end); - const range = end - start; + const now = Math.floor(Date.now() / 1000); + const rangeSeconds = useCustomRange + ? Math.floor(endDate.getTime() / 1000) - Math.floor(startDate.getTime() / 1000) + : selectedRange.value; - // 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); - } + const instancesData = {}; - // 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' }); + data.forEach(item => { + const instance = item.instance || 'default'; + const timestamp = item.timestamp; + const value = parseFloat(item.value); - 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; - - const interpolatedData = []; - for (let i = 0; i < data.length - 1; i++) { - interpolatedData.push(data[i]); - - 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]; - const nextValue = nextPoint[key]; - interpolatedPoint[key] = currentValue + ((nextValue - currentValue) * j) / (steps + 1); - } - }); - - interpolatedData.push(interpolatedPoint); + if (!instancesData[instance]) { + instancesData[instance] = []; } - } - interpolatedData.push(data[data.length - 1]); - return interpolatedData.slice(0, minPoints); - }; + const formatted = formatTime(timestamp, rangeSeconds); + instancesData[instance].push({ + time: formatted.display, // для отображения + value: value, + timestamp: timestamp, // для сортировки + originalTime: formatted // для группировки + }); + }); - const handleRangeChange = (event) => { + // Группируем точки с одинаковым временем (в пределах секунды) + Object.keys(instancesData).forEach(instance => { + const grouped = []; + const timeMap = {}; + + instancesData[instance].forEach(point => { + const timeKey = Math.floor(point.timestamp / 1000); // группируем по секундам + if (!timeMap[timeKey]) { + timeMap[timeKey] = { + ...point, + count: 1 + }; + grouped.push(timeMap[timeKey]); + } else { + // Усредняем значения для точек в одну секунду + timeMap[timeKey].value = (timeMap[timeKey].value * timeMap[timeKey].count + point.value) / + (timeMap[timeKey].count + 1); + timeMap[timeKey].count += 1; + } + }); + + instancesData[instance] = grouped.sort((a, b) => a.timestamp - b.timestamp); + }); + + console.log('Processed chart data:', instancesData); + setChartData(instancesData); + setSelectedGraphRange(null); + setFilteredData(null); + }, [metricName, formatTime, useCustomRange, startDate, endDate, selectedRange.value]); + const handleRangeChange = useCallback((event) => { const selectedValue = event.target.value; const range = TIME_RANGES.find(r => r.value === parseInt(selectedValue, 10)); - // Сбрасываем данные и состояние - setChartData({}); - setFilteredData(null); - setSelectedGraphRange(null); - - // Обновляем диапазон и даты setSelectedRange(range); setUseCustomRange(false); + setChartData(null); + setSelectedGraphRange(null); + setFilteredData(null); 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); - + const handleCustomRangeChange = useCallback(() => { setUseCustomRange(true); + setChartData(null); + setSelectedGraphRange(null); + setFilteredData(null); + }, []); - // Принудительно запрашиваем новые данные - setTimeout(() => { - fetchData(); - }, 0); - }; - - const handleResetZoom = () => { + const handleResetZoom = useCallback(() => { 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]); + }, [setupWebSocket]); useEffect(() => { - // При изменении диапазона или дат перезапускаем интервал - if (socketRef.current?.connected) { - clearInterval(intervalRef.current); + if (!socketRef.current?.connected) return; + + clearInterval(intervalRef.current); + fetchData(); + + intervalRef.current = setInterval(() => { fetchData(); + }, selectedRange.interval); - intervalRef.current = setInterval(() => { - fetchData(); - }, selectedRange.interval); - } - }, [selectedRange, useCustomRange, startDate, endDate, metricName]); + return () => clearInterval(intervalRef.current); + }, [fetchData, selectedRange.interval]); useEffect(() => { - if (selectedGraphRange) { - const { startIndex, endIndex } = selectedGraphRange; - const allTimes = Object.values(chartData) - .flat() - .map(point => point.time) - .filter((time, index, self) => self.indexOf(time) === index); - - const data = allTimes.map(time => { - const point = { time }; - Object.keys(chartData).forEach(key => { - const instanceData = chartData[key].find(p => p.time === time); - point[key] = instanceData ? instanceData.value : null; - }); - return point; - }); - - const filtered = data.slice(startIndex, endIndex + 1); - const interpolated = interpolateData(filtered, 15); - setFilteredData(interpolated); - } else { + if (!chartData || !selectedGraphRange) { setFilteredData(null); + return; } + + const { startIndex, endIndex } = selectedGraphRange; + const allTimes = Object.values(chartData) + .flat() + .map(point => point.time) + .filter((time, index, self) => self.indexOf(time) === index); + + const data = allTimes.map(time => { + const point = { time }; + Object.keys(chartData).forEach(key => { + const instanceData = chartData[key].find(p => p.time === time); + point[key] = instanceData ? instanceData.value : null; + }); + return point; + }).sort((a, b) => { + const timeA = chartData[Object.keys(chartData)[0]].find(d => d.time === a.time)?.timestamp; + const timeB = chartData[Object.keys(chartData)[0]].find(d => d.time === b.time)?.timestamp; + return timeA - timeB; + }); + + const filtered = data.slice(startIndex, endIndex + 1); + setFilteredData(filtered); }, [selectedGraphRange, chartData]); - if (!Object.keys(chartData).length) return

Loading...

; + if (chartData === null) { + return
Loading data...
; + } - const allTimes = Object.values(chartData) - .flat() - .map(point => point.time) - .filter((time, index, self) => self.indexOf(time) === index); - - const data = allTimes.map(time => { - const point = { time }; - Object.keys(chartData).forEach(key => { - const instanceData = chartData[key].find(p => p.time === time); - point[key] = instanceData ? instanceData.value : null; - }); - return point; - }); + if (Object.keys(chartData).length === 0) { + return
No data available
; + } return (
{ marginBottom: '20px', position: 'relative' }}> -
- {connectionStatus === 'connected' ? 'Online' : - connectionStatus === 'error' ? 'Connection Error' : 'Offline'} -
- {/* Заголовок графика */} -

-

+ - {/* Группа элементов управления */} -
- {/* Стандартные диапазоны */} -
-
- - -
+ - {/* Кнопка сброса */} - -
+ - - - {/* Кастомный диапазон */} -
-
- Или укажите свой диапазон: -
-
-
- setStartDate(date)} - showTimeSelect - timeFormat="HH:mm" - timeIntervals={15} - dateFormat="yyyy-MM-dd HH:mm" - placeholderText="Начальная дата" - customInput={ - - } - /> -
-
- setEndDate(date)} - showTimeSelect - timeFormat="HH:mm" - timeIntervals={15} - dateFormat="yyyy-MM-dd HH:mm" - placeholderText="Конечная дата" - customInput={ - - } - /> -
- -
-
-
- - {/* Индикатор текущего диапазона */} -
- Текущий диапазон: {useCustomRange - ? `${startDate.toLocaleString()} - ${endDate.toLocaleString()}` - : selectedRange.label} -
- - {/* График */} @@ -505,4 +321,4 @@ const PrometheusChart = ({ metricName }) => { ); }; -export default PrometheusChart; \ No newline at end of file +export default React.memo(PrometheusChart); \ No newline at end of file