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 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);
|
||||||
|
|
@ -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 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);
|
||||||
Loading…
Reference in New Issue