Established a connection to the back using a web socket
test-org/trust-module-frontend/pipeline/pr-rc This commit looks good Details

pull/29/head
DmitriyA 2025-04-01 11:50:00 -04:00
parent 6a4640ba93
commit 4405c693aa
3 changed files with 213 additions and 109 deletions

View File

@ -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",

View File

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