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 }) => ({
cursor: "pointer",
paddingLeft: theme.spacing(2 + level * 2),
position: 'relative', // Добавляем для позиционирования индикатора
position: 'relative',
'&:hover': {
backgroundColor: theme.palette.action.hover,
},

View File

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

View File

@ -39,7 +39,7 @@ export const useDataParser = (nodePositions, collapsedNodes) => {
const baseLevelRadius = 150;
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 items = item.items || [];
@ -58,7 +58,7 @@ export const useDataParser = (nodePositions, collapsedNodes) => {
data: {
...item,
label: item.title,
style: getNodeStyle(item, isLeaf), // Переносим стили в data
style: getNodeStyle(item, isLeaf),
hasChildren: items.length > 0,
collapsed: collapsedNodes[nodeId]
}
@ -88,7 +88,7 @@ export const useDataParser = (nodePositions, collapsedNodes) => {
const centerNode = {
id: data.id,
type: 'customNode', // Добавляем тип узла
type: 'customNode',
position: nodePositions[data.id] || { x: centerX, y: centerY },
style: getCenterNodeStyle(data),
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',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden', // Чтобы текст не выходил за границы
textOverflow: 'ellipsis', // Добавляем многоточие если текст не помещается
whiteSpace: 'nowrap', // Запрещаем перенос строк
padding: '0 8px', // Горизонтальный padding для текста
boxSizing: 'border-box' // Учитываем padding в общей ширине
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
padding: '0 8px',
boxSizing: 'border-box'
}}
title={data.label} // Простой tooltip при наведении
title={data.label}
>
{/* Хендл для входящих соединений */}
<Handle

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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