refactoring, fixed bugs with the web socket
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details

pull/29/head
DmitriyA 2025-04-02 19:50:08 -04:00
parent 4405c693aa
commit 32ece2f0ff
6 changed files with 424 additions and 407 deletions

View File

@ -0,0 +1,20 @@
import React from 'react';
export const ConnectionStatusIndicator = ({ connectionStatus }) => {
return (
<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>
);
};

View File

@ -0,0 +1,17 @@
import React from 'react';
export const CurrentRangeDisplay = ({ useCustomRange, startDate, endDate, selectedRange }) => {
return (
<div style={{
margin: '10px 0',
padding: '8px 12px',
backgroundColor: '#f0f7ff',
borderRadius: '4px',
borderLeft: '3px solid #4a6baf'
}}>
Текущий диапазон: {useCustomRange
? `${startDate.toLocaleString()} - ${endDate.toLocaleString()}`
: selectedRange.label}
</div>
);
};

View File

@ -1,7 +1,13 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Line, ResponsiveContainer, ReferenceArea } from 'recharts'; 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 [selectionArea, setSelectionArea] = useState(null);
const [isSelecting, setIsSelecting] = useState(false); const [isSelecting, setIsSelecting] = useState(false);
const chartRef = useRef(null); const chartRef = useRef(null);
@ -19,11 +25,14 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
point[key] = instanceData ? instanceData.value : null; point[key] = instanceData ? instanceData.value : null;
}); });
return point; 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; const displayData = filteredData || data;
// Блокировка выделения текста при перетаскивании
useEffect(() => { useEffect(() => {
const handleSelectStart = (e) => { const handleSelectStart = (e) => {
if (isSelecting) { if (isSelecting) {
@ -57,10 +66,12 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
const startIndex = data.findIndex(point => point.time === selectionArea.start); const startIndex = data.findIndex(point => point.time === selectionArea.start);
const endIndex = data.findIndex(point => point.time === selectionArea.end); const endIndex = data.findIndex(point => point.time === selectionArea.end);
onRangeSelect({ if (startIndex >= 0 && endIndex >= 0) {
startIndex: Math.min(startIndex, endIndex), onRangeSelect({
endIndex: Math.max(startIndex, endIndex) startIndex: Math.min(startIndex, endIndex),
}); endIndex: Math.max(startIndex, endIndex)
});
}
setSelectionArea(null); setSelectionArea(null);
}; };
@ -68,17 +79,17 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
const CustomTooltip = ({ active, payload, label }) => { const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) { if (active && payload && payload.length) {
return ( return (
<div className="custom-tooltip" style={{ <div style={{
backgroundColor: '#fff', backgroundColor: '#fff',
padding: '10px', padding: '10px',
border: '1px solid #ccc', border: '1px solid #ccc',
borderRadius: '4px', borderRadius: '4px',
boxShadow: '0 2px 5px rgba(0,0,0,0.1)' boxShadow: '0 2px 5px rgba(0,0,0,0.1)'
}}> }}>
<p style={{ fontWeight: 'bold', marginBottom: '5px' }}>{`Время: ${label}`}</p> <p style={{ fontWeight: 'bold', marginBottom: '5px' }}>{`${label}`}</p>
{payload.map((item, index) => ( {payload.map((item, index) => (
<p key={index} style={{ color: item.color }}> <p key={index} style={{ color: item.color }}>
{`${item.value}`} {`Значение: ${item.value}`}
</p> </p>
))} ))}
</div> </div>
@ -87,9 +98,13 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
return null; return null;
}; };
if (!data.length) {
return <div style={{ padding: '20px', textAlign: 'center' }}>Нет данных для отображения</div>;
}
return ( return (
<div <div
style={{ position: 'relative' }} style={{ position: 'relative', height: '400px' }}
ref={containerRef} ref={containerRef}
className={isSelecting ? 'no-selection' : ''} className={isSelecting ? 'no-selection' : ''}
> >
@ -102,20 +117,7 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
`} `}
</style> </style>
<div style={{ <ResponsiveContainer width="100%" height="100%">
position: 'absolute',
top: 10,
left: 10,
backgroundColor: 'rgba(255,255,255,0.8)',
padding: '5px 10px',
borderRadius: 4,
fontSize: 12,
zIndex: 10,
pointerEvents: 'none'
}}>
</div>
<ResponsiveContainer width="100%" height={400}>
<LineChart <LineChart
data={displayData} data={displayData}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
@ -127,30 +129,21 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" /> <CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
<XAxis <XAxis
dataKey="time" dataKey="time"
tick={{ fill: '#666' }} tick={{ fontSize: 12 }}
tickMargin={10}
/> />
<YAxis <YAxis tick={{ fontSize: 12 }} />
tick={{ fill: '#666' }} <Tooltip content={<CustomTooltip />} />
tickMargin={10} {Object.keys(chartData).map((instance, index) => (
/>
<Tooltip
content={<CustomTooltip />}
cursor={{ stroke: '#ccc', strokeWidth: 1 }}
/>
{Object.keys(chartData).map((key, index) => (
<Line <Line
key={key} key={instance}
type="monotone" type="monotone"
dataKey={key} dataKey={instance}
stroke={colors[index % colors.length]} stroke={colors[index % colors.length]}
strokeWidth={2} strokeWidth={2}
dot={false} dot={false}
activeDot={{ r: 6 }} activeDot={{ r: 6 }}
name={key}
/> />
))} ))}
{selectionArea?.start && selectionArea?.end && ( {selectionArea?.start && selectionArea?.end && (
<ReferenceArea <ReferenceArea
x1={selectionArea.start} x1={selectionArea.start}
@ -165,4 +158,4 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
); );
}; };
export default LineChartComponent; export default React.memo(LineChartComponent);

View File

@ -0,0 +1,151 @@
import React from 'react';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import { TIME_RANGES } from './constants';
export const TimeRangeSelector = ({
selectedRange,
handleRangeChange,
startDate,
setStartDate,
endDate,
setEndDate,
useCustomRange,
handleCustomRangeChange,
handleResetZoom
}) => {
return (
<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>
);
};

View File

@ -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;

View File

@ -1,81 +1,102 @@
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef, useCallback } 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'; import { io } from 'socket.io-client';
import LineChartComponent from './Components/LineChartComponent';
const MAX_POINTS = 20; import { TimeRangeSelector } from './Components/TimeRangeSelector';
const COLORS = ['#3e95cd', '#8e5ea2', '#3cba9f', '#e8c3b9', '#c45850']; import { ConnectionStatusIndicator } from './Components/ConnectionStatusIndicator';
const TIME_RANGES = [ import { CurrentRangeDisplay } from './Components/CurrentRangeDisplay';
{ label: '1 минута', value: 60, interval: 3000 }, import { TIME_RANGES, COLORS } from './Components/constants';
{ 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 PrometheusChart = ({ metricName }) => {
const [chartData, setChartData] = useState({}); const [chartData, setChartData] = useState(null);
const [selectedRange, setSelectedRange] = useState(TIME_RANGES[0]); const [selectedRange, setSelectedRange] = useState(TIME_RANGES[0]);
const [startDate, setStartDate] = useState(new Date()); const [startDate, setStartDate] = useState(new Date());
const [endDate, setEndDate] = useState(new Date()); const [endDate, setEndDate] = useState(new Date());
const [useCustomRange, setUseCustomRange] = useState(false); const [useCustomRange, setUseCustomRange] = useState(false);
const [connectionStatus, setConnectionStatus] = useState('disconnected');
const [selectedGraphRange, setSelectedGraphRange] = useState(null); const [selectedGraphRange, setSelectedGraphRange] = useState(null);
const [filteredData, setFilteredData] = useState(null); const [filteredData, setFilteredData] = useState(null);
const [connectionStatus, setConnectionStatus] = useState('disconnected');
const intervalRef = useRef(null); const intervalRef = useRef(null);
const socketRef = 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', { const socket = io('http://192.168.2.39:3000/metrics-ws', {
path: '/socket.io',
transports: ['websocket'], transports: ['websocket'],
reconnection: true, reconnection: true,
reconnectionAttempts: 5, reconnectionAttempts: Infinity,
reconnectionDelay: 1000, reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
}); });
socketRef.current = socket; socketRef.current = socket;
socket.on('connect', () => { socket.on('connect', () => {
console.log('WebSocket connected');
setConnectionStatus('connected'); setConnectionStatus('connected');
fetchData(); fetchData();
}); });
socket.on('disconnect', () => { socket.on('disconnect', (reason) => {
console.log('WebSocket disconnected:', reason);
setConnectionStatus('disconnected'); 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'); setConnectionStatus('error');
setTimeout(() => socket.connect(), 1000);
}); });
socket.on('metrics-data', (response) => { socket.on('metrics-data', (response) => {
console.log('Received raw metrics data:', response);
processMetricsData(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; const range = end - start;
if (range <= 3600) return 5; if (range <= 3600) return 5;
if (range <= 21600) return 30; if (range <= 21600) return 30;
if (range <= 86400) return 120; if (range <= 86400) return 120;
return 300; return 300;
}; }, []);
const fetchData = () => { const fetchData = useCallback(() => {
try { try {
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
let start = useCustomRange let start = useCustomRange
@ -85,13 +106,13 @@ const PrometheusChart = ({ metricName }) => {
? Math.floor(endDate.getTime() / 1000) ? Math.floor(endDate.getTime() / 1000)
: now; : now;
// Проверка на корректность диапазона
if (start >= end) { if (start >= end) {
console.error('Invalid time range: start >= end'); console.error('Invalid time range: start >= end');
return; return;
} }
const step = calculateStep(start, end); const step = calculateStep(start, end);
console.log(`Fetching data for ${metricName}`, { start, end, step });
if (socketRef.current?.connected) { if (socketRef.current?.connected) {
socketRef.current.emit('get-metrics', { socketRef.current.emit('get-metrics', {
@ -102,219 +123,163 @@ const PrometheusChart = ({ metricName }) => {
}); });
} else { } else {
console.error('WebSocket is not connected'); console.error('WebSocket is not connected');
setupWebSocket();
} }
} catch (error) { } catch (error) {
console.error('Error in fetchData:', 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; const { metric, data } = response;
if (metric !== metricName) return; if (metric !== metricName) return;
let metrics = Array.isArray(data) ? data : []; if (!Array.isArray(data)) {
let start, end; console.error('Invalid data format:', data);
if (metrics.length === 0) {
console.warn('Received empty metrics data');
return; return;
} }
if (useCustomRange) { console.log('Processing metrics data:', data);
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 now = Math.floor(Date.now() / 1000);
const range = end - start; const rangeSeconds = useCustomRange
? Math.floor(endDate.getTime() / 1000) - Math.floor(startDate.getTime() / 1000)
: selectedRange.value;
// 1. Генерируем ВСЕ ожидаемые временные точки const instancesData = {};
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. Создаем карту "время -> значение" для каждого инстанса data.forEach(item => {
const timeValueMap = {}; const instance = item.instance || 'default';
metrics.forEach(m => { const timestamp = item.timestamp;
const date = new Date(m.timestamp); const value = parseFloat(item.value);
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 (!instancesData[instance]) {
if (!timeValueMap[key]) timeValueMap[key] = {}; instancesData[instance] = [];
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]); const formatted = formatTime(timestamp, rangeSeconds);
return interpolatedData.slice(0, minPoints); 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 selectedValue = event.target.value;
const range = TIME_RANGES.find(r => r.value === parseInt(selectedValue, 10)); const range = TIME_RANGES.find(r => r.value === parseInt(selectedValue, 10));
// Сбрасываем данные и состояние
setChartData({});
setFilteredData(null);
setSelectedGraphRange(null);
// Обновляем диапазон и даты
setSelectedRange(range); setSelectedRange(range);
setUseCustomRange(false); setUseCustomRange(false);
setChartData(null);
setSelectedGraphRange(null);
setFilteredData(null);
const now = new Date(); const now = new Date();
setEndDate(now); setEndDate(now);
setStartDate(new Date(now.getTime() - range.value * 1000)); setStartDate(new Date(now.getTime() - range.value * 1000));
}, []);
// Принудительно запрашиваем новые данные const handleCustomRangeChange = useCallback(() => {
// Используем setTimeout для гарантированного обновления состояния перед запросом
setTimeout(() => {
fetchData();
}, 0);
};
const handleCustomRangeChange = () => {
// Сбрасываем данные и состояние
setChartData({});
setFilteredData(null);
setSelectedGraphRange(null);
setUseCustomRange(true); setUseCustomRange(true);
setChartData(null);
setSelectedGraphRange(null);
setFilteredData(null);
}, []);
// Принудительно запрашиваем новые данные const handleResetZoom = useCallback(() => {
setTimeout(() => {
fetchData();
}, 0);
};
const handleResetZoom = () => {
setSelectedGraphRange(null); setSelectedGraphRange(null);
setFilteredData(null); setFilteredData(null);
fetchData(); fetchData();
}; }, [fetchData]);
useEffect(() => { useEffect(() => {
const socket = setupWebSocket(); const socket = setupWebSocket();
// Первоначальная загрузка данных
fetchData();
// Настраиваем интервал обновления
const intervalId = setInterval(() => {
fetchData();
}, selectedRange.interval);
intervalRef.current = intervalId;
return () => { return () => {
clearInterval(intervalRef.current); clearInterval(intervalRef.current);
socket.disconnect(); socket.disconnect();
}; };
}, [metricName, selectedRange.value]); }, [setupWebSocket]);
useEffect(() => { useEffect(() => {
// При изменении диапазона или дат перезапускаем интервал if (!socketRef.current?.connected) return;
if (socketRef.current?.connected) {
clearInterval(intervalRef.current); clearInterval(intervalRef.current);
fetchData();
intervalRef.current = setInterval(() => {
fetchData(); fetchData();
}, selectedRange.interval);
intervalRef.current = setInterval(() => { return () => clearInterval(intervalRef.current);
fetchData(); }, [fetchData, selectedRange.interval]);
}, selectedRange.interval);
}
}, [selectedRange, useCustomRange, startDate, endDate, metricName]);
useEffect(() => { useEffect(() => {
if (selectedGraphRange) { if (!chartData || !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); 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]); }, [selectedGraphRange, chartData]);
if (!Object.keys(chartData).length) return <p>Loading...</p>; if (chartData === null) {
return <div style={{ padding: '20px', textAlign: 'center' }}>Loading data...</div>;
}
const allTimes = Object.values(chartData) if (Object.keys(chartData).length === 0) {
.flat() return <div style={{ padding: '20px', textAlign: 'center' }}>No data available</div>;
.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 ( return (
<div style={{ <div style={{
@ -324,180 +289,31 @@ const PrometheusChart = ({ metricName }) => {
marginBottom: '20px', marginBottom: '20px',
position: 'relative' position: 'relative'
}}> }}>
<div style={{ <ConnectionStatusIndicator connectionStatus={connectionStatus} />
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>
{/* Группа элементов управления */} <TimeRangeSelector
<div style={{ selectedRange={selectedRange}
display: 'flex', handleRangeChange={handleRangeChange}
flexWrap: 'wrap', startDate={startDate}
gap: '15px', setStartDate={setStartDate}
alignItems: 'center', endDate={endDate}
marginBottom: '15px' setEndDate={setEndDate}
}}> useCustomRange={useCustomRange}
{/* Стандартные диапазоны */} handleCustomRangeChange={handleCustomRangeChange}
<div style={{ flex: '1 1 200px', display: 'flex', gap: '10px', alignItems: 'flex-end' }}> handleResetZoom={handleResetZoom}
<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>
{/* Кнопка сброса */} <CurrentRangeDisplay
<button useCustomRange={useCustomRange}
onClick={handleResetZoom} startDate={startDate}
style={{ endDate={endDate}
padding: '8px 16px', selectedRange={selectedRange}
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 <LineChartComponent
chartData={chartData} chartData={chartData}
metricName={metricName} metricName={metricName}
colors={COLORS} colors={COLORS}
description={metricName}
onRangeSelect={setSelectedGraphRange} onRangeSelect={setSelectedGraphRange}
filteredData={filteredData} filteredData={filteredData}
/> />
@ -505,4 +321,4 @@ const PrometheusChart = ({ metricName }) => {
); );
}; };
export default PrometheusChart; export default React.memo(PrometheusChart);