Merge pull request 'debugging' (#29) from debugging into redesign
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
Reviewed-on: http://git.enode/deployer3000/trust-module-frontend/pulls/29pull/28/head
commit
46484efdea
|
|
@ -11,28 +11,59 @@ const LineChartComponent = ({
|
||||||
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);
|
||||||
const containerRef = useRef(null);
|
|
||||||
|
|
||||||
const allTimes = Object.values(chartData)
|
const allTimestamps = Object.values(chartData)
|
||||||
.flat()
|
.flat()
|
||||||
.map(point => point.time)
|
.map(point => point.timestamp)
|
||||||
.filter((time, index, self) => self.indexOf(time) === index);
|
.filter((timestamp, index, self) => self.indexOf(timestamp) === index)
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
|
||||||
|
const data = allTimestamps.map(timestamp => {
|
||||||
|
const point = { timestamp };
|
||||||
|
|
||||||
|
const firstPoint = Object.values(chartData)
|
||||||
|
.flat()
|
||||||
|
.find(p => p.timestamp === timestamp);
|
||||||
|
|
||||||
|
if (firstPoint) {
|
||||||
|
point.time = firstPoint.time;
|
||||||
|
point.fullTime = firstPoint.fullTime;
|
||||||
|
}
|
||||||
|
|
||||||
const data = allTimes.map(time => {
|
|
||||||
const point = { time };
|
|
||||||
Object.keys(chartData).forEach(key => {
|
Object.keys(chartData).forEach(key => {
|
||||||
const instanceData = chartData[key].find(p => p.time === time);
|
const instanceData = chartData[key].find(p => p.timestamp === timestamp);
|
||||||
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;
|
||||||
|
|
||||||
|
const instanceKeys = displayData.length
|
||||||
|
? Object.keys(displayData[0]).filter(k => !['timestamp', 'time', 'fullTime'].includes(k))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Функция для определения оптимального формата времени в зависимости от диапазона
|
||||||
|
const getTimeFormat = () => {
|
||||||
|
if (!data.length) return 'HH:mm:ss';
|
||||||
|
|
||||||
|
const first = data[0].timestamp;
|
||||||
|
const last = data[data.length - 1].timestamp;
|
||||||
|
const range = last - first;
|
||||||
|
|
||||||
|
// Если диапазон больше 24 часов - показываем дату
|
||||||
|
if (range > 86400000) {
|
||||||
|
return 'dd.MM HH:mm';
|
||||||
|
}
|
||||||
|
// Если больше 1 часа - показываем часы и минуты
|
||||||
|
if (range > 3600000) {
|
||||||
|
return 'HH:mm';
|
||||||
|
}
|
||||||
|
// Для коротких диапазонов - показываем время с секундами
|
||||||
|
return 'HH:mm:ss';
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleSelectStart = (e) => {
|
const handleSelectStart = (e) => {
|
||||||
if (isSelecting) {
|
if (isSelecting) {
|
||||||
|
|
@ -40,44 +71,66 @@ const LineChartComponent = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
document.addEventListener('selectstart', handleSelectStart);
|
document.addEventListener('selectstart', handleSelectStart);
|
||||||
return () => document.removeEventListener('selectstart', handleSelectStart);
|
return () => document.removeEventListener('selectstart', handleSelectStart);
|
||||||
}, [isSelecting]);
|
}, [isSelecting]);
|
||||||
|
|
||||||
const handleMouseDown = (e) => {
|
const handleMouseDown = (e) => {
|
||||||
if (!e || !e.activeLabel) return;
|
if (!e) return;
|
||||||
|
|
||||||
|
// Получаем индекс точки по координатам
|
||||||
|
const activeIndex = e.activeTooltipIndex;
|
||||||
|
if (activeIndex === undefined || activeIndex < 0 || activeIndex >= data.length) return;
|
||||||
|
|
||||||
setIsSelecting(true);
|
setIsSelecting(true);
|
||||||
setSelectionArea({ start: e.activeLabel, end: null });
|
setSelectionArea({
|
||||||
|
start: data[activeIndex].timestamp,
|
||||||
|
end: null,
|
||||||
|
startIndex: activeIndex,
|
||||||
|
endIndex: null
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseMove = (e) => {
|
const handleMouseMove = (e) => {
|
||||||
if (!selectionArea?.start || !e?.activeLabel) return;
|
if (!isSelecting || !selectionArea?.start || !e) return;
|
||||||
setSelectionArea(prev => ({ ...prev, end: e.activeLabel }));
|
|
||||||
|
const activeIndex = e.activeTooltipIndex;
|
||||||
|
if (activeIndex === undefined || activeIndex < 0 || activeIndex >= data.length) return;
|
||||||
|
|
||||||
|
setSelectionArea(prev => ({
|
||||||
|
...prev,
|
||||||
|
end: data[activeIndex].timestamp,
|
||||||
|
endIndex: activeIndex
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
const handleMouseUp = () => {
|
||||||
|
if (!isSelecting || !selectionArea?.start || !selectionArea?.end) {
|
||||||
setIsSelecting(false);
|
setIsSelecting(false);
|
||||||
|
|
||||||
if (!selectionArea?.start || !selectionArea?.end) {
|
|
||||||
setSelectionArea(null);
|
setSelectionArea(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const startIndex = data.findIndex(point => point.time === selectionArea.start);
|
const startIndex = Math.min(selectionArea.startIndex, selectionArea.endIndex);
|
||||||
const endIndex = data.findIndex(point => point.time === selectionArea.end);
|
const endIndex = Math.max(selectionArea.startIndex, selectionArea.endIndex);
|
||||||
|
|
||||||
|
// Нормализуем индексы к диапазону [0, 1] для родительского компонента
|
||||||
|
const normalizedStart = startIndex / (data.length - 1);
|
||||||
|
const normalizedEnd = endIndex / (data.length - 1);
|
||||||
|
|
||||||
if (startIndex >= 0 && endIndex >= 0) {
|
|
||||||
onRangeSelect({
|
onRangeSelect({
|
||||||
startIndex: Math.min(startIndex, endIndex),
|
startIndex: normalizedStart,
|
||||||
endIndex: Math.max(startIndex, endIndex)
|
endIndex: normalizedEnd
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
|
setIsSelecting(false);
|
||||||
setSelectionArea(null);
|
setSelectionArea(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const CustomTooltip = ({ active, payload, label }) => {
|
const CustomTooltip = ({ active, payload, label }) => {
|
||||||
if (active && payload && payload.length) {
|
if (active && payload && payload.length) {
|
||||||
|
const currentPoint = data.find(point => point.timestamp === label);
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
|
|
@ -86,7 +139,9 @@ const LineChartComponent = ({
|
||||||
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' }}>
|
||||||
|
{currentPoint?.fullTime || new Date(label).toLocaleString('ru-RU')}
|
||||||
|
</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}`}
|
||||||
|
|
@ -103,20 +158,7 @@ const LineChartComponent = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div style={{ position: 'relative', height: '400px' }}>
|
||||||
style={{ position: 'relative', height: '400px' }}
|
|
||||||
ref={containerRef}
|
|
||||||
className={isSelecting ? 'no-selection' : ''}
|
|
||||||
>
|
|
||||||
<style>
|
|
||||||
{`
|
|
||||||
.no-selection {
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<LineChart
|
<LineChart
|
||||||
data={displayData}
|
data={displayData}
|
||||||
|
|
@ -128,16 +170,44 @@ const LineChartComponent = ({
|
||||||
>
|
>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
|
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="time"
|
dataKey="timestamp"
|
||||||
tick={{ fontSize: 12 }}
|
height={75}
|
||||||
|
tick={{ fontSize: 12, angle: -45, textAnchor: 'end' }}
|
||||||
|
interval={Math.max(1, Math.floor(data.length / 10))}
|
||||||
|
tickFormatter={(timestamp) => {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const format = getTimeFormat();
|
||||||
|
|
||||||
|
if (format === 'dd.MM HH:mm') {
|
||||||
|
return date.toLocaleString('ru-RU', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
} else if (format === 'HH:mm') {
|
||||||
|
return date.toLocaleString('ru-RU', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return date.toLocaleString('ru-RU', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<YAxis tick={{ fontSize: 12 }} />
|
<YAxis tick={{ fontSize: 12 }} />
|
||||||
<Tooltip content={<CustomTooltip />} />
|
<Tooltip content={<CustomTooltip />} />
|
||||||
{Object.keys(chartData).map((instance, index) => (
|
{instanceKeys.map((instance, index) => (
|
||||||
<Line
|
<Line
|
||||||
key={instance}
|
key={instance}
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey={instance}
|
dataKey={instance}
|
||||||
|
name={instance}
|
||||||
stroke={colors[index % colors.length]}
|
stroke={colors[index % colors.length]}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
dot={false}
|
dot={false}
|
||||||
|
|
|
||||||
|
|
@ -17,39 +17,57 @@ const PrometheusChart = ({ metricName }) => {
|
||||||
const [selectedGraphRange, setSelectedGraphRange] = useState(null);
|
const [selectedGraphRange, setSelectedGraphRange] = useState(null);
|
||||||
const [filteredData, setFilteredData] = useState(null);
|
const [filteredData, setFilteredData] = useState(null);
|
||||||
const [isSelectingRange, setIsSelectingRange] = useState(false);
|
const [isSelectingRange, setIsSelectingRange] = useState(false);
|
||||||
|
const [lastCustomRange, setLastCustomRange] = useState(null);
|
||||||
const intervalRef = useRef(null);
|
const intervalRef = useRef(null);
|
||||||
const socketRef = useRef(null);
|
const socketRef = useRef(null);
|
||||||
|
const debounceRef = useRef(null);
|
||||||
|
|
||||||
const formatTime = useCallback((timestamp, rangeSeconds) => {
|
const formatTime = useCallback((timestamp, rangeSeconds) => {
|
||||||
const date = new Date(timestamp);
|
const ts = typeof timestamp === 'number' ? timestamp : Date.now();
|
||||||
if (rangeSeconds > 86400) {
|
const date = new Date(ts);
|
||||||
return {
|
|
||||||
display: date.toLocaleString([], {
|
// Определяем формат в зависимости от диапазона
|
||||||
day: '2-digit',
|
const showFullDate = rangeSeconds > 86400; // больше суток
|
||||||
month: '2-digit',
|
|
||||||
year: '2-digit',
|
const timeOptions = {
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
}),
|
|
||||||
timestamp: timestamp
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
display: date.toLocaleTimeString([], {
|
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
second: '2-digit'
|
second: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const dateOptions = showFullDate ? {
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
...timeOptions
|
||||||
|
} : timeOptions;
|
||||||
|
|
||||||
|
return {
|
||||||
|
display: date.toLocaleString('ru-RU', dateOptions),
|
||||||
|
fullDisplay: date.toLocaleString('ru-RU', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: false
|
||||||
}),
|
}),
|
||||||
timestamp: timestamp
|
timestamp: ts
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const calculateStep = useCallback((start, end) => {
|
const calculateStep = useCallback((start, end) => {
|
||||||
const range = end - start;
|
const range = end - start;
|
||||||
if (range <= 3600) return 5;
|
if (range <= 60) return 1; // 1 мин
|
||||||
if (range <= 21600) return 30;
|
if (range <= 300) return 5; // 5 мин
|
||||||
if (range <= 86400) return 120;
|
if (range <= 1800) return 15; // 30 мин
|
||||||
return 300;
|
if (range <= 3600) return 30; // 1 час
|
||||||
|
if (range <= 10800) return 60; // 3 часа
|
||||||
|
if (range <= 21600) return 120; // 6 часов
|
||||||
|
if (range <= 43200) return 300; // 12 часов
|
||||||
|
if (range <= 86400) return 600; // 24 часа
|
||||||
|
return 1800; // > 24 часов
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchData = useCallback(() => {
|
const fetchData = useCallback(() => {
|
||||||
|
|
@ -72,26 +90,8 @@ const PrometheusChart = ({ metricName }) => {
|
||||||
}
|
}
|
||||||
}, [metricName, selectedRange.value, isSelectingRange]);
|
}, [metricName, selectedRange.value, isSelectingRange]);
|
||||||
|
|
||||||
const groupBySecond = (points) => {
|
|
||||||
const grouped = [];
|
|
||||||
const timeMap = {};
|
|
||||||
|
|
||||||
points.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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return grouped;
|
|
||||||
};
|
|
||||||
|
|
||||||
const processMetricsData = useCallback((response) => {
|
const processMetricsData = useCallback((response) => {
|
||||||
|
console.log('Processing metrics data:', response);
|
||||||
if (response.metric !== metricName) return;
|
if (response.metric !== metricName) return;
|
||||||
|
|
||||||
const dataArray = Array.isArray(response.data) ? response.data : [response.data];
|
const dataArray = Array.isArray(response.data) ? response.data : [response.data];
|
||||||
|
|
@ -99,32 +99,43 @@ const PrometheusChart = ({ metricName }) => {
|
||||||
|
|
||||||
setChartData(prev => {
|
setChartData(prev => {
|
||||||
const newData = { ...(prev || {}) };
|
const newData = { ...(prev || {}) };
|
||||||
|
const rangeSeconds = useCustomRange
|
||||||
|
? (endDate.getTime() - startDate.getTime()) / 1000
|
||||||
|
: selectedRange.value;
|
||||||
|
|
||||||
dataArray.forEach(item => {
|
dataArray.forEach(item => {
|
||||||
const instance = item.instance || 'default';
|
const instance = item.instance || 'default';
|
||||||
if (!newData[instance]) newData[instance] = [];
|
if (!newData[instance]) newData[instance] = [];
|
||||||
|
|
||||||
const timestamp = item.timestamp > 1e12 ? item.timestamp : item.timestamp * 1000;
|
// Унифицированная конвертация timestamp
|
||||||
const value = parseFloat(item.value);
|
let timestamp;
|
||||||
|
if (typeof item.timestamp === 'number') {
|
||||||
|
// Определяем, в секундах или миллисекундах пришел timestamp
|
||||||
|
timestamp = item.timestamp > 1e12 ? item.timestamp : item.timestamp * 1000;
|
||||||
|
} else {
|
||||||
|
timestamp = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = parseFloat(item.value);
|
||||||
|
const formattedTime = formatTime(timestamp, rangeSeconds);
|
||||||
|
|
||||||
if (!newData[instance].some(p => p.timestamp === timestamp)) {
|
|
||||||
newData[instance].push({
|
newData[instance].push({
|
||||||
time: formatTime(timestamp, selectedRange.value).display,
|
time: formattedTime.display,
|
||||||
|
fullTime: formattedTime.fullDisplay,
|
||||||
value: value,
|
value: value,
|
||||||
timestamp: timestamp
|
timestamp: timestamp
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Сортируем и ограничиваем данные
|
||||||
Object.keys(newData).forEach(instance => {
|
Object.keys(newData).forEach(instance => {
|
||||||
newData[instance] = groupBySecond(newData[instance])
|
newData[instance] = newData[instance]
|
||||||
.sort((a, b) => a.timestamp - b.timestamp)
|
.sort((a, b) => a.timestamp - b.timestamp)
|
||||||
.slice(-1000);
|
.slice(-1000);
|
||||||
});
|
});
|
||||||
|
return newData;
|
||||||
return Object.keys(newData).length ? newData : prev;
|
|
||||||
});
|
});
|
||||||
}, [metricName, selectedRange.value, formatTime]);
|
}, [metricName, selectedRange.value, formatTime, useCustomRange, startDate, endDate]);
|
||||||
|
|
||||||
const setupWebSocket = useCallback(() => {
|
const setupWebSocket = useCallback(() => {
|
||||||
if (socketRef.current) {
|
if (socketRef.current) {
|
||||||
|
|
@ -178,7 +189,7 @@ const PrometheusChart = ({ metricName }) => {
|
||||||
const fetchCustomRangeData = useCallback(async () => {
|
const fetchCustomRangeData = useCallback(async () => {
|
||||||
const start = Math.floor(startDate.getTime() / 1000);
|
const start = Math.floor(startDate.getTime() / 1000);
|
||||||
const end = Math.floor(endDate.getTime() / 1000);
|
const end = Math.floor(endDate.getTime() / 1000);
|
||||||
const step = calculateStep(start, end);
|
const rangeSeconds = end - start;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/metrics`, {
|
const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/metrics`, {
|
||||||
|
|
@ -186,18 +197,21 @@ const PrometheusChart = ({ metricName }) => {
|
||||||
metric: metricName,
|
metric: metricName,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
step
|
step: calculateStep(start, end)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data) { // Изменили условие, так как бэкенд возвращает массив напрямую
|
if (response.data?.length) {
|
||||||
|
// Преобразуем данные перед передачей в processMetricsData
|
||||||
|
const processedData = response.data.map(item => ({
|
||||||
|
...item,
|
||||||
|
timestamp: item.timestamp, // оставляем в секундах - processMetricsData конвертирует
|
||||||
|
value: item.value.toString()
|
||||||
|
}));
|
||||||
|
|
||||||
processMetricsData({
|
processMetricsData({
|
||||||
metric: metricName,
|
metric: metricName,
|
||||||
data: response.data.map(item => ({
|
data: processedData
|
||||||
...item,
|
|
||||||
timestamp: item.timestamp / 1000, // или item.timestamp если уже в секундах
|
|
||||||
value: item.value.toString() // преобразуем в строку, как ожидает processMetricsData
|
|
||||||
}))
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -207,6 +221,12 @@ const PrometheusChart = ({ metricName }) => {
|
||||||
|
|
||||||
|
|
||||||
const handleRangeChange = useCallback((event) => {
|
const handleRangeChange = useCallback((event) => {
|
||||||
|
// Очищаем текущий интервал
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
intervalRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
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));
|
||||||
|
|
||||||
|
|
@ -220,50 +240,131 @@ const PrometheusChart = ({ metricName }) => {
|
||||||
setEndDate(now);
|
setEndDate(now);
|
||||||
setStartDate(new Date(now.getTime() - range.value * 1000));
|
setStartDate(new Date(now.getTime() - range.value * 1000));
|
||||||
|
|
||||||
// Переподключение сокета при возврате к стандартным диапазонам
|
// Переподключение сокета
|
||||||
if (!socketRef.current?.connected) {
|
if (!socketRef.current?.connected) {
|
||||||
socketRef.current?.connect();
|
socketRef.current?.connect();
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCustomRangeChange = useCallback(() => {
|
const handleCustomRangeChange = useCallback(() => {
|
||||||
|
// Отключаем WebSocket соединение
|
||||||
|
if (socketRef.current?.connected) {
|
||||||
|
socketRef.current.disconnect();
|
||||||
|
setConnectionStatus('disconnected');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Очищаем интервал обновления
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
intervalRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
setUseCustomRange(true);
|
setUseCustomRange(true);
|
||||||
setChartData(null);
|
setChartData(null);
|
||||||
setSelectedGraphRange(null);
|
setSelectedGraphRange(null);
|
||||||
setFilteredData(null);
|
setFilteredData(null);
|
||||||
}, []);
|
fetchCustomRangeData();
|
||||||
|
}, [fetchCustomRangeData]);
|
||||||
|
|
||||||
const handleResetZoom = useCallback(() => {
|
const handleResetZoom = useCallback(() => {
|
||||||
setSelectedGraphRange(null);
|
setSelectedGraphRange(null);
|
||||||
setFilteredData(null);
|
setFilteredData(null);
|
||||||
setIsSelectingRange(false);
|
setIsSelectingRange(false);
|
||||||
|
|
||||||
|
if (useCustomRange) {
|
||||||
|
fetchCustomRangeData();
|
||||||
|
} else {
|
||||||
|
if (!socketRef.current?.connected) {
|
||||||
|
socketRef.current?.connect();
|
||||||
|
}
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [fetchData]);
|
}
|
||||||
|
|
||||||
|
if (lastCustomRange) {
|
||||||
|
handleRangeSelect(lastCustomRange);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [fetchData, fetchCustomRangeData, useCustomRange]);
|
||||||
|
|
||||||
|
const interpolateData = useCallback((data, targetPointCount) => {
|
||||||
|
if (!data || data.length < 2) return data;
|
||||||
|
if (data.length >= targetPointCount) return data;
|
||||||
|
|
||||||
|
const interpolated = [];
|
||||||
|
const step = (data.length - 1) / (targetPointCount - 1);
|
||||||
|
|
||||||
|
for (let i = 0; i < targetPointCount; i++) {
|
||||||
|
const index = i * step;
|
||||||
|
const lowerIndex = Math.floor(index);
|
||||||
|
const upperIndex = Math.ceil(index);
|
||||||
|
|
||||||
|
if (lowerIndex === upperIndex) {
|
||||||
|
interpolated.push(data[lowerIndex]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fraction = index - lowerIndex;
|
||||||
|
const interpolatedPoint = {};
|
||||||
|
|
||||||
|
Object.keys(data[lowerIndex]).forEach(key => {
|
||||||
|
if (key === 'timestamp') {
|
||||||
|
interpolatedPoint[key] = data[lowerIndex][key] +
|
||||||
|
fraction * (data[upperIndex][key] - data[lowerIndex][key]);
|
||||||
|
|
||||||
|
// Добавляем отображаемое время
|
||||||
|
const { display, fullDisplay } = formatTime(interpolatedPoint[key],
|
||||||
|
(endDate - startDate) / 1000);
|
||||||
|
interpolatedPoint.time = display;
|
||||||
|
interpolatedPoint.fullTime = fullDisplay;
|
||||||
|
} else if (typeof data[lowerIndex][key] === 'number') {
|
||||||
|
interpolatedPoint[key] = data[lowerIndex][key] +
|
||||||
|
fraction * (data[upperIndex][key] - data[lowerIndex][key]);
|
||||||
|
} else {
|
||||||
|
interpolatedPoint[key] = data[lowerIndex][key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
interpolated.push(interpolatedPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
return interpolated;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleRangeSelect = useCallback((range) => {
|
const handleRangeSelect = useCallback((range) => {
|
||||||
if (range) {
|
setLastCustomRange(range);
|
||||||
// Начало выделения - останавливаем обновления
|
if (!range || !chartData) return;
|
||||||
|
|
||||||
setIsSelectingRange(true);
|
setIsSelectingRange(true);
|
||||||
setSelectedGraphRange(range);
|
setSelectedGraphRange(range);
|
||||||
|
|
||||||
// Отключаем сокет
|
// Отключаем автоматические обновления
|
||||||
if (socketRef.current?.connected) {
|
if (socketRef.current?.connected) {
|
||||||
socketRef.current.disconnect();
|
socketRef.current.disconnect();
|
||||||
}
|
}
|
||||||
// Очищаем интервал
|
|
||||||
if (intervalRef.current) {
|
if (intervalRef.current) {
|
||||||
clearInterval(intervalRef.current);
|
clearInterval(intervalRef.current);
|
||||||
intervalRef.current = null;
|
intervalRef.current = null;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Окончание выделения - возобновляем соединение
|
|
||||||
setIsSelectingRange(false);
|
|
||||||
|
|
||||||
if (!useCustomRange && socketRef.current && !socketRef.current.connected) {
|
// Получаем все точки и сортируем по времени
|
||||||
socketRef.current.connect();
|
const allPoints = Object.values(chartData).flat();
|
||||||
}
|
const sortedPoints = allPoints.sort((a, b) => a.timestamp - b.timestamp);
|
||||||
}
|
|
||||||
}, [useCustomRange]);
|
// Вычисляем абсолютные индексы
|
||||||
|
const startIndex = Math.floor(range.startIndex * (sortedPoints.length - 1));
|
||||||
|
const endIndex = Math.floor(range.endIndex * (sortedPoints.length - 1));
|
||||||
|
|
||||||
|
// Фильтруем точки по выбранному диапазону
|
||||||
|
const filtered = sortedPoints.slice(startIndex, endIndex + 1);
|
||||||
|
|
||||||
|
// Применяем интерполяцию только если точек меньше 100
|
||||||
|
const interpolated = filtered.length < 100 ?
|
||||||
|
interpolateData(filtered, Math.min(100, filtered.length * 3)) :
|
||||||
|
filtered;
|
||||||
|
|
||||||
|
setFilteredData(interpolated);
|
||||||
|
setIsSelectingRange(false);
|
||||||
|
}, [chartData, interpolateData, formatTime]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const socket = setupWebSocket();
|
const socket = setupWebSocket();
|
||||||
|
|
@ -273,16 +374,29 @@ const PrometheusChart = ({ metricName }) => {
|
||||||
};
|
};
|
||||||
}, [setupWebSocket]);
|
}, [setupWebSocket]);
|
||||||
|
|
||||||
|
// Обновим useEffect для кастомного диапазона
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (useCustomRange) {
|
if (useCustomRange && !isSelectingRange) {
|
||||||
if (socketRef.current?.connected) {
|
// Очищаем предыдущий таймер
|
||||||
socketRef.current.disconnect();
|
if (debounceRef.current) {
|
||||||
}
|
clearTimeout(debounceRef.current);
|
||||||
fetchCustomRangeData();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!socketRef.current?.connected || isSelectingRange) return;
|
// Устанавливаем новый таймер с задержкой 500 мс
|
||||||
|
debounceRef.current = setTimeout(() => {
|
||||||
|
fetchCustomRangeData();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (debounceRef.current) {
|
||||||
|
clearTimeout(debounceRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [useCustomRange, isSelectingRange, startDate, endDate, fetchCustomRangeData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (useCustomRange || isSelectingRange) return;
|
||||||
|
|
||||||
const fetchDataWrapper = () => {
|
const fetchDataWrapper = () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -292,45 +406,41 @@ const PrometheusChart = ({ metricName }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Очищаем предыдущий интервал
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запускаем сразу и затем по интервалу
|
||||||
fetchDataWrapper();
|
fetchDataWrapper();
|
||||||
intervalRef.current = setInterval(fetchDataWrapper, selectedRange.interval);
|
intervalRef.current = setInterval(fetchDataWrapper, selectedRange.interval);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (intervalRef.current) {
|
if (intervalRef.current) {
|
||||||
clearInterval(intervalRef.current);
|
clearInterval(intervalRef.current);
|
||||||
intervalRef.current = null;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [fetchData, fetchCustomRangeData, selectedRange.interval, useCustomRange, isSelectingRange]);
|
}, [fetchData, selectedRange.interval, useCustomRange, isSelectingRange]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!chartData || !selectedGraphRange) {
|
if (!selectedGraphRange || !chartData) {
|
||||||
setFilteredData(null);
|
setFilteredData(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { startIndex, endIndex } = selectedGraphRange;
|
const allPoints = Object.values(chartData).flat();
|
||||||
const allTimes = Object.values(chartData)
|
const sortedPoints = allPoints.sort((a, b) => a.timestamp - b.timestamp);
|
||||||
.flat()
|
|
||||||
.map(point => point.time)
|
|
||||||
.filter((time, index, self) => self.indexOf(time) === index);
|
|
||||||
|
|
||||||
const data = allTimes.map(time => {
|
const startIndex = Math.floor(selectedGraphRange.startIndex * (sortedPoints.length - 1));
|
||||||
const point = { time };
|
const endIndex = Math.floor(selectedGraphRange.endIndex * (sortedPoints.length - 1));
|
||||||
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);
|
const filtered = sortedPoints.slice(startIndex, endIndex + 1);
|
||||||
setFilteredData(filtered);
|
const interpolated = filtered.length > 100 ?
|
||||||
}, [selectedGraphRange, chartData]);
|
interpolateData(filtered, 100) :
|
||||||
|
filtered;
|
||||||
|
|
||||||
|
setFilteredData(interpolated);
|
||||||
|
}, [selectedGraphRange, chartData, interpolateData]);
|
||||||
|
|
||||||
if (chartData === null) {
|
if (chartData === null) {
|
||||||
return <div style={{ padding: '20px', textAlign: 'center' }}>Loading data...</div>;
|
return <div style={{ padding: '20px', textAlign: 'center' }}>Loading data...</div>;
|
||||||
|
|
@ -373,7 +483,7 @@ const PrometheusChart = ({ metricName }) => {
|
||||||
chartData={chartData}
|
chartData={chartData}
|
||||||
metricName={metricName}
|
metricName={metricName}
|
||||||
colors={COLORS}
|
colors={COLORS}
|
||||||
onRangeSelect={handleRangeSelect} // Используем модифицированный обработчик
|
onRangeSelect={handleRangeSelect}
|
||||||
filteredData={filteredData}
|
filteredData={filteredData}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@ const getMetricName = (id) => {
|
||||||
return `zvks_apiforsnmp_measure_${id}`;
|
return `zvks_apiforsnmp_measure_${id}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
//!!!!!!!!!!Пофиксить вкладуи с eth4, во всех eth 1-4 открывается именно 4 !!!!!!!!!!!!!
|
|
||||||
|
|
||||||
// Функция для рекурсивного сбора всех id потомков
|
// Функция для рекурсивного сбора всех id потомков
|
||||||
const getAllChildIds = (node) => {
|
const getAllChildIds = (node) => {
|
||||||
let ids = [];
|
let ids = [];
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import SystemStatusChart from "../../Charts/SystemStatusChart";
|
import SystemStatusChart from "../../Charts/SystemStatusChart";
|
||||||
import TreeTable from "../UI/TreeTable";
|
import TreeTable from "../UI/TreeTable";
|
||||||
|
|
||||||
import FlowChart from "../TreeChart/FlowChart";
|
import FlowChart from "../TreeChart/FlowChart";
|
||||||
|
|
||||||
const TabContent = ({ activeTab, statusHistories, treeData1, tabContent, handleOpenTab }) => {
|
const TabContent = ({ activeTab, statusHistories, treeData1, tabContent, handleOpenTab }) => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue