prepared the graph for refactoring and added an indicator

pull/40/head
DmitriyA 2025-04-28 09:27:07 -04:00
parent d5aa312104
commit b9a2be4860
7 changed files with 129 additions and 71 deletions

View File

@ -1,34 +1,26 @@
import React from 'react';
import { Box, Skeleton } from '@mui/material';
const ChartSkeleton = () => (
<Box sx={{
backgroundColor: '#fff',
borderRadius: '8px',
padding: '20px',
marginBottom: '20px',
position: 'relative'
}}>
<Box sx={{ position: 'absolute', right: '20px', top: '20px' }}>
<Skeleton variant="circular" width={16} height={16} />
</Box>
const ChartSkeleton = ({ count = 1 }) => {
return (
<>
{Array.from({ length: count }).map((_, index) => (
<Box
key={index}
sx={{
backgroundColor: '#fff',
borderRadius: '8px',
padding: '20px',
mb: 3,
height: '400px'
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<Skeleton variant="text" width="40%" height={30} />
<Skeleton variant="text" width="20%" height={30} />
</Box>
<Skeleton variant="rectangular" width="100%" height="80%" />
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 2, gap: 2 }}>
{[1, 2, 3, 4].map((i) => (
<Skeleton key={i} variant="rounded" width={80} height={36} />
))}
</Box>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<Skeleton variant="text" width="40%" height={30} />
<Skeleton variant="text" width="30%" height={30} />
</Box>
<Skeleton variant="rectangular" width="100%" height={300} />
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 2, gap: 2 }}>
{[1, 2, 3, 4].map((_, i) => (
<Skeleton key={i} variant="rounded" width={80} height={36} />
))}
</>
);
};
export default ChartSkeleton;
</Box>
</Box>
);

View File

@ -101,53 +101,63 @@ const PrometheusChart = ({ metricName }) => {
}, []);
const processMetricsData = useCallback((response) => {
const processMetricsData = useCallback((response, replace = false) => {
console.log('Processing metrics data:', response);
if (response.metric !== metricName) return;
const dataArray = Array.isArray(response.data) ? response.data : [response.data];
if (!dataArray.length) return;
setChartData(prev => {
const newData = { ...(prev || {}) };
const rangeSeconds = useCustomRange
? (endDate.getTime() - startDate.getTime()) / 1000
: selectedRange.value;
const newData = {};
const rangeSeconds = useCustomRange
? (endDate.getTime() - startDate.getTime()) / 1000
: selectedRange.value;
dataArray.forEach(item => {
const instance = item.instance || 'default';
if (!newData[instance]) newData[instance] = [];
dataArray.forEach(item => {
const instance = item.instance || 'default';
if (!newData[instance]) newData[instance] = [];
// Унифицированная конвертация timestamp
let timestamp;
if (typeof item.timestamp === 'number') {
// Определяем, в секундах или миллисекундах пришел timestamp
timestamp = item.timestamp > 1e12 ? item.timestamp : item.timestamp * 1000;
} else {
timestamp = Date.now();
}
let timestamp;
if (typeof item.timestamp === 'number') {
timestamp = item.timestamp > 1e12 ? item.timestamp : item.timestamp * 1000;
} else {
timestamp = Date.now();
}
const value = parseFloat(item.value);
const formattedTime = formatTime(timestamp, rangeSeconds);
const value = parseFloat(item.value);
const formattedTime = formatTime(timestamp, rangeSeconds);
newData[instance].push({
time: formattedTime.display,
fullTime: formattedTime.fullDisplay,
value: value,
timestamp: timestamp
});
newData[instance].push({
time: formattedTime.display,
fullTime: formattedTime.fullDisplay,
value,
timestamp
});
// Сортируем и ограничиваем данные
Object.keys(newData).forEach(instance => {
newData[instance] = newData[instance]
.sort((a, b) => a.timestamp - b.timestamp)
.slice(-1000);
});
return newData;
});
Object.keys(newData).forEach(instance => {
newData[instance] = newData[instance]
.sort((a, b) => a.timestamp - b.timestamp)
.slice(-1000);
});
if (replace) {
setChartData(newData); // Заменяем полностью
} else {
setChartData(prev => {
const merged = { ...(prev || {}) };
Object.keys(newData).forEach(instance => {
if (!merged[instance]) merged[instance] = [];
merged[instance] = [...merged[instance], ...newData[instance]]
.sort((a, b) => a.timestamp - b.timestamp)
.slice(-1000);
});
return merged;
});
}
}, [metricName, selectedRange.value, formatTime, useCustomRange, startDate, endDate]);
const fetchData = useCallback(() => {
if (isSelectingRange) return;
@ -197,7 +207,7 @@ const PrometheusChart = ({ metricName }) => {
processMetricsData({
metric: metricName,
data: processedData
});
}, true);
}
} catch (error) {
console.error('Ошибка при получении кастомных данных:', error);
@ -340,7 +350,9 @@ const PrometheusChart = ({ metricName }) => {
useEffect(() => {
// Обработчик данных с сервера
const handleMetricsData = (data) => {
processMetricsData({ metric: metricName, data });
if (!useCustomRange) {
processMetricsData({ metric: metricName, data });
}
};
// Подписываемся на обновления метрики
@ -359,7 +371,8 @@ const PrometheusChart = ({ metricName }) => {
clearInterval(intervalRef.current);
}
};
}, [metricName, processMetricsData]);
}, [metricName, useCustomRange, processMetricsData]);
useEffect(() => {
if (useCustomRange && !isSelectingRange) {

View File

@ -1,4 +1,3 @@
// src/services/WebSocketManager.js
import { io } from 'socket.io-client';
class WebSocketManager {
@ -7,13 +6,16 @@ class WebSocketManager {
this.subscribers = new Map();
this.connectionStatus = 'disconnected';
this.connectionCallbacks = new Set();
this.connecting = false;
}
connect() {
if (this.socket && (this.socket.connected || this.socket.reconnecting)) {
if (this.socket?.connected || this.connecting) {
return this.socket;
}
this.connecting = true;
this.socket = io(`${import.meta.env.VITE_BACK_WS_URL}/api/metrics-ws`, {
transports: ['websocket'],
reconnection: true,
@ -24,11 +26,13 @@ class WebSocketManager {
this.socket.on('connect', () => {
this.connectionStatus = 'connected';
this.connecting = false;
this.notifyConnectionStatus();
});
this.socket.on('disconnect', (reason) => {
this.connectionStatus = 'disconnected';
this.connecting = false;
this.notifyConnectionStatus();
if (reason === 'io server disconnect') this.socket.connect();
});
@ -50,7 +54,9 @@ class WebSocketManager {
}
subscribe(metricName, callback) {
this.connect();
if (!this.socket?.connected) {
this.connect();
}
if (!this.subscribers.has(metricName)) {
this.subscribers.set(metricName, new Set());

View File

@ -1,9 +1,30 @@
import SystemStatusChart from "../../Charts/SystemStatusChart";
import TreeTable from "../UI/TreeTable";
import FlowChart from "../TreeChart/FlowChart";
import { getStatusColor } from "../TreeChart/dataUtils";
const TabContent = ({ activeTab, statusHistories, treeData1, tabContent, handleOpenTab }) => {
// Функция для подсчета количества элементов каждого статуса
const countStatuses = (data) => {
const counts = { green: 0, yellow: 0, orange: 0, red: 0 };
const countRecursive = (node) => {
if (node.status) {
counts[node.status]++;
}
if (node.items && node.items.length > 0) {
node.items.forEach(child => countRecursive(child));
}
};
countRecursive(data);
return counts;
};
if (activeTab === "Главная") {
const statusCounts = treeData1 ? countStatuses(treeData1) : { green: 0, yellow: 0, orange: 0, red: 0 };
return (
<div>
<h2 style={{ textAlign: 'center' }}>Общий мониторинг состояния системы</h2>
@ -17,6 +38,32 @@ const TabContent = ({ activeTab, statusHistories, treeData1, tabContent, handleO
<SystemStatusChart data={statusHistories.history2} />
</div>
</div>
{/* Контейнер для индикаторов статусов */}
<div style={{
display: 'flex',
justifyContent: 'flex-end',
marginTop: '20px',
gap: '10px'
}}>
{Object.entries(statusCounts).map(([status, count]) => (
<div key={status} style={{
width: '30px',
height: '30px',
backgroundColor: getStatusColor(status),
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
color: 'white',
fontWeight: 'bold',
borderRadius: '5px',
boxShadow: '0 2px 5px rgba(0,0,0,0.2)'
}}>
{count}
</div>
))}
</div>
<label>Статус компонентов системы</label>
<TreeTable data={treeData1} />
</div>