Established a connection to the back using a 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
6a4640ba93
commit
4405c693aa
|
|
@ -26,7 +26,8 @@
|
||||||
"@mui/icons-material": "^6.4.8",
|
"@mui/icons-material": "^6.4.8",
|
||||||
"reactflow": "^11.11.4",
|
"reactflow": "^11.11.4",
|
||||||
"vite-plugin-svgr": "^4.3.0",
|
"vite-plugin-svgr": "^4.3.0",
|
||||||
"react-scripts": "^5.0.1"
|
"react-scripts": "^5.0.1",
|
||||||
|
"socket.io-client": "^4.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.17.0",
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import React, { useEffect, useState, useRef } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
import axios from 'axios';
|
|
||||||
import DatePicker from 'react-datepicker';
|
import DatePicker from 'react-datepicker';
|
||||||
import 'react-datepicker/dist/react-datepicker.css';
|
import 'react-datepicker/dist/react-datepicker.css';
|
||||||
import LineChartComponent from './Components/LineChartComponent';
|
import LineChartComponent from './Components/LineChartComponent';
|
||||||
|
import { io } from 'socket.io-client';
|
||||||
|
|
||||||
const MAX_POINTS = 20;
|
const MAX_POINTS = 20;
|
||||||
const COLORS = ['#3e95cd', '#8e5ea2', '#3cba9f', '#e8c3b9', '#c45850'];
|
const COLORS = ['#3e95cd', '#8e5ea2', '#3cba9f', '#e8c3b9', '#c45850'];
|
||||||
|
|
@ -30,11 +30,142 @@ const PrometheusChart = ({ metricName }) => {
|
||||||
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 [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 setupWebSocket = () => {
|
||||||
|
const socket = io('http://192.168.2.39:3000/metrics-ws', {
|
||||||
|
path: '/socket.io',
|
||||||
|
transports: ['websocket'],
|
||||||
|
reconnection: true,
|
||||||
|
reconnectionAttempts: 5,
|
||||||
|
reconnectionDelay: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
socketRef.current = socket;
|
||||||
|
|
||||||
|
socket.on('connect', () => {
|
||||||
|
setConnectionStatus('connected');
|
||||||
|
fetchData();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
setConnectionStatus('disconnected');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('connect_error', (err) => {
|
||||||
|
setConnectionStatus('error');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('metrics-data', (response) => {
|
||||||
|
processMetricsData(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
return socket;
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateStep = (start, end) => {
|
||||||
|
const range = end - start;
|
||||||
|
if (range <= 3600) return 5;
|
||||||
|
if (range <= 21600) return 30;
|
||||||
|
if (range <= 86400) return 120;
|
||||||
|
return 300;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchData = () => {
|
||||||
|
try {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
let start = useCustomRange
|
||||||
|
? Math.floor(startDate.getTime() / 1000)
|
||||||
|
: now - selectedRange.value;
|
||||||
|
let end = useCustomRange
|
||||||
|
? Math.floor(endDate.getTime() / 1000)
|
||||||
|
: now;
|
||||||
|
|
||||||
|
// Проверка на корректность диапазона
|
||||||
|
if (start >= end) {
|
||||||
|
console.error('Invalid time range: start >= end');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const step = calculateStep(start, end);
|
||||||
|
|
||||||
|
if (socketRef.current?.connected) {
|
||||||
|
socketRef.current.emit('get-metrics', {
|
||||||
|
metric: metricName,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
step
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('WebSocket is not connected');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in fetchData:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const processMetricsData = (response) => {
|
||||||
|
const { metric, data } = response;
|
||||||
|
if (metric !== metricName) return;
|
||||||
|
|
||||||
|
let metrics = Array.isArray(data) ? data : [];
|
||||||
|
let start, end;
|
||||||
|
|
||||||
|
if (metrics.length === 0) {
|
||||||
|
console.warn('Received empty metrics data');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useCustomRange) {
|
||||||
|
start = Math.floor(startDate.getTime() / 1000);
|
||||||
|
end = Math.floor(endDate.getTime() / 1000);
|
||||||
|
} else {
|
||||||
|
end = Math.floor(Date.now() / 1000);
|
||||||
|
start = end - selectedRange.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const step = calculateStep(start, end);
|
||||||
|
const range = end - start;
|
||||||
|
|
||||||
|
// 1. Генерируем ВСЕ ожидаемые временные точки
|
||||||
|
const timePoints = [];
|
||||||
|
for (let t = start; t <= end; t += step) {
|
||||||
|
const date = new Date(t * 1000);
|
||||||
|
const formattedTime = range > 86400
|
||||||
|
? date.toLocaleString([], { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
|
||||||
|
: date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||||
|
timePoints.push(formattedTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Создаем карту "время -> значение" для каждого инстанса
|
||||||
|
const timeValueMap = {};
|
||||||
|
metrics.forEach(m => {
|
||||||
|
const date = new Date(m.timestamp);
|
||||||
|
const formattedTime = range > 86400
|
||||||
|
? date.toLocaleString([], { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
|
||||||
|
: date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||||
|
|
||||||
|
const key = m.instance;
|
||||||
|
if (!timeValueMap[key]) timeValueMap[key] = {};
|
||||||
|
timeValueMap[key][formattedTime] = m.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Строим финальные данные, гарантируя все временные точки
|
||||||
|
const newChartData = {};
|
||||||
|
Object.keys(timeValueMap).forEach(instance => {
|
||||||
|
newChartData[instance] = timePoints.map(time => ({
|
||||||
|
time,
|
||||||
|
value: timeValueMap[instance][time] ?? null // null если данных нет
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
setChartData({ ...newChartData }); // Форсируем обновление
|
||||||
|
};
|
||||||
|
|
||||||
// Функция для интерполяции данных
|
|
||||||
const interpolateData = (data, minPoints = 15) => {
|
const interpolateData = (data, minPoints = 15) => {
|
||||||
if (data.length >= minPoints) return data;
|
if (data.length >= minPoints) return data;
|
||||||
|
|
||||||
|
|
@ -44,19 +175,15 @@ const PrometheusChart = ({ metricName }) => {
|
||||||
|
|
||||||
const currentPoint = data[i];
|
const currentPoint = data[i];
|
||||||
const nextPoint = data[i + 1];
|
const nextPoint = data[i + 1];
|
||||||
|
|
||||||
// Вычисляем разницу во времени между точками
|
|
||||||
const currentTime = new Date(currentPoint.time).getTime();
|
const currentTime = new Date(currentPoint.time).getTime();
|
||||||
const nextTime = new Date(nextPoint.time).getTime();
|
const nextTime = new Date(nextPoint.time).getTime();
|
||||||
const timeDiff = nextTime - currentTime;
|
const timeDiff = nextTime - currentTime;
|
||||||
|
|
||||||
// Добавляем промежуточные точки
|
|
||||||
const steps = Math.ceil((minPoints - data.length) / (data.length - 1));
|
const steps = Math.ceil((minPoints - data.length) / (data.length - 1));
|
||||||
for (let j = 1; j <= steps; j++) {
|
for (let j = 1; j <= steps; j++) {
|
||||||
const interpolatedTime = new Date(currentTime + (timeDiff * j) / (steps + 1)).toLocaleString();
|
const interpolatedTime = new Date(currentTime + (timeDiff * j) / (steps + 1)).toLocaleString();
|
||||||
const interpolatedPoint = { time: interpolatedTime };
|
const interpolatedPoint = { time: interpolatedTime };
|
||||||
|
|
||||||
// Интерполируем значения для каждой метрики
|
|
||||||
Object.keys(currentPoint).forEach(key => {
|
Object.keys(currentPoint).forEach(key => {
|
||||||
if (key !== 'time') {
|
if (key !== 'time') {
|
||||||
const currentValue = currentPoint[key];
|
const currentValue = currentPoint[key];
|
||||||
|
|
@ -69,122 +196,85 @@ const PrometheusChart = ({ metricName }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interpolatedData.push(data[data.length - 1]); // Добавляем последнюю точку
|
interpolatedData.push(data[data.length - 1]);
|
||||||
return interpolatedData.slice(0, minPoints); // Обрезаем до minPoints
|
return interpolatedData.slice(0, minPoints);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
let start, end;
|
|
||||||
|
|
||||||
if (useCustomRange) {
|
|
||||||
start = Math.floor(startDate.getTime() / 1000);
|
|
||||||
end = Math.floor(endDate.getTime() / 1000);
|
|
||||||
} else {
|
|
||||||
end = Math.floor(Date.now() / 1000);
|
|
||||||
start = end - selectedRange.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
let step;
|
|
||||||
const range = end - start;
|
|
||||||
if (range <= 3600) step = 5;
|
|
||||||
else if (range <= 21600) step = 30;
|
|
||||||
else if (range <= 86400) step = 120;
|
|
||||||
else step = 300;
|
|
||||||
|
|
||||||
const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/metrics`, {
|
|
||||||
params: {
|
|
||||||
metric: metricName,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
step
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = response.data;
|
|
||||||
let metrics = Array.isArray(result) ? result : result.data || [];
|
|
||||||
|
|
||||||
if (!Array.isArray(metrics)) {
|
|
||||||
metrics = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedData = {};
|
|
||||||
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 (!updatedData[key]) updatedData[key] = {};
|
|
||||||
updatedData[key][formattedTime] = m.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
const chartData = {};
|
|
||||||
Object.keys(updatedData).forEach(key => {
|
|
||||||
chartData[key] = timePoints.map(time => ({
|
|
||||||
time,
|
|
||||||
value: updatedData[key][time] ?? null,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
setChartData(chartData);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ошибка при загрузке метрик:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchData();
|
|
||||||
|
|
||||||
intervalRef.current = setInterval(() => {
|
|
||||||
fetchData();
|
|
||||||
}, selectedRange.interval);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (intervalRef.current) {
|
|
||||||
clearInterval(intervalRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [metricName, selectedRange, useCustomRange, startDate, endDate]);
|
|
||||||
|
|
||||||
const handleRangeChange = (event) => {
|
const handleRangeChange = (event) => {
|
||||||
const selectedValue = event.target.value;
|
const selectedValue = event.target.value;
|
||||||
const range = TIME_RANGES.find(range => range.value === parseInt(selectedValue, 10));
|
const range = TIME_RANGES.find(r => r.value === parseInt(selectedValue, 10));
|
||||||
|
|
||||||
// Принудительно сбрасываем состояние
|
// Сбрасываем данные и состояние
|
||||||
setSelectedGraphRange(null);
|
setChartData({});
|
||||||
setFilteredData(null);
|
setFilteredData(null);
|
||||||
|
setSelectedGraphRange(null);
|
||||||
|
|
||||||
// Обновляем диапазон
|
// Обновляем диапазон и даты
|
||||||
setSelectedRange({ ...range }); // Создаем новый объект, чтобы React увидел изменение
|
setSelectedRange(range);
|
||||||
setUseCustomRange(false);
|
setUseCustomRange(false);
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
setEndDate(now);
|
||||||
|
setStartDate(new Date(now.getTime() - range.value * 1000));
|
||||||
|
|
||||||
|
// Принудительно запрашиваем новые данные
|
||||||
|
// Используем setTimeout для гарантированного обновления состояния перед запросом
|
||||||
|
setTimeout(() => {
|
||||||
|
fetchData();
|
||||||
|
}, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCustomRangeChange = () => {
|
const handleCustomRangeChange = () => {
|
||||||
|
// Сбрасываем данные и состояние
|
||||||
|
setChartData({});
|
||||||
|
setFilteredData(null);
|
||||||
|
setSelectedGraphRange(null);
|
||||||
|
|
||||||
setUseCustomRange(true);
|
setUseCustomRange(true);
|
||||||
setSelectedGraphRange(null); // Сбрасываем выбранный диапазон
|
|
||||||
setFilteredData(null); // Сбрасываем отфильтрованные данные
|
// Принудительно запрашиваем новые данные
|
||||||
|
setTimeout(() => {
|
||||||
|
fetchData();
|
||||||
|
}, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResetZoom = () => {
|
const handleResetZoom = () => {
|
||||||
setSelectedGraphRange(null);
|
setSelectedGraphRange(null);
|
||||||
setFilteredData(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]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// При изменении диапазона или дат перезапускаем интервал
|
||||||
|
if (socketRef.current?.connected) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
fetchData();
|
||||||
|
|
||||||
|
intervalRef.current = setInterval(() => {
|
||||||
|
fetchData();
|
||||||
|
}, selectedRange.interval);
|
||||||
|
}
|
||||||
|
}, [selectedRange, useCustomRange, startDate, endDate, metricName]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedGraphRange) {
|
if (selectedGraphRange) {
|
||||||
const { startIndex, endIndex } = selectedGraphRange;
|
const { startIndex, endIndex } = selectedGraphRange;
|
||||||
|
|
@ -203,12 +293,10 @@ const PrometheusChart = ({ metricName }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const filtered = data.slice(startIndex, endIndex + 1);
|
const filtered = data.slice(startIndex, endIndex + 1);
|
||||||
|
|
||||||
// Интерполируем данные, если точек меньше 15
|
|
||||||
const interpolated = interpolateData(filtered, 15);
|
const interpolated = interpolateData(filtered, 15);
|
||||||
setFilteredData(interpolated); // Сохраняем интерполированные данные
|
setFilteredData(interpolated);
|
||||||
} else {
|
} else {
|
||||||
setFilteredData(null); // Сбрасываем фильтрацию, если диапазон не выбран
|
setFilteredData(null);
|
||||||
}
|
}
|
||||||
}, [selectedGraphRange, chartData]);
|
}, [selectedGraphRange, chartData]);
|
||||||
|
|
||||||
|
|
@ -233,8 +321,23 @@ const PrometheusChart = ({ metricName }) => {
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
padding: '20px',
|
padding: '20px',
|
||||||
marginBottom: '20px'
|
marginBottom: '20px',
|
||||||
|
position: 'relative'
|
||||||
}}>
|
}}>
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '10px',
|
||||||
|
right: '10px',
|
||||||
|
padding: '5px 10px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
backgroundColor: connectionStatus === 'connected' ? '#4CAF50' :
|
||||||
|
connectionStatus === 'error' ? '#F44336' : '#FFC107',
|
||||||
|
color: 'white',
|
||||||
|
fontSize: '12px'
|
||||||
|
}}>
|
||||||
|
{connectionStatus === 'connected' ? 'Online' :
|
||||||
|
connectionStatus === 'error' ? 'Connection Error' : 'Offline'}
|
||||||
|
</div>
|
||||||
{/* Заголовок графика */}
|
{/* Заголовок графика */}
|
||||||
<h3 style={{ marginTop: 0, color: '#333' }}>
|
<h3 style={{ marginTop: 0, color: '#333' }}>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue