trust-module-frontend/src/Charts/PrometheusChart.jsx

508 lines
20 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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 { 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 },
];
const PrometheusChart = ({ metricName }) => {
const [chartData, setChartData] = useState({});
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 [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;
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);
}
}
interpolatedData.push(data[data.length - 1]);
return interpolatedData.slice(0, minPoints);
};
const handleRangeChange = (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);
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);
// Принудительно запрашиваем новые данные
setTimeout(() => {
fetchData();
}, 0);
};
const handleResetZoom = () => {
setSelectedGraphRange(null);
setFilteredData(null);
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;
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 {
setFilteredData(null);
}
}, [selectedGraphRange, chartData]);
if (!Object.keys(chartData).length) return <p>Loading...</p>;
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;
});
return (
<div style={{
backgroundColor: '#fff',
borderRadius: '8px',
padding: '20px',
marginBottom: '20px',
position: 'relative'
}}>
<div style={{
position: 'absolute',
top: '10px',
right: '10px',
padding: '5px 10px',
borderRadius: '4px',
backgroundColor: connectionStatus === 'connected' ? '#4CAF50' :
connectionStatus === 'error' ? '#F44336' : '#FFC107',
color: 'white',
fontSize: '12px'
}}>
{connectionStatus === 'connected' ? 'Online' :
connectionStatus === 'error' ? 'Connection Error' : 'Offline'}
</div>
{/* Заголовок графика */}
<h3 style={{ marginTop: 0, color: '#333' }}>
</h3>
{/* Группа элементов управления */}
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: '15px',
alignItems: 'center',
marginBottom: '15px'
}}>
{/* Стандартные диапазоны */}
<div style={{ flex: '1 1 200px', display: 'flex', gap: '10px', alignItems: 'flex-end' }}>
<div style={{ flex: '1' }}>
<label htmlFor="time-range" style={{
display: 'block',
marginBottom: '5px',
fontWeight: '500',
color: '#555'
}}>Стандартные диапазоны:</label>
<select
id="time-range"
value={selectedRange.value}
onChange={handleRangeChange}
style={{
width: '100%',
padding: '8px 12px',
borderRadius: '4px',
border: '1px solid #ddd',
color: "#333",
backgroundColor: '#f9f9f9'
}}
>
{TIME_RANGES.map(range => (
<option key={range.value} value={range.value}>{range.label}</option>
))}
</select>
</div>
{/* Кнопка сброса */}
<button
onClick={handleResetZoom}
style={{
padding: '8px 16px',
backgroundColor: '#f0f0f0',
color: '#333',
border: '1px solid #ddd',
borderRadius: '4px',
cursor: 'pointer',
transition: 'all 0.2s',
height: '36px',
whiteSpace: 'nowrap'
}}
onMouseOver={(e) => e.target.style.backgroundColor = '#e0e0e0'}
onMouseOut={(e) => e.target.style.backgroundColor = '#f0f0f0'}
>
Сбросить
</button>
</div>
{/* Кастомный диапазон */}
<div style={{ flex: '1 1 300px' }}>
<div style={{
marginBottom: '10px',
fontWeight: '500',
color: '#555'
}}>
Или укажите свой диапазон:
</div>
<div style={{
display: 'flex',
gap: '10px',
flexWrap: 'wrap'
}}>
<div style={{ flex: '1 1 200px' }}>
<DatePicker
selected={startDate}
onChange={(date) => setStartDate(date)}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="yyyy-MM-dd HH:mm"
placeholderText="Начальная дата"
customInput={
<input style={{
backgroundColor: '#f9f9f9',
color: "#555",
width: '100%',
padding: '8px 12px',
borderRadius: '4px',
border: '1px solid #ddd'
}} />
}
/>
</div>
<div style={{ flex: '1 1 200px' }}>
<DatePicker
selected={endDate}
onChange={(date) => setEndDate(date)}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="yyyy-MM-dd HH:mm"
placeholderText="Конечная дата"
customInput={
<input style={{
backgroundColor: '#f9f9f9',
color: "#555",
width: '100%',
padding: '8px 12px',
borderRadius: '4px',
border: '1px solid #ddd'
}} />
}
/>
</div>
<button
onClick={handleCustomRangeChange}
style={{
padding: '8px 16px',
backgroundColor: '#4a6baf',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
transition: 'background-color 0.2s',
flex: '0 0 auto',
alignSelf: 'flex-end'
}}
onMouseOver={(e) => e.target.style.backgroundColor = '#3a5a9f'}
onMouseOut={(e) => e.target.style.backgroundColor = '#4a6baf'}
>
Применить
</button>
</div>
</div>
</div>
{/* Индикатор текущего диапазона */}
<div style={{
margin: '10px 0',
padding: '8px 12px',
backgroundColor: '#f0f7ff',
borderRadius: '4px',
borderLeft: '3px solid #4a6baf'
}}>
Текущий диапазон: {useCustomRange
? `${startDate.toLocaleString()} - ${endDate.toLocaleString()}`
: selectedRange.label}
</div >
{/* График */}
<LineChartComponent
chartData={chartData}
metricName={metricName}
colors={COLORS}
description={metricName}
onRangeSelect={setSelectedGraphRange}
filteredData={filteredData}
/>
</div>
);
};
export default PrometheusChart;