graph refactor

pull/40/head
DmitriyA 2025-05-05 09:11:48 -04:00
parent b9a2be4860
commit 4dfd972615
14 changed files with 616 additions and 190 deletions

View File

@ -0,0 +1,97 @@
import React from 'react';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
const DateRangeSelector = ({
startDate,
endDate,
onStartDateChange,
onEndDateChange,
onApply
}) => {
return (
<div style={{
marginTop: 10,
backgroundColor: '#f5f5f5',
padding: '15px',
borderRadius: '4px'
}}>
<div style={{
marginBottom: '10px',
fontWeight: '500',
color: '#555'
}}>
Укажите диапазон дат:
</div>
<div style={{
display: 'flex',
gap: '10px',
flexWrap: 'wrap',
alignItems: 'flex-end'
}}>
<div style={{ flex: '1 1 200px' }}>
<DatePicker
selected={startDate}
onChange={onStartDateChange}
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={onEndDateChange}
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={onApply}
style={{
padding: '8px 16px',
backgroundColor: '#4a6baf',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
transition: 'background-color 0.2s',
flex: '0 0 auto',
height: '36px'
}}
onMouseOver={(e) => e.target.style.backgroundColor = '#3a5a9f'}
onMouseOut={(e) => e.target.style.backgroundColor = '#4a6baf'}
>
Применить
</button>
</div>
</div>
);
};
export default DateRangeSelector;

View File

@ -0,0 +1,68 @@
import React from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
const LineChartComponent = ({
data,
title,
description,
metaInfo,
dataKey = 'value',
lineColor = '#8884d8',
height = 400,
showLegend = true,
showGrid = true,
customTooltip,
customXAxisFormatter,
customYAxis,
additionalLines = []
}) => {
return (
<div style={{ width: '100%', height: '560' }}>
{title && <h3>{title}</h3>}
{description && (
<p style={{ marginTop: -10, color: '#666' }}>{description}</p>
)}
{metaInfo && (
<div style={{ fontSize: 12, color: '#888', marginBottom: 10 }}>
{metaInfo}
</div>
)}
<ResponsiveContainer width="100%" height="80%">
<LineChart
data={data}
margin={{
top: 5,
right: 30,
left: 20,
bottom: 5,
}}
>
{showGrid && <CartesianGrid strokeDasharray="3 3" />}
<XAxis
dataKey="timestamp"
tickFormatter={customXAxisFormatter || ((timestamp) => new Date(timestamp).toLocaleTimeString())}
/>
{customYAxis || <YAxis />}
<Tooltip
content={customTooltip}
labelFormatter={(timestamp) => new Date(timestamp).toLocaleString()}
/>
{showLegend && <Legend />}
<Line
type="monotone"
dataKey={dataKey}
stroke={lineColor}
activeDot={{ r: 8 }}
name={title}
/>
{additionalLines.map((lineProps, index) => (
<Line key={index} {...lineProps} />
))}
</LineChart>
</ResponsiveContainer>
</div>
);
};
export default LineChartComponent;

View File

@ -0,0 +1,142 @@
import { io } from 'socket.io-client';
class MetricsService {
constructor(baseUrl) {
console.log('MetricsService constructor');
this.baseUrl = baseUrl || window.location.origin;
this.socket = null;
this.subscriptions = new Map();
}
// HTTP методы - адаптированы под ваш бэкенд
async fetchMetricsRange(metric, start, end, step = 15) {
try {
// Формируем URL согласно вашему API
const url = new URL(`${this.baseUrl}/api/metrics`);
url.searchParams.append('metric', metric);
url.searchParams.append('start', start);
url.searchParams.append('end', end);
url.searchParams.append('step', step);
console.log('Fetching metrics range from:', url.toString());
const response = await fetch(url.toString());
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
const data = await response.json();
// Проверяем формат данных
if (!Array.isArray(data)) {
console.error('Unexpected data format:', data);
throw new Error('Invalid data format: expected array');
}
return data;
} catch (error) {
console.error('Error in fetchMetricsRange:', error);
throw error;
}
}
async fetchMetrics(metric) {
try {
// Формируем URL для текущих метрик
const url = new URL(`${this.baseUrl}/api/metrics`);
url.searchParams.append('metric', metric);
console.log('Fetching current metrics from:', url.toString());
const response = await fetch(url.toString());
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
const data = await response.json();
// Проверяем формат данных
if (!Array.isArray(data)) {
console.error('Unexpected data format:', data);
throw new Error('Invalid data format: expected array');
}
return data;
} catch (error) {
console.error('Error in fetchMetrics:', error);
throw error;
}
}
// WebSocket методы - остаются без изменений
connectWebSocket() {
if (this.socket && this.socket.connected) return;
console.trace('connectWebSocket called');
this.socket = io(`${this.baseUrl}/api/metrics-ws`, {
transports: ['websocket'],
withCredentials: true,
});
this.socket.on('connect', () => {
console.log('Socket.IO connected');
// Подписаться заново на все метрики
for (const [metric, callbacks] of this.subscriptions.entries()) {
this.socket.emit('subscribe-metric', { metric });
}
});
this.socket.on('disconnect', () => {
console.log('Socket.IO disconnected');
});
this.socket.on('metrics-data', ({ metric, data }) => {
const callbacks = this.subscriptions.get(metric) || [];
callbacks.forEach(cb => cb(data));
});
this.socket.on('metrics-error', payload => {
console.error('Metrics error:', payload);
});
}
subscribeToMetric(metric, callback, interval = 5000) {
this.connectWebSocket();
if (!this.subscriptions.has(metric)) {
this.subscriptions.set(metric, []);
this.socket.emit('subscribe-metric', { metric, interval });
}
this.subscriptions.get(metric).push(callback);
return () => this.unsubscribeFromMetric(metric, callback);
}
unsubscribeFromMetric(metric, callback) {
const callbacks = this.subscriptions.get(metric) || [];
const filtered = callbacks.filter(cb => cb !== callback);
if (filtered.length === 0) {
this.subscriptions.delete(metric);
if (this.socket && this.socket.connected) {
this.socket.emit('unsubscribe-metric', { metric });
}
} else {
this.subscriptions.set(metric, filtered);
}
}
disconnectWebSocket() {
if (this.socket) {
this.socket.close();
}
}
}
export const metricsService = new MetricsService(import.meta.env.VITE_BACK_URL);

View File

@ -0,0 +1,169 @@
import React, { useState, useEffect } from 'react';
import LineChartComponent from './Components/LineChartComponent';
import DateRangeSelector from './Components/DateRangeSelector';
import { metricsService } from './Components/metricsService';
import { Button, Radio, message } from 'antd';
import moment from 'moment';
const PrometheusChart = ({ metricName, chartHeight = 560 }) => {
const [chartData, setChartData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const [metricInfo, setMetricInfo] = useState({});
const [mode, setMode] = useState('realtime');
const [startDate, setStartDate] = useState(moment().subtract(1, 'hour').toDate());
const [endDate, setEndDate] = useState(moment().toDate());
const [isLiveUpdating, setIsLiveUpdating] = useState(false);
const fetchHistoricalData = async (start, end) => {
setIsLoading(true);
setError(null);
try {
const startUnix = Math.floor(new Date(start).getTime() / 1000);
const endUnix = Math.floor(new Date(end).getTime() / 1000);
const data = await metricsService.fetchMetricsRange(metricName, startUnix, endUnix, 15);
const dataArray = Array.isArray(data) ? data : [data];
const formattedData = dataArray.map(item => ({
timestamp: item.timestamp,
value: parseFloat(item.value),
name: item.__name__ || metricName,
status: item.status
}));
if (dataArray.length > 0) {
setMetricInfo({
type: dataArray[0].type,
description: dataArray[0].description,
instance: dataArray[0].instance,
job: dataArray[0].job
});
}
setChartData(formattedData);
} catch (err) {
console.error(`Error loading historical data for ${metricName}:`, err);
setError(err.message);
message.error(`Failed to load historical data: ${err.message}`);
} finally {
setIsLoading(false);
}
};
const startRealtimeUpdates = () => {
setIsLiveUpdating(true);
setIsLoading(true);
const end = new Date();
const start = new Date(end.getTime() - 3600 * 1000);
fetchHistoricalData(start, end).finally(() => {
setIsLoading(false);
});
return metricsService.subscribeToMetric(
metricName,
(newData) => {
const newDataArray = Array.isArray(newData) ? newData : [newData];
const formattedNewData = newDataArray.map(item => ({
timestamp: item.timestamp,
value: parseFloat(item.value),
name: item.__name__ || metricName,
status: item.status
}));
setChartData(prevData => [...prevData, ...formattedNewData].slice(-200));
},
5000
);
};
const stopRealtimeUpdates = () => {
setIsLiveUpdating(false);
metricsService.unsubscribeFromMetric(metricName);
};
const handleCustomRangeApply = () => {
if (startDate && endDate) {
fetchHistoricalData(startDate, endDate);
}
};
useEffect(() => {
let unsubscribe;
if (mode === 'realtime') {
unsubscribe = startRealtimeUpdates();
} else {
stopRealtimeUpdates();
fetchHistoricalData(startDate, endDate);
}
return () => {
if (unsubscribe) unsubscribe();
stopRealtimeUpdates();
};
}, [mode, metricName]);
const metaInfo = [
metricInfo.instance && `Instance: ${metricInfo.instance}`,
metricInfo.job && `Job: ${metricInfo.job}`,
metricInfo.type && `Type: ${metricInfo.type}`
].filter(Boolean).join(' | ');
return (
<div>
<div style={{ marginBottom: 16 }}>
<Radio.Group
value={mode}
onChange={(e) => setMode(e.target.value)}
buttonStyle="solid"
style={{ marginBottom: 10 }}
>
<Radio.Button value="realtime">Режим реального времени</Radio.Button>
<Radio.Button value="historical">Исторические данные</Radio.Button>
</Radio.Group>
{mode === 'historical' && (
<DateRangeSelector
startDate={startDate}
endDate={endDate}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
onApply={handleCustomRangeApply}
/>
)}
{mode === 'realtime' && isLiveUpdating && (
<Button
type="primary"
danger
onClick={() => setMode('historical')}
style={{ marginTop: 10 }}
>
Остановить обновление
</Button>
)}
</div>
{isLoading ? (
<div>Loading chart data...</div>
) : error ? (
<div>Error loading metric: {error}</div>
) : chartData.length === 0 ? (
<div>No data available for {metricName}</div>
) : (
<LineChartComponent
data={chartData}
title={metricName}
description={metricInfo.description}
metaInfo={metaInfo}
height={chartHeight}
/>
)}
</div>
);
};
export default PrometheusChart;

View File

@ -15,7 +15,7 @@ import { getStatusColor } from "../../TreeChart/dataUtils";
const StyledListItem = styled(ListItem)(({ theme, level }) => ({ const StyledListItem = styled(ListItem)(({ theme, level }) => ({
cursor: "pointer", cursor: "pointer",
paddingLeft: theme.spacing(2 + level * 2), paddingLeft: theme.spacing(2 + level * 2),
position: 'relative', // Добавляем для позиционирования индикатора position: 'relative',
'&:hover': { '&:hover': {
backgroundColor: theme.palette.action.hover, backgroundColor: theme.palette.action.hover,
}, },

View File

@ -45,12 +45,10 @@ const FlowChart = ({ data }) => {
const findAndCollapseLastLevelParents = (items) => { const findAndCollapseLastLevelParents = (items) => {
items.forEach(item => { items.forEach(item => {
if (item.items && item.items.length > 0) { if (item.items && item.items.length > 0) {
// Проверяем, есть ли у детей свои дети
const hasGrandchildren = item.items.some(child => const hasGrandchildren = item.items.some(child =>
child.items && child.items.length > 0 child.items && child.items.length > 0
); );
// Если у детей нет своих детей - это родители последнего уровня
if (!hasGrandchildren) { if (!hasGrandchildren) {
toggleNodeCollapse(item.id); toggleNodeCollapse(item.id);
} else { } else {

View File

@ -39,7 +39,7 @@ export const useDataParser = (nodePositions, collapsedNodes) => {
const baseLevelRadius = 150; const baseLevelRadius = 150;
const traverse = (item, parentId = null, level = 0, angleStart = 0, angleEnd = 2 * Math.PI, parentRadius = 0) => { const traverse = (item, parentId = null, level = 0, angleStart = 0, angleEnd = 2 * Math.PI, parentRadius = 0) => {
if (!item || collapsedNodes[parentId]) return; // Пропускаем свёрнутые узлы if (!item || collapsedNodes[parentId]) return;
const nodeId = item.id; const nodeId = item.id;
const items = item.items || []; const items = item.items || [];
@ -58,7 +58,7 @@ export const useDataParser = (nodePositions, collapsedNodes) => {
data: { data: {
...item, ...item,
label: item.title, label: item.title,
style: getNodeStyle(item, isLeaf), // Переносим стили в data style: getNodeStyle(item, isLeaf),
hasChildren: items.length > 0, hasChildren: items.length > 0,
collapsed: collapsedNodes[nodeId] collapsed: collapsedNodes[nodeId]
} }
@ -88,7 +88,7 @@ export const useDataParser = (nodePositions, collapsedNodes) => {
const centerNode = { const centerNode = {
id: data.id, id: data.id,
type: 'customNode', // Добавляем тип узла type: 'customNode',
position: nodePositions[data.id] || { x: centerX, y: centerY }, position: nodePositions[data.id] || { x: centerX, y: centerY },
style: getCenterNodeStyle(data), style: getCenterNodeStyle(data),
data: { label: data.title, hasChildren: data.items.length > 0, collapsed: collapsedNodes[data.id] } data: { label: data.title, hasChildren: data.items.length > 0, collapsed: collapsedNodes[data.id] }

View File

@ -10,13 +10,13 @@ const NodeWrapper = memo(({ id, data, selected }) => {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
overflow: 'hidden', // Чтобы текст не выходил за границы overflow: 'hidden',
textOverflow: 'ellipsis', // Добавляем многоточие если текст не помещается textOverflow: 'ellipsis',
whiteSpace: 'nowrap', // Запрещаем перенос строк whiteSpace: 'nowrap',
padding: '0 8px', // Горизонтальный padding для текста padding: '0 8px',
boxSizing: 'border-box' // Учитываем padding в общей ширине boxSizing: 'border-box'
}} }}
title={data.label} // Простой tooltip при наведении title={data.label}
> >
{/* Хендл для входящих соединений */} {/* Хендл для входящих соединений */}
<Handle <Handle

View File

@ -6,7 +6,7 @@ export const useFlowChart = (initialData) => {
const [nodes, setNodes, onNodesChange] = useNodesState([]); const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [nodePositions, setNodePositions] = useState({}); const [nodePositions, setNodePositions] = useState({});
const [collapsedNodes, setCollapsedNodes] = useState({}); // Добавили const [collapsedNodes, setCollapsedNodes] = useState({});
const toggleNodeCollapse = useCallback((nodeId) => { const toggleNodeCollapse = useCallback((nodeId) => {
setCollapsedNodes((prev) => ({ setCollapsedNodes((prev) => ({

View File

@ -2,7 +2,6 @@ import { useCallback } from 'react';
export const useNodeHandlers = (debouncedSetNodePositions) => { export const useNodeHandlers = (debouncedSetNodePositions) => {
const onNodeDrag = useCallback((event, node) => { const onNodeDrag = useCallback((event, node) => {
// Фиксируем позицию сразу при перемещении
node.position = { node.position = {
x: Math.round(node.position.x), x: Math.round(node.position.x),
y: Math.round(node.position.y) y: Math.round(node.position.y)

View File

@ -1,48 +1,43 @@
const StatusManager = () => { const StatusManager = () => {
const getRandomStatus = () => { const getRandomStatus = () => {
const statuses = [ const statuses = [
...Array(90).fill("green"), // 90% шанс ...Array(90).fill("green"),
...Array(6).fill("yellow"), // 6% шанс ...Array(6).fill("yellow"),
...Array(3).fill("orange"), // 3% шанс ...Array(3).fill("orange"),
...Array(1).fill("red"), // 1% шанс ...Array(1).fill("red"),
]; ];
return statuses[Math.floor(Math.random() * statuses.length)]; return statuses[Math.floor(Math.random() * statuses.length)];
}; };
const getStatusWeight = (status) => { const getStatusWeight = (status) => {
switch (status) { switch (status) {
case "green": return 1; // 100% здоровья case "green": return 1;
case "yellow": return 0.75; case "yellow": return 0.75;
case "orange": return 0.5; case "orange": return 0.5;
case "red": return 0.25; // 25% здоровья case "red": return 0.25;
default: return 1; // По умолчанию "green" default: return 1;
} }
}; };
const updateStatuses = (data) => { const updateStatuses = (data) => {
if (!data.items || data.items.length === 0) { if (!data.items || data.items.length === 0) {
// Если это элемент нижнего уровня, генерируем случайный статус
data.status = getRandomStatus(); data.status = getRandomStatus();
return getStatusWeight(data.status); return getStatusWeight(data.status);
} }
// Рекурсивно обновляем статусы для всех дочерних элементов
let childStatusWeights = data.items.map((child) => updateStatuses(child)); let childStatusWeights = data.items.map((child) => updateStatuses(child));
// Проверяем, есть ли дочерние элементы (избегаем деления на 0)
if (childStatusWeights.length === 0) { if (childStatusWeights.length === 0) {
data.status = "green"; data.status = "green";
return 1; return 1;
} }
// Вычисляем среднее арифметическое значение весов статусов
const averageStatusWeight = const averageStatusWeight =
childStatusWeights.reduce((sum, weight) => sum + weight, 0) / childStatusWeights.length; childStatusWeights.reduce((sum, weight) => sum + weight, 0) / childStatusWeights.length;
// Определяем статус текущего элемента
data.status = getStatusFromWeight(averageStatusWeight); data.status = getStatusFromWeight(averageStatusWeight);
return Math.max(0, averageStatusWeight); // Гарантия, что не будет отрицательных значений return Math.max(0, averageStatusWeight);
}; };
const getStatusFromWeight = (weight) => { const getStatusFromWeight = (weight) => {
@ -69,16 +64,13 @@ const StatusManager = () => {
}; };
}; };
// Создаем два независимых менеджера статусов
export const statusManager1 = StatusManager(); export const statusManager1 = StatusManager();
export const statusManager2 = StatusManager(); export const statusManager2 = StatusManager();
// Функция для расчета процентов здоровья системы
export const calculateStatusPercentage = (averageStatusValue) => { export const calculateStatusPercentage = (averageStatusValue) => {
return Math.max(0, Math.min(100, averageStatusValue * 100)); return Math.max(0, Math.min(100, averageStatusValue * 100));
}; };
// Экспортируем getStatusColor отдельно
export const getStatusColor = (status) => { export const getStatusColor = (status) => {
switch (status) { switch (status) {
case "green": case "green":

View File

@ -400,182 +400,136 @@
"items": [ "items": [
{ {
"id": "182", "id": "182",
"title": "Graviton S2082I (device$18)", "title": "Graviton S2082I (device$19)",
"items": [ "items": [
{ {
"id": "42", "id": "42",
"title": "OS Linux (module$4) АО", "title": "OS Linux (module$6) АО",
"items": [ "items": [
{ {
"id": "1902", "id": "371",
"title": "Загрузка процессора за 1 минуту" "title": "Загрузка процессора за 1 минуту"
}, },
{ {
"id": "1912", "id": "372",
"title": "Загрузка процессора за 5 минут" "title": "Загрузка процессора за 5 минут"
}, },
{ {
"id": "1922", "id": "373",
"title": "Загрузка процессора за 15 минут" "title": "Загрузка процессора за 15 минут"
}, },
{ {
"id": "1972", "id": "378",
"title": "Общий объем SWAP-файла" "title": "Общий объем SWAP-файла"
}, },
{ {
"id": "1982", "id": "379",
"title": "Используемый объем SWAP-файла" "title": "Используемый объем SWAP-файла"
}, },
{ {
"id": "1992", "id": "380",
"title": "Общий объем физической оперативной памяти" "title": "Общий объем физической оперативной памяти"
}, },
{ {
"id": "2002", "id": "381",
"title": "Доступный объем физической оперативной памяти" "title": "Доступный объем физической оперативной памяти"
}, },
{ {
"id": "2012", "id": "382",
"title": "Свободный объем физической и виртуальной оперативной памяти" "title": "Свободный объем физической и виртуальной оперативной памяти"
}, },
{ {
"id": "2022", "id": "383",
"title": "Буферизованный объем оперативной памяти" "title": "Буферизованный объем оперативной памяти"
}, },
{ {
"id": "2032", "id": "384",
"title": "Кэшированый объем оперативной памяти" "title": "Кэшированый объем оперативной памяти"
}, },
{ {
"id": "2742", "id": "375",
"title": "Используемый объем SWAP-файла"
},
{
"id": "2752",
"title": "Время затраченное процессором на процессы с пониженным приоритетом" "title": "Время затраченное процессором на процессы с пониженным приоритетом"
}, },
{ {
"id": "2762", "id": "376",
"title": "Время затраченное процессором на процессы ядра ОС" "title": "Время затраченное процессором на процессы ядра ОС"
}, },
{ {
"id": "2772", "id": "377",
"title": "Время простоя процессора" "title": "Время простоя процессора"
}, },
{ {
"id": "2782", "id": "385",
"title": "Общая емкость жестких дисков" "title": "Общая емкость жестких дисков"
}, },
{ {
"id": "2792", "id": "386",
"title": "Доступная емкость жестких дисков" "title": "Доступная емкость жестких дисков"
} }
] ]
}, },
{
"id": "52",
"title": "Vinteo (module$5) ПО",
"items": [
{
"id": "312",
"title": "Общее количество участников"
},
{
"id": "322",
"title": "Ожидание соединения"
},
{
"id": "332",
"title": "Зарегистрированные абоненты"
},
{
"id": "342",
"title": "Количество пользоватей HLS"
},
{
"id": "352",
"title": "Общее количество P2P комнат"
},
{
"id": "362",
"title": "Общее количество конференций"
},
{
"id": "372",
"title": "Общее количество активных конференций"
},
{
"id": "382",
"title": "Статус записи"
},
{
"id": "392",
"title": "Общее количество сохранённых записей"
}
]
},
{ {
"id": "2802", "id": "2802",
"title": "Сетевой адаптер №1 (port$261) Eth_1", "title": "Сетевой адаптер №1 (port$261) Eth_1",
"items": [ "items": [
{ {
"id": "2072", "id": "388",
"title": "Скорость порта Eth_1" "title": "Скорость порта Eth_1"
}, },
{ {
"id": "2092", "id": "390",
"title": "Административное состояние порта Eth_1" "title": "Административное состояние порта Eth_1"
}, },
{ {
"id": "2102", "id": "391",
"title": "Оперативное состояние порта Eth_1" "title": "Оперативное состояние порта Eth_1"
}, },
{ {
"id": "2112", "id": "392",
"title": "Общее количество отправленных октетов Eth_1" "title": "Общее количество отправленных октетов Eth_1"
}, },
{ {
"id": "2122", "id": "393",
"title": "Количество входящих Multicast пакетов Eth_1" "title": "Количество входящих Multicast пакетов Eth_1"
}, },
{ {
"id": "2132", "id": "394",
"title": "Количество иcходящих Multiicast пакетов Eth_1" "title": "Количество иcходящих Multiicast пакетов Eth_1"
}, },
{ {
"id": "2142", "id": "395",
"title": "Количество входящих Broadcast пакетов Eth_1" "title": "Количество входящих Broadcast пакетов Eth_1"
}, },
{ {
"id": "2152", "id": "396",
"title": "Количество иcходящих Broadcast пакетов Eth_1" "title": "Количество иcходящих Broadcast пакетов Eth_1"
}, },
{ {
"id": "2162", "id": "397",
"title": "Количество входящих Unicast пакетов Eth_1" "title": "Количество входящих Unicast пакетов Eth_1"
}, },
{ {
"id": "2172", "id": "398",
"title": "Количество иcходящих Unicast пакетов Eth_1" "title": "Количество иcходящих Unicast пакетов Eth_1"
}, },
{ {
"id": "2182", "id": "399",
"title": "Количество входящих пакетов помеченные как отброшенные Eth_1" "title": "Количество входящих пакетов помеченные как отброшенные Eth_1"
}, },
{ {
"id": "2192", "id": "400",
"title": "Количество иcходящих пакетов помеченные как отброшенные Eth_1" "title": "Количество иcходящих пакетов помеченные как отброшенные Eth_1"
}, },
{ {
"id": "2202", "id": "401",
"title": "Количество входящих пакетов с ошибкой Eth_1" "title": "Количество входящих пакетов с ошибкой Eth_1"
}, },
{ {
"id": "2212", "id": "402",
"title": "Количество исходящих пакетов с ошибкой Eth_1" "title": "Количество исходящих пакетов с ошибкой Eth_1"
}, },
{ {
"id": "2222", "id": "403",
"title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_1" "title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_1"
} }
] ]
@ -585,63 +539,63 @@
"title": "Сетевой адаптер №2 (port$262) Eth_2", "title": "Сетевой адаптер №2 (port$262) Eth_2",
"items": [ "items": [
{ {
"id": "2242", "id": "405",
"title": "Скорость порта Eth_2" "title": "Скорость порта Eth_2"
}, },
{ {
"id": "2262", "id": "407",
"title": "Административное состояние порта Eth_2" "title": "Административное состояние порта Eth_2"
}, },
{ {
"id": "2272", "id": "408",
"title": "Оперативное состояние порта Eth_2" "title": "Оперативное состояние порта Eth_2"
}, },
{ {
"id": "2282", "id": "409",
"title": "Общее количество отправленных октетов Eth_2" "title": "Общее количество отправленных октетов Eth_2"
}, },
{ {
"id": "2292", "id": "410",
"title": "Количество входящих Multicast пакетов Eth_2" "title": "Количество входящих Multicast пакетов Eth_2"
}, },
{ {
"id": "2302", "id": "411",
"title": "Количество иcходящих Multiicast пакетов Eth_2" "title": "Количество иcходящих Multiicast пакетов Eth_2"
}, },
{ {
"id": "2312", "id": "412",
"title": "Количество входящих Broadcast пакетов Eth_2" "title": "Количество входящих Broadcast пакетов Eth_2"
}, },
{ {
"id": "2322", "id": "413",
"title": "Количество иcходящих Broadcast пакетов Eth_2" "title": "Количество иcходящих Broadcast пакетов Eth_2"
}, },
{ {
"id": "2332", "id": "414",
"title": "Количество входящих Unicast пакетов Eth_2" "title": "Количество входящих Unicast пакетов Eth_2"
}, },
{ {
"id": "2342", "id": "415",
"title": "Количество иcходящих Unicast пакетов Eth_2" "title": "Количество иcходящих Unicast пакетов Eth_2"
}, },
{ {
"id": "2352", "id": "416",
"title": "Количество входящих пакетов помеченные как отброшенные Eth_2" "title": "Количество входящих пакетов помеченные как отброшенные Eth_2"
}, },
{ {
"id": "2362", "id": "417",
"title": "Количество иcходящих пакетов помеченные как отброшенные Eth_2" "title": "Количество иcходящих пакетов помеченные как отброшенные Eth_2"
}, },
{ {
"id": "2372", "id": "418",
"title": "Количество входящих пакетов с ошибкой Eth_2" "title": "Количество входящих пакетов с ошибкой Eth_2"
}, },
{ {
"id": "2382", "id": "419",
"title": "Количество исходящих пакетов с ошибкой Eth_2" "title": "Количество исходящих пакетов с ошибкой Eth_2"
}, },
{ {
"id": "2392", "id": "420",
"title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_2" "title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_2"
} }
] ]
@ -651,63 +605,63 @@
"title": "Сетевой адаптер №3 (port$263) Eth_3", "title": "Сетевой адаптер №3 (port$263) Eth_3",
"items": [ "items": [
{ {
"id": "2412", "id": "422",
"title": "Скорость порта Eth_3" "title": "Скорость порта Eth_3"
}, },
{ {
"id": "2432", "id": "424",
"title": "Административное состояние порта Eth_3" "title": "Административное состояние порта Eth_3"
}, },
{ {
"id": "2442", "id": "425",
"title": "Оперативное состояние порта Eth_3" "title": "Оперативное состояние порта Eth_3"
}, },
{ {
"id": "2452", "id": "426",
"title": "Общее количество отправленных октетов Eth_3" "title": "Общее количество отправленных октетов Eth_3"
}, },
{ {
"id": "2462", "id": "427",
"title": "Количество входящих Multicast пакетов Eth_3" "title": "Количество входящих Multicast пакетов Eth_3"
}, },
{ {
"id": "2472", "id": "428",
"title": "Количество иcходящих Multiicast пакетов Eth_3" "title": "Количество иcходящих Multiicast пакетов Eth_3"
}, },
{ {
"id": "2482", "id": "429",
"title": "Количество входящих Broadcast пакетов Eth_3" "title": "Количество входящих Broadcast пакетов Eth_3"
}, },
{ {
"id": "2492", "id": "430",
"title": "Количество иcходящих Broadcast пакетов Eth_3" "title": "Количество иcходящих Broadcast пакетов Eth_3"
}, },
{ {
"id": "2502", "id": "431",
"title": "Количество входящих Unicast пакетов Eth_3" "title": "Количество входящих Unicast пакетов Eth_3"
}, },
{ {
"id": "2512", "id": "432",
"title": "Количество иcходящих Unicast пакетов Eth_3" "title": "Количество иcходящих Unicast пакетов Eth_3"
}, },
{ {
"id": "2522", "id": "433",
"title": "Количество входящих пакетов помеченные как отброшенные Eth_3" "title": "Количество входящих пакетов помеченные как отброшенные Eth_3"
}, },
{ {
"id": "2532", "id": "434",
"title": "Количество иcходящих пакетов помеченные как отброшенные Eth_3" "title": "Количество иcходящих пакетов помеченные как отброшенные Eth_3"
}, },
{ {
"id": "2542", "id": "435",
"title": "Количество входящих пакетов с ошибкой Eth_3" "title": "Количество входящих пакетов с ошибкой Eth_3"
}, },
{ {
"id": "2552", "id": "436",
"title": "Количество исходящих пакетов с ошибкой Eth_3" "title": "Количество исходящих пакетов с ошибкой Eth_3"
}, },
{ {
"id": "2562", "id": "437",
"title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_3" "title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_3"
} }
] ]
@ -717,63 +671,63 @@
"title": "Сетевой адаптер №4 (port$264) Eth_4", "title": "Сетевой адаптер №4 (port$264) Eth_4",
"items": [ "items": [
{ {
"id": "2582", "id": "439",
"title": "Скорость порта Eth_4" "title": "Скорость порта Eth_4"
}, },
{ {
"id": "2602", "id": "441",
"title": "Административное состояние порта Eth_4" "title": "Административное состояние порта Eth_4"
}, },
{ {
"id": "2612", "id": "442",
"title": "Оперативное состояние порта Eth_4" "title": "Оперативное состояние порта Eth_4"
}, },
{ {
"id": "2622", "id": "443",
"title": "Общее количество отправленных октетов Eth_4" "title": "Общее количество отправленных октетов Eth_4"
}, },
{ {
"id": "2632", "id": "444",
"title": "Количество входящих Multicast пакетов Eth_4" "title": "Количество входящих Multicast пакетов Eth_4"
}, },
{ {
"id": "2642", "id": "445",
"title": "Количество иcходящих Multiicast пакетов Eth_4" "title": "Количество иcходящих Multiicast пакетов Eth_4"
}, },
{ {
"id": "2652", "id": "446",
"title": "Количество входящих Broadcast пакетов Eth_4" "title": "Количество входящих Broadcast пакетов Eth_4"
}, },
{ {
"id": "2662", "id": "447",
"title": "Количество иcходящих Broadcast пакетов Eth_4" "title": "Количество иcходящих Broadcast пакетов Eth_4"
}, },
{ {
"id": "2672", "id": "448",
"title": "Количество входящих Unicast пакетов Eth_4" "title": "Количество входящих Unicast пакетов Eth_4"
}, },
{ {
"id": "2682", "id": "449",
"title": "Количество иcходящих Unicast пакетов Eth_4" "title": "Количество иcходящих Unicast пакетов Eth_4"
}, },
{ {
"id": "2692", "id": "450",
"title": "Количество входящих пакетов помеченные как отброшенные Eth_4" "title": "Количество входящих пакетов помеченные как отброшенные Eth_4"
}, },
{ {
"id": "2702", "id": "451",
"title": "Количество иcходящих пакетов помеченные как отброшенные Eth_4" "title": "Количество иcходящих пакетов помеченные как отброшенные Eth_4"
}, },
{ {
"id": "2712", "id": "452",
"title": "Количество входящих пакетов с ошибкой Eth_4" "title": "Количество входящих пакетов с ошибкой Eth_4"
}, },
{ {
"id": "2722", "id": "453",
"title": "Количество исходящих пакетов с ошибкой Eth_4" "title": "Количество исходящих пакетов с ошибкой Eth_4"
}, },
{ {
"id": "2732", "id": "454",
"title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_4" "title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_4"
} }
] ]
@ -889,15 +843,15 @@
"title": "Общее количество конференций" "title": "Общее количество конференций"
}, },
{ {
"id": "373", "id": "373000",
"title": "Общее количество активных конференций" "title": "Общее количество активных конференций"
}, },
{ {
"id": "383", "id": "38300",
"title": "Статус записи" "title": "Статус записи"
}, },
{ {
"id": "393", "id": "39300",
"title": "Общее количество сохранённых записей" "title": "Общее количество сохранённых записей"
} }
] ]
@ -1281,11 +1235,11 @@
"title": "Общее количество активных конференций" "title": "Общее количество активных конференций"
}, },
{ {
"id": "384", "id": "38400",
"title": "Статус записи" "title": "Статус записи"
}, },
{ {
"id": "394", "id": "39400",
"title": "Общее количество сохранённых записей" "title": "Общее количество сохранённых записей"
} }
] ]
@ -1671,7 +1625,7 @@
"title": "Общее количество конференций" "title": "Общее количество конференций"
}, },
{ {
"id": "379", "id": "37900",
"title": "Общее количество активных конференций" "title": "Общее количество активных конференций"
}, },
{ {
@ -1679,7 +1633,7 @@
"title": "Статус записи" "title": "Статус записи"
}, },
{ {
"id": "399", "id": "39900",
"title": "Общее количество сохранённых записей" "title": "Общее количество сохранённых записей"
} }
] ]
@ -2447,15 +2401,15 @@
"title": "Общее количество конференций" "title": "Общее количество конференций"
}, },
{ {
"id": "378", "id": "37800",
"title": "Общее количество активных конференций" "title": "Общее количество активных конференций"
}, },
{ {
"id": "388", "id": "38800",
"title": "Статус записи" "title": "Статус записи"
}, },
{ {
"id": "398", "id": "39800",
"title": "Общее количество сохранённых записей" "title": "Общее количество сохранённых записей"
} }
] ]
@ -2841,15 +2795,15 @@
"title": "Общее количество конференций" "title": "Общее количество конференций"
}, },
{ {
"id": "375", "id": "37500",
"title": "Общее количество активных конференций" "title": "Общее количество активных конференций"
}, },
{ {
"id": "385", "id": "38500",
"title": "Статус записи" "title": "Статус записи"
}, },
{ {
"id": "395", "id": "39500",
"title": "Общее количество сохранённых записей" "title": "Общее количество сохранённых записей"
} }
] ]
@ -3229,15 +3183,15 @@
"title": "Общее количество конференций" "title": "Общее количество конференций"
}, },
{ {
"id": "376", "id": "37600",
"title": "Общее количество активных конференций" "title": "Общее количество активных конференций"
}, },
{ {
"id": "386", "id": "38600",
"title": "Статус записи" "title": "Статус записи"
}, },
{ {
"id": "396", "id": "39600",
"title": "Общее количество сохранённых записей" "title": "Общее количество сохранённых записей"
} }
] ]
@ -3617,7 +3571,7 @@
"title": "Общее количество конференций" "title": "Общее количество конференций"
}, },
{ {
"id": "377", "id": "37700",
"title": "Общее количество активных конференций" "title": "Общее количество активных конференций"
}, },
{ {
@ -3625,7 +3579,7 @@
"title": "Статус записи" "title": "Статус записи"
}, },
{ {
"id": "397", "id": "39700",
"title": "Общее количество сохранённых записей" "title": "Общее количество сохранённых записей"
} }
] ]

View File

@ -2,9 +2,10 @@ import React, { lazy, Suspense } from "react";
import Skeleton from '@mui/material/Skeleton'; import Skeleton from '@mui/material/Skeleton';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
const PrometheusChart = lazy(() => import('../../Charts/PrometheusChart')); const PrometheusChart = lazy(() => import('../../Charts2/PrometheusChart'));
import LazyChartBatchRenderer from "../hooks/LazyChartBatchRender"; import LazyChartBatchRenderer from "../hooks/LazyChartBatchRender";
// Функция для генерации названия метрики на основе id
const getMetricName = (id) => { const getMetricName = (id) => {
return `zvks_apiforsnmp_measure_${id}`; return `zvks_apiforsnmp_measure_${id}`;
}; };
@ -22,18 +23,19 @@ const getAllChildIds = (node) => {
return ids; return ids;
}; };
// Компонент Skeleton для графика
const ChartSkeleton = () => ( const ChartSkeleton = () => (
<Box sx={{ width: '100%' }}> <Box sx={{ width: '100%' }}>
<Skeleton variant="text" width="60%" height={30} /> {/* Заголовок */} <Skeleton variant="text" width="60%" height={30} />
<Skeleton variant="rectangular" width="100%" height={300} sx={{ mt: 2 }} /> {/* График */} <Skeleton variant="rectangular" width="100%" height={300} sx={{ mt: 2 }} />
</Box> </Box>
); );
// Компонент Skeleton для родительского контейнера
const ContainerSkeleton = () => ( const ContainerSkeleton = () => (
<Box sx={{ width: '100%' }}> <Box sx={{ width: '100%' }}>
<Skeleton variant="text" width="40%" height={40} /> {/* Заголовок */} <Skeleton variant="text" width="40%" height={40} /> {/* Заголовок */}
<Skeleton variant="text" width="80%" height={20} sx={{ mt: 1 }} /> {/* Описание */} <Skeleton variant="text" width="80%" height={20} sx={{ mt: 1 }} />
{/* Место для дочерних элементов */}
<Box sx={{ mt: 2 }}> <Box sx={{ mt: 2 }}>
{[...Array(3)].map((_, i) => ( {[...Array(3)].map((_, i) => (
<ChartSkeleton key={i} /> <ChartSkeleton key={i} />
@ -42,24 +44,22 @@ const ContainerSkeleton = () => (
</Box> </Box>
); );
const tabContent = (data, existingContent = {}) => { const tabContent = (data) => {
const tabContent = { ...existingContent }; const tabContent = {};
// Функция для рекурсивного обхода и сбора данных
const generateContent = (nodes) => { const generateContent = (nodes) => {
nodes.forEach((node) => { nodes.forEach((node) => {
if (tabContent[node.id]) return;
if (node.items && node.items.length > 0) { if (node.items && node.items.length > 0) {
generateContent(node.items); const childrenContent = generateContent(node.items);
const content = ( const content = (
<div key={node.id}> <div>
<h2>{node.title}</h2> <h2>{node.title}</h2>
<Suspense fallback={<ContainerSkeleton />}> <Suspense fallback={<ContainerSkeleton />}>
<LazyChartBatchRenderer <LazyChartBatchRenderer charts={node.items.map((child) => tabContent[child.id].content)} />
charts={node.items.map(child => tabContent[child.id]?.content) || <ChartSkeleton />}
/>
</Suspense> </Suspense>
<p>Контент для {node.title}.</p>
</div> </div>
); );
@ -77,17 +77,26 @@ const tabContent = (data, existingContent = {}) => {
</Suspense> </Suspense>
</div> </div>
); );
tabContent[node.id] = { tabContent[node.id] = {
title: node.title, title: node.title,
content: content, content: content,
}; };
} }
}); });
return (
<div>
{nodes.map((node) => (
<div key={node.id}>{tabContent[node.id].content}</div>
))}
</div>
);
}; };
if (data.items && data.items.length > 0) { if (data.items && data.items.length > 0) {
generateContent(data.items); generateContent(data.items);
} else {
console.warn("Данные отсутствуют или массив items пуст");
} }
return tabContent; return tabContent;

View File

@ -2,8 +2,6 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import './index.css' import './index.css'
import App from './App.jsx' import App from './App.jsx'
//import './Style/light-theme.css'; // Подключаем светлую тему по умолчанию
//import './Style/dark-theme.css'; // Подключаем темную тему
createRoot(document.getElementById('root')).render( createRoot(document.getElementById('root')).render(
<StrictMode> <StrictMode>