refactoring, fixed bugs with the web socket
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good
Details
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good
Details
parent
4405c693aa
commit
32ece2f0ff
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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);
|
||||
|
||||
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 (
|
||||
<div className="custom-tooltip" style={{
|
||||
<div style={{
|
||||
backgroundColor: '#fff',
|
||||
padding: '10px',
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '4px',
|
||||
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) => (
|
||||
<p key={index} style={{ color: item.color }}>
|
||||
{`${item.value}`}
|
||||
{`Значение: ${item.value}`}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -87,9 +98,13 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
|
|||
return null;
|
||||
};
|
||||
|
||||
if (!data.length) {
|
||||
return <div style={{ padding: '20px', textAlign: 'center' }}>Нет данных для отображения</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ position: 'relative' }}
|
||||
style={{ position: 'relative', height: '400px' }}
|
||||
ref={containerRef}
|
||||
className={isSelecting ? 'no-selection' : ''}
|
||||
>
|
||||
|
|
@ -102,20 +117,7 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
|
|||
`}
|
||||
</style>
|
||||
|
||||
<div style={{
|
||||
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}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart
|
||||
data={displayData}
|
||||
onMouseDown={handleMouseDown}
|
||||
|
|
@ -127,30 +129,21 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
|
|||
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
tick={{ fill: '#666' }}
|
||||
tickMargin={10}
|
||||
tick={{ fontSize: 12 }}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fill: '#666' }}
|
||||
tickMargin={10}
|
||||
/>
|
||||
<Tooltip
|
||||
content={<CustomTooltip />}
|
||||
cursor={{ stroke: '#ccc', strokeWidth: 1 }}
|
||||
/>
|
||||
{Object.keys(chartData).map((key, index) => (
|
||||
<YAxis tick={{ fontSize: 12 }} />
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
{Object.keys(chartData).map((instance, index) => (
|
||||
<Line
|
||||
key={key}
|
||||
key={instance}
|
||||
type="monotone"
|
||||
dataKey={key}
|
||||
dataKey={instance}
|
||||
stroke={colors[index % colors.length]}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
activeDot={{ r: 6 }}
|
||||
name={key}
|
||||
/>
|
||||
))}
|
||||
|
||||
{selectionArea?.start && selectionArea?.end && (
|
||||
<ReferenceArea
|
||||
x1={selectionArea.start}
|
||||
|
|
@ -165,4 +158,4 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang
|
|||
);
|
||||
};
|
||||
|
||||
export default LineChartComponent;
|
||||
export default React.memo(LineChartComponent);
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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,181 +123,133 @@ 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);
|
||||
console.log('Processing metrics data:', data);
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const rangeSeconds = useCustomRange
|
||||
? Math.floor(endDate.getTime() / 1000) - Math.floor(startDate.getTime() / 1000)
|
||||
: selectedRange.value;
|
||||
|
||||
const instancesData = {};
|
||||
|
||||
data.forEach(item => {
|
||||
const instance = item.instance || 'default';
|
||||
const timestamp = item.timestamp;
|
||||
const value = parseFloat(item.value);
|
||||
|
||||
if (!instancesData[instance]) {
|
||||
instancesData[instance] = [];
|
||||
}
|
||||
|
||||
const formatted = formatTime(timestamp, rangeSeconds);
|
||||
instancesData[instance].push({
|
||||
time: formatted.display, // для отображения
|
||||
value: value,
|
||||
timestamp: timestamp, // для сортировки
|
||||
originalTime: formatted // для группировки
|
||||
});
|
||||
});
|
||||
|
||||
// Группируем точки с одинаковым временем (в пределах секунды)
|
||||
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 {
|
||||
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);
|
||||
// Усредняем значения для точек в одну секунду
|
||||
timeMap[timeKey].value = (timeMap[timeKey].value * timeMap[timeKey].count + point.value) /
|
||||
(timeMap[timeKey].count + 1);
|
||||
timeMap[timeKey].count += 1;
|
||||
}
|
||||
});
|
||||
|
||||
interpolatedData.push(interpolatedPoint);
|
||||
}
|
||||
}
|
||||
instancesData[instance] = grouped.sort((a, b) => a.timestamp - b.timestamp);
|
||||
});
|
||||
|
||||
interpolatedData.push(data[data.length - 1]);
|
||||
return interpolatedData.slice(0, minPoints);
|
||||
};
|
||||
|
||||
const handleRangeChange = (event) => {
|
||||
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) {
|
||||
if (!socketRef.current?.connected) return;
|
||||
|
||||
clearInterval(intervalRef.current);
|
||||
fetchData();
|
||||
|
||||
intervalRef.current = setInterval(() => {
|
||||
fetchData();
|
||||
}, selectedRange.interval);
|
||||
}
|
||||
}, [selectedRange, useCustomRange, startDate, endDate, metricName]);
|
||||
|
||||
return () => clearInterval(intervalRef.current);
|
||||
}, [fetchData, selectedRange.interval]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedGraphRange) {
|
||||
if (!chartData || !selectedGraphRange) {
|
||||
setFilteredData(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const { startIndex, endIndex } = selectedGraphRange;
|
||||
const allTimes = Object.values(chartData)
|
||||
.flat()
|
||||
|
|
@ -290,31 +263,23 @@ const PrometheusChart = ({ metricName }) => {
|
|||
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);
|
||||
const interpolated = interpolateData(filtered, 15);
|
||||
setFilteredData(interpolated);
|
||||
} else {
|
||||
setFilteredData(null);
|
||||
}
|
||||
setFilteredData(filtered);
|
||||
}, [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)
|
||||
.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 <div style={{ padding: '20px', textAlign: 'center' }}>No data available</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
|
|
@ -324,180 +289,31 @@ const PrometheusChart = ({ metricName }) => {
|
|||
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>
|
||||
<ConnectionStatusIndicator connectionStatus={connectionStatus} />
|
||||
|
||||
{/* Группа элементов управления */}
|
||||
<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'
|
||||
}} />
|
||||
}
|
||||
<TimeRangeSelector
|
||||
selectedRange={selectedRange}
|
||||
handleRangeChange={handleRangeChange}
|
||||
startDate={startDate}
|
||||
setStartDate={setStartDate}
|
||||
endDate={endDate}
|
||||
setEndDate={setEndDate}
|
||||
useCustomRange={useCustomRange}
|
||||
handleCustomRangeChange={handleCustomRangeChange}
|
||||
handleResetZoom={handleResetZoom}
|
||||
/>
|
||||
</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'
|
||||
}} />
|
||||
}
|
||||
|
||||
<CurrentRangeDisplay
|
||||
useCustomRange={useCustomRange}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
selectedRange={selectedRange}
|
||||
/>
|
||||
</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}
|
||||
/>
|
||||
|
|
@ -505,4 +321,4 @@ const PrometheusChart = ({ metricName }) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default PrometheusChart;
|
||||
export default React.memo(PrometheusChart);
|
||||
Loading…
Reference in New Issue