+ {/* Стандартные диапазоны */}
+
+
+ Стандартные диапазоны:
+
+ {TIME_RANGES.map(range => (
+ {range.label}
+ ))}
+
+
+
+
e.target.style.backgroundColor = '#e0e0e0'}
+ onMouseOut={(e) => e.target.style.backgroundColor = '#f0f0f0'}
+ >
+ Сбросить
+
+
+
+ {/* Кастомный диапазон */}
+
+
+ Или укажите свой диапазон:
+
+
+
+ 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={
+
+ }
+ />
+
+
e.target.style.backgroundColor = '#3a5a9f'}
+ onMouseOut={(e) => e.target.style.backgroundColor = '#4a6baf'}
+ >
+ Применить
+
+
+
+
+ );
+};
\ 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 {
marginBottom: '20px',
position: 'relative'
}}>
-
- {connectionStatus === 'connected' ? 'Online' :
- connectionStatus === 'error' ? 'Connection Error' : 'Offline'}
-
- {/* Заголовок графика */}
-
-
+
- {/* Группа элементов управления */}
-
- {/* Стандартные диапазоны */}
-
-
- Стандартные диапазоны:
-
- {TIME_RANGES.map(range => (
- {range.label}
- ))}
-
-
+
- {/* Кнопка сброса */}
-
e.target.style.backgroundColor = '#e0e0e0'}
- onMouseOut={(e) => e.target.style.backgroundColor = '#f0f0f0'}
- >
- Сбросить
-
-
+
-
-
- {/* Кастомный диапазон */}
-
-
- Или укажите свой диапазон:
-
-
-
- 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={
-
- }
- />
-
-
e.target.style.backgroundColor = '#3a5a9f'}
- onMouseOut={(e) => e.target.style.backgroundColor = '#4a6baf'}
- >
- Применить
-
-
-
-
-
- {/* Индикатор текущего диапазона */}
-
- Текущий диапазон: {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