diff --git a/src/Charts2/Components/LineChartComponent.jsx b/src/Charts2/Components/LineChartComponent.jsx
index 4a5cea5..2f990f6 100644
--- a/src/Charts2/Components/LineChartComponent.jsx
+++ b/src/Charts2/Components/LineChartComponent.jsx
@@ -1,12 +1,12 @@
import React from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
-const LineChartComponent = ({
- data,
- title,
- description,
- metaInfo,
- dataKey = 'value',
+const LineChartComponent = ({
+ data,
+ title,
+ description,
+ metaInfo,
+ dataKey = 'value',
lineColor = '#8884d8',
height = 400,
showLegend = true,
@@ -17,7 +17,7 @@ const LineChartComponent = ({
additionalLines = []
}) => {
return (
-
+
{title &&
{title}
}
{description && (
{description}
@@ -27,7 +27,7 @@ const LineChartComponent = ({
{metaInfo}
)}
-
+
{
+ this.cleanupAll();
+ });
}
- // Инициализация WebSocket соединения
connectWebSocket() {
- if (this.socket && this.socket.connected) return;
+ if (this.socket) {
+ console.log('WebSocket already exists');
+ return;
+ }
+ console.log('Connecting WebSocket...');
this.socket = io(`${this.baseUrl.replace('http', 'ws')}/api/metrics-ws`, {
transports: ['websocket'],
withCredentials: true,
@@ -19,7 +26,6 @@ class MetricsService {
this.socket.on('connect', () => {
console.log('WebSocket connected');
- // Восстанавливаем подписки при переподключении
this.subscriptions.forEach((_, metric) => {
this.socket.emit('subscribe-metric', { metric });
});
@@ -27,10 +33,10 @@ class MetricsService {
this.socket.on('disconnect', () => {
console.log('WebSocket disconnected');
+ this.socket = null;
});
this.socket.on('metrics-data', ({ metric, data, requestId }) => {
- // Обработка исторических данных
if (requestId && this.pendingRequests.has(requestId)) {
const { resolve } = this.pendingRequests.get(requestId);
resolve(data);
@@ -38,7 +44,6 @@ class MetricsService {
return;
}
- // Обработка реального времени
const callbacks = this.subscriptions.get(metric) || [];
callbacks.forEach(cb => cb(data));
});
@@ -52,12 +57,11 @@ class MetricsService {
});
}
- // Запрос исторических данных через WebSocket
- async fetchMetricsRange(metric, start, end, step = 15) {
+ async fetchMetricsRange(metric, start, end, step = 15, filters = {}) {
return new Promise((resolve, reject) => {
this.connectWebSocket();
- const requestId = `range-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+ const requestId = `range-${Date.now()}`;
this.pendingRequests.set(requestId, { resolve, reject });
this.socket.emit('get-metrics', {
@@ -65,35 +69,35 @@ class MetricsService {
start,
end,
step,
+ filters,
isRangeQuery: true,
requestId
});
- // Таймаут запроса
setTimeout(() => {
if (this.pendingRequests.has(requestId)) {
- this.pendingRequests.delete(requestId);
reject(new Error('Request timeout'));
+ this.pendingRequests.delete(requestId);
}
}, 30000);
});
}
- // Подписка на обновления в реальном времени
- subscribeToMetric(metric, callback, interval = 5000) {
+ subscribeToMetric(metric, callback, interval = 5000, filters = {}) {
this.connectWebSocket();
- if (!this.subscriptions.has(metric)) {
- this.subscriptions.set(metric, []);
- this.socket.emit('subscribe-metric', { metric, interval });
- }
+ const alreadySubscribed = this.subscriptions.has(metric);
+ const callbacks = this.subscriptions.get(metric) || [];
+ callbacks.push(callback);
+ this.subscriptions.set(metric, callbacks);
- this.subscriptions.get(metric).push(callback);
+ if (!alreadySubscribed) {
+ this.socket.emit('subscribe-metric', { metric, interval, filters });
+ }
return () => this.unsubscribeFromMetric(metric, callback);
}
- // Отписка от метрики
unsubscribeFromMetric(metric, callback) {
const callbacks = this.subscriptions.get(metric) || [];
const filtered = callbacks.filter(cb => cb !== callback);
@@ -108,12 +112,20 @@ class MetricsService {
}
}
- // Закрытие соединения
+ cleanupAll() {
+ if (this.socket && this.socket.connected) {
+ this.socket.emit('unsubscribe-all');
+ }
+ this.subscriptions.clear();
+ this.disconnectWebSocket();
+ }
+
disconnectWebSocket() {
if (this.socket) {
- this.socket.close();
+ this.socket.disconnect();
+ this.socket = null;
}
}
}
-export const metricsService = new MetricsService(import.meta.env.VITE_BACK_URL);
\ No newline at end of file
+export const metricsService = new MetricsService(import.meta.env.VITE_BACK_URL);
diff --git a/src/Charts2/PrometheusChart.jsx b/src/Charts2/PrometheusChart.jsx
index 24c37a7..33b711e 100644
--- a/src/Charts2/PrometheusChart.jsx
+++ b/src/Charts2/PrometheusChart.jsx
@@ -2,43 +2,79 @@ 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 { Button, Radio, message, Tag } from 'antd';
import moment from 'moment';
-const PrometheusChart = ({ metricName, chartHeight = 560 }) => {
+const PrometheusChart = ({ metricInfo, chartHeight = 560 }) => {
+ const {
+ name: metricName,
+ filters = {},
+ title = metricName,
+ description,
+ context = {} // Добавляем контекст из path
+ } = metricInfo || {};
+
+ console.log("⚙️ PrometheusChart -> metricInfo:", metricInfo);
+ console.log("📌 Контекст -> device:", context.device, "source_id:", context.source_id, "deviceId:", context.deviceId);
+
+ // Получаем полный контекст из родительских элементов
+ const { device, source_id: module, deviceId, parent } = context;
+
const [chartData, setChartData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
- const [metricInfo, setMetricInfo] = useState({});
+ const [metricMeta, setMetricMeta] = 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 => ({
+ // Генерация уникального ключа для подписки
+ const getSubscriptionKey = () => {
+ return `${metricName}_${device || 'all'}_${module || 'all'}_${deviceId || 'all'}`;
+ };
+
+ const formatMetricData = (dataArray) => {
+ return dataArray
+ .map(item => ({
timestamp: item.timestamp,
value: parseFloat(item.value),
name: item.__name__ || metricName,
- status: item.status
- }));
+ status: item.status,
+ device: item.device?.trim() || null,
+ source_id: item.source_id || null
+ }))
+ .sort((a, b) => a.timestamp - b.timestamp);
+ };
- if (dataArray.length > 0) {
- setMetricInfo({
- type: dataArray[0].type,
- description: dataArray[0].description,
- instance: dataArray[0].instance,
- job: dataArray[0].job
+ const fetchHistoricalData = async (start, end) => {
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ const extendedFilters = {
+ ...filters,
+ ...(device && { device: device.toString() }), // убедитесь, что device строка
+ ...(source_id && { source_id: source_id.toString() })
+ };
+
+ console.log('Fetching with filters:', extendedFilters); // для отладки
+
+ const data = await metricsService.fetchMetricsRange(
+ metricName,
+ Math.floor(start.getTime() / 1000),
+ Math.floor(end.getTime() / 1000),
+ 15,
+ extendedFilters
+ );
+
+ const formattedData = formatMetricData(data);
+ if (formattedData.length > 0) {
+ setMetricMeta({
+ type: data[0]?.type,
+ description: data[0]?.description || description,
+ instance: data[0]?.instance,
+ job: data[0]?.job
});
}
@@ -55,33 +91,43 @@ const PrometheusChart = ({ metricName, chartHeight = 560 }) => {
const startRealtimeUpdates = () => {
setIsLiveUpdating(true);
setIsLoading(true);
-
+
const end = new Date();
const start = new Date(end.getTime() - 3600 * 1000);
- fetchHistoricalData(start, end).finally(() => {
- setIsLoading(false);
- });
+ fetchHistoricalData(start, end).finally(() => setIsLoading(false));
return metricsService.subscribeToMetric(
- metricName,
+ getSubscriptionKey(), // Уникальный ключ для подписки
(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
- }));
+ const filteredData = newData.filter(item => {
+ // Строгая проверка всех доступных фильтров
+ if (device && item.device?.trim() !== device) return false;
+ if (module && item.source_id !== module) return false;
+ return true;
+ });
- setChartData(prevData => [...prevData, ...formattedNewData].slice(-200));
+ if (filteredData.length > 0) {
+ const formattedData = formatMetricData(filteredData);
+ setChartData(prev => {
+ const newChartData = [...prev, ...formattedData]
+ .filter((v, i, a) => a.findIndex(t => t.timestamp === v.timestamp) === i)
+ .slice(-200);
+ return newChartData;
+ });
+ }
},
- 5000
+ 5000,
+ {
+ ...filters,
+ ...(device && { device }),
+ ...(module && { source_id: module })
+ }
);
};
const stopRealtimeUpdates = () => {
setIsLiveUpdating(false);
- metricsService.unsubscribeFromMetric(metricName);
+ metricsService.unsubscribeFromMetric(getSubscriptionKey());
};
const handleCustomRangeApply = () => {
@@ -92,7 +138,6 @@ const PrometheusChart = ({ metricName, chartHeight = 560 }) => {
useEffect(() => {
let unsubscribe;
-
if (mode === 'realtime') {
unsubscribe = startRealtimeUpdates();
} else {
@@ -104,19 +149,30 @@ const PrometheusChart = ({ metricName, chartHeight = 560 }) => {
if (unsubscribe) unsubscribe();
stopRealtimeUpdates();
};
- }, [mode, metricName]);
+ }, [mode, metricName, device, module]);
+
+ // Рекурсивно собираем путь для отображения
+ const getFullPath = () => {
+ const path = [];
+ let current = parent;
+ while (current) {
+ path.unshift(current.title);
+ current = current.parent;
+ }
+ return path.join(' > ');
+ };
const metaInfo = [
- metricInfo.instance && `Instance: ${metricInfo.instance}`,
- metricInfo.job && `Job: ${metricInfo.job}`,
- metricInfo.type && `Type: ${metricInfo.type}`
+ metricMeta.instance && `Instance: ${metricMeta.instance}`,
+ metricMeta.job && `Job: ${metricMeta.job}`,
+ metricMeta.type && `Type: ${metricMeta.type}`
].filter(Boolean).join(' | ');
return (
- setMode(e.target.value)}
buttonStyle="solid"
style={{ marginBottom: 10 }}
@@ -124,7 +180,7 @@ const PrometheusChart = ({ metricName, chartHeight = 560 }) => {
Режим реального времени
Исторические данные
-
+
{mode === 'historical' && (
{
onApply={handleCustomRangeApply}
/>
)}
-
+
{mode === 'realtime' && isLiveUpdating && (
-
)}
-
+
+ {/* Отображаем полный путь к метрике */}
+ {parent && (
+
+ Путь: {getFullPath()}
+ {device && Устройство: {device}}
+ {module && Модуль: {module.split('$')[1]}}
+
+ )}
+
{isLoading ? (
-
Loading chart data...
+
Загрузка графика...
) : error ? (
-
Error loading metric: {error}
+
Ошибка: {error}
) : chartData.length === 0 ? (
-
No data available for {metricName}
+
Нет данных для метрики: {metricName}
) : (
)}
diff --git a/src/Components/Layout/Dashboard.jsx b/src/Components/Layout/Dashboard.jsx
index 5df3408..74565b0 100755
--- a/src/Components/Layout/Dashboard.jsx
+++ b/src/Components/Layout/Dashboard.jsx
@@ -1,6 +1,5 @@
import React, { useState, useEffect } from "react";
import { Box, styled } from "@mui/material";
-import SidebarMenu from "./SidebarMenu";
import { statusManager1, statusManager2 } from "../TreeChart/dataUtils";
import generateTabContent from "../TreeChart/tabContent";
import CustomTabs from "../UI/MUItabs";
@@ -8,6 +7,7 @@ import useTabs from "../hooks/useTabs";
import useSidebarResize from "../hooks/useSidebarResize";
import TabContent from "../hooks/TabContent";
import menuData from "../TreeChart/menuData.json";
+import SidebarMenuWrapper from "./SidebarMenuWrapper";
// Создаем стилизованные компоненты
const DashboardContainer = styled(Box)(({ theme }) => ({
@@ -19,24 +19,6 @@ const DashboardContainer = styled(Box)(({ theme }) => ({
color: theme.palette.text.primary,
}));
-const SidebarWrapper = styled(Box)(({ theme }) => ({
- position: 'relative',
- backgroundColor: theme.palette.custom.sidebar,
- color: theme.palette.custom.sidebarText,
-}));
-
-const SidebarResizer = styled(Box)(({ theme }) => ({
- position: 'absolute',
- right: 0,
- top: 0,
- bottom: 0,
- width: '4px',
- cursor: 'col-resize',
- '&:hover': {
- backgroundColor: theme.palette.primary.main,
- },
-}));
-
const MainContent = styled(Box)(({ theme }) => ({
flexGrow: 1,
display: 'flex',
@@ -56,11 +38,9 @@ const Content = styled(Box)(({ theme }) => ({
const Dashboard = ({ isDarkMode, setIsDarkMode }) => {
const { tabs, activeTab, handleOpenTab, handleCloseTab, setActiveTab } = useTabs("Главная");
- const { sidebarWidth, startResizing } = useSidebarResize(290);
const [tabContent, setTabContent] = useState({});
const [treeData1, setTreeData1] = useState(menuData);
const [treeData2, setTreeData2] = useState(menuData);
- const [collapsed, setCollapsed] = useState(false);
const [statusHistories, setStatusHistories] = useState({
history1: [],
history2: [],
@@ -99,22 +79,53 @@ const Dashboard = ({ isDarkMode, setIsDarkMode }) => {
return () => clearInterval(interval);
}, [treeData1, treeData2]);
+ const handleMenuSelect = (item) => {
+ const tabId = `tab_${item.id}`;
+ const tabTitle = item.title || 'Новая вкладка';
+
+ const generateTabContentForItem = (item) => {
+ return (
+
+ {item.title}
+ {item.description && (
+ {item.description}
+ )}
+ {item.metric && (
+
+ Метрика: {item.metric}
+ {/* Здесь можно добавить визуализацию метрики */}
+
+ )}
+
+ );
+ };
+
+ // Проверяем, существует ли уже такая вкладка
+ const existingTab = tabs.find(tab => tab.id === tabId);
+
+ if (!existingTab) {
+ const newTab = {
+ id: tabId,
+ title: tabTitle,
+ content: generateTabContentForItem(item),
+ type: 'menuItem',
+ itemData: item // Сохраняем данные элемента для возможного обновления
+ };
+
+ handleOpenTab(newTab);
+ } else {
+ setActiveTab(tabId);
+ }
+ };
+
return (
- {/* Сайдбар */}
-
-
-
-
+ {/* Сайдбар - теперь используется SidebarMenuWrapper */}
+
{/* Основной контент */}
{
flexGrow: 1,
overflow: 'hidden'
}}>
- {/* Вкладки*/}
+ {/* Вкладки */}
({
- width: "5px",
- cursor: "ew-resize",
- backgroundColor: 'transparent',
- height: "100%",
- position: "absolute",
- right: 0,
- top: 0,
- transition: 'background-color 0.2s',
- '&:hover': {
- backgroundColor: theme.palette.primary.main,
- },
- zIndex: 2
-}));
-
-const SidebarMenu = ({ data, onOpenTab, sidebarWidth, startResizing, collapsed, setCollapsed, isDarkMode, setIsDarkMode }) => {
+const SidebarMenu = ({
+ data,
+ isDarkMode,
+ setIsDarkMode,
+ onEditItem,
+ onSelectItem,
+ editModalOpen,
+ editingItem,
+ onCloseEditModal,
+ onSaveChanges
+}) => {
+ const [collapsed, setCollapsed] = useState(false);
+ const { sidebarWidth, startResizing } = useSidebarResize(290);
const [hovered, setHovered] = useState(false);
- const [menuData, setMenuData] = useState(data);
- const contentCache = useRef({});
-
- useEffect(() => {
- if (data) {
- const dataCopy = JSON.parse(JSON.stringify(data));
- statusManager1.updateStatuses(dataCopy);
- setMenuData(dataCopy);
- }
- }, [data]);
const handleToggleCollapse = () => {
setCollapsed(!collapsed);
};
- const handleSelectItem = (id, title, children) => {
- onOpenTab(id, title);
-
- contentCache.current = tabContent({ items: children }, contentCache.current);
- if (contentCache.current[id]) {
- onOpenTab(id, title, contentCache.current[id].content);
- }
- };
-
-
-
- const drawerWidth = collapsed ? 64 : sidebarWidth;
+ const SidebarResizer = styled('div')(({ theme }) => ({
+ width: '4px',
+ cursor: 'ew-resize',
+ backgroundColor: 'transparent',
+ '&:hover': {
+ backgroundColor: theme.palette.action.hover,
+ },
+ height: '100%',
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ zIndex: 1000,
+ }));
return (
setHovered(false)}
sx={{
position: 'relative',
- width: drawerWidth,
+ width: collapsed ? 64 : sidebarWidth,
transition: 'width 0.3s ease',
}}
>
- {/* Верхняя часть с логотипом и кнопкой */}
+ {/* Заголовок и кнопка сворачивания */}
- {/* Логотип - центрируется в доступном пространстве */}
-
-
-
-
- {/* Мини-логотип (только в свернутом состоянии) */}
- {collapsed && (
-
-
-
+ {!collapsed && (
+
+ Меню
+
)}
- {/* Кнопка сворачивания/разворачивания */}
{collapsed ? : }
@@ -156,56 +113,94 @@ const SidebarMenu = ({ data, onOpenTab, sidebarWidth, startResizing, collapsed,
- {/* Содержимое меню */}
-
-
- {!collapsed && (
-
- Меню
-
- )}
- {menuData && (
+ {/* Основное содержимое меню */}
+
+
+ {data && (
)}
- {/* Футер */}
-
-
- {/* Ресайзер */}
{!collapsed && (
)}
+
+ {/* Модальное окно редактирования */}
+
);
};
+const EditMenuItemDialog = ({ open, item, onClose, onSave }) => {
+ const [formData, setFormData] = useState(item || {});
+
+ useEffect(() => {
+ setFormData(item || {});
+ }, [item]);
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({ ...prev, [name]: value }));
+ };
+
+ const handleSubmit = () => {
+ onSave(formData);
+ };
+
+ if (!item) return null;
+
+ return (
+
+ );
+};
+
export default SidebarMenu;
\ No newline at end of file
diff --git a/src/Components/Layout/SidebarMenuComponents/MenuItem.jsx b/src/Components/Layout/SidebarMenuComponents/MenuItem.jsx
index 636a973..9b3e01e 100644
--- a/src/Components/Layout/SidebarMenuComponents/MenuItem.jsx
+++ b/src/Components/Layout/SidebarMenuComponents/MenuItem.jsx
@@ -1,21 +1,24 @@
-import React from "react";
+// MenuItem.jsx
+import React, { useState } from "react";
import {
ListItem,
ListItemIcon,
ListItemText,
Collapse,
List,
- styled
+ styled,
+ IconButton,
+ Menu,
+ MenuItem as MuiMenuItem
} from "@mui/material";
-import { ExpandLess, ExpandMore, Folder, FolderOpen } from "@mui/icons-material";
+import { ExpandLess, ExpandMore, Folder, FolderOpen, Edit } from "@mui/icons-material";
import { getStatusColor } from "../../TreeChart/dataUtils";
-
-
+import StatusIndicator from "./StatusIndicator"
const StyledListItem = styled(ListItem)(({ theme, level }) => ({
cursor: "pointer",
paddingLeft: theme.spacing(2 + level * 2),
- position: 'relative',
+ position: 'relative',
'&:hover': {
backgroundColor: theme.palette.action.hover,
},
@@ -24,59 +27,64 @@ const StyledListItem = styled(ListItem)(({ theme, level }) => ({
},
}));
-const StatusIndicator = styled('div')(({ theme, status }) => ({
- position: 'absolute',
- left: 0,
- top: 0,
- bottom: 0,
- width: '4px',
- backgroundColor: status ? getStatusColor(status) : 'transparent',
- borderTopRightRadius: '4px',
- borderBottomRightRadius: '4px',
-}));
-
-const IconWrapper = styled('div')(({ theme }) => ({
- cursor: "pointer",
- borderRadius: theme.shape.borderRadius,
- padding: theme.spacing(0.5),
- '&:hover': {
- backgroundColor: theme.palette.action.selected,
- },
-}));
-
-const MenuItem = ({ item, onSelectItem, level = 0, collapsed }) => {
- const [isOpen, setIsOpen] = React.useState(false);
+const MenuItem = ({ item, onSelectItem, level = 0, collapsed, onEdit }) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const [contextMenu, setContextMenu] = useState(null);
const hasChildren = Array.isArray(item.items) && item.items.length > 0;
+ const handleContextMenu = (e) => {
+ e.preventDefault();
+ setContextMenu(
+ contextMenu === null
+ ? { mouseX: e.clientX - 2, mouseY: e.clientY - 4 }
+ : null
+ );
+ };
+
+ const handleCloseContextMenu = () => {
+ setContextMenu(null);
+ };
+
+ const handleEditClick = () => {
+ onEdit(item);
+ handleCloseContextMenu();
+ };
+
const handleToggle = (e) => {
e.stopPropagation();
setIsOpen(!isOpen);
};
- const handleOpenTab = (e) => {
+ const handleItemClick = (e) => {
e.stopPropagation();
- const allChildren = getAllChildren(item);
- onSelectItem(item.id, item.title, allChildren);
+
+ // Если есть обработчик выбора и элемент можно выбрать
+ if (onSelectItem && (!item.items || item.items.length === 0)) {
+ onSelectItem(item);
+ }
+
+ // Если есть подэлементы - переключаем раскрытие
+ if (item.items && item.items.length > 0) {
+ setIsOpen(!isOpen);
+ }
};
return (
<>
- {/* Индикатор статуса */}
{!collapsed && }
-
- {hasChildren ? (isOpen ? : ) : }
-
+ {hasChildren ? (isOpen ? : ) : }
{!collapsed && (
@@ -88,10 +96,39 @@ const MenuItem = ({ item, onSelectItem, level = 0, collapsed }) => {
}}
/>
{hasChildren && (isOpen ? : )}
+
+ {level > 0 && (
+ {
+ e.stopPropagation();
+ handleEditClick();
+ }}
+ sx={{ ml: 1 }}
+ >
+
+
+ )}
>
)}
+ {/* Контекстное меню */}
+
+
{hasChildren && !collapsed && (
@@ -100,6 +137,7 @@ const MenuItem = ({ item, onSelectItem, level = 0, collapsed }) => {
key={index}
item={child}
onSelectItem={onSelectItem}
+ onEdit={onEdit}
level={level + 1}
collapsed={collapsed}
/>
@@ -111,15 +149,4 @@ const MenuItem = ({ item, onSelectItem, level = 0, collapsed }) => {
);
};
-const getAllChildren = (node) => {
- let children = [];
- if (node.items && node.items.length > 0) {
- node.items.forEach((child) => {
- children.push(child);
- children = children.concat(getAllChildren(child));
- });
- }
- return children;
-};
-
export default MenuItem;
\ No newline at end of file
diff --git a/src/Components/Layout/SidebarMenuWrapper.jsx b/src/Components/Layout/SidebarMenuWrapper.jsx
new file mode 100644
index 0000000..22255f9
--- /dev/null
+++ b/src/Components/Layout/SidebarMenuWrapper.jsx
@@ -0,0 +1,104 @@
+import React, { useState, useEffect } from 'react';
+import SidebarMenu from './SidebarMenu';
+import { Box, CircularProgress, Typography } from '@mui/material';
+import axios from 'axios';
+
+const SidebarMenuWrapper = ({ isDarkMode, setIsDarkMode, onMenuSelect }) => {
+ const [menuData, setMenuData] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [editingItem, setEditingItem] = useState(null);
+ const [editModalOpen, setEditModalOpen] = useState(false);
+
+ useEffect(() => {
+ const fetchMenuData = async () => {
+ try {
+ const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/menu/full`);
+ setMenuData(response.data); // axios хранит данные в response.data
+ } catch (err) {
+ console.error('Error fetching menu data:', err);
+ setError(err.message || 'Failed to fetch menu data');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchMenuData();
+ }, []);
+
+ const handleSaveChanges = async (updatedItem) => {
+ try {
+ const response = await axios.put(
+ `${import.meta.env.VITE_BACK_URL}/api/menu/${updatedItem.id}`,
+ updatedItem,
+ {
+ headers: {
+ 'Content-Type': 'application/json',
+ }
+ }
+ );
+
+ // Обновляем локальное состояние
+ const updateItemInTree = (items) => {
+ return items.map(item => {
+ if (item.id === updatedItem.id) {
+ return { ...item, ...updatedItem };
+ }
+ if (item.items) {
+ return { ...item, items: updateItemInTree(item.items) };
+ }
+ return item;
+ });
+ };
+
+ setMenuData(prev => ({
+ ...prev,
+ items: updateItemInTree(prev.items),
+ }));
+
+ setEditModalOpen(false);
+ } catch (err) {
+ console.error('Error updating menu item:', err);
+ setError(err.response?.data?.message || err.message || 'Failed to update menu item');
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ Error loading menu: {error}
+
+ );
+ }
+
+ if (!menuData) {
+ return null;
+ }
+
+ return (
+ {
+ setEditingItem(item);
+ setEditModalOpen(true);
+ }}
+ onSelectItem={onMenuSelect}
+ editModalOpen={editModalOpen}
+ editingItem={editingItem}
+ onCloseEditModal={() => setEditModalOpen(false)}
+ onSaveChanges={handleSaveChanges}
+ />
+ );
+};
+
+export default SidebarMenuWrapper;
\ No newline at end of file
diff --git a/src/Components/TreeChart/menuData.json b/src/Components/TreeChart/menuData.json
index fcbdd2f..b2526a2 100755
--- a/src/Components/TreeChart/menuData.json
+++ b/src/Components/TreeChart/menuData.json
@@ -11,16 +11,18 @@
"id": "media_server_1",
"items": [
{
- "id": "18",
+ "id": "device$18",
"title": "Graviton S2082I (device$18)",
"items": [
{
- "id": "4",
- "title": "OS Linux (module$4) АО",
+ "id": "module$11",
+ "title": "OS Linux (module$11) АО",
"items": [
{
- "id": "190",
- "title": "Загрузка процессора за 1 минуту"
+ "id": "zvks_cpu1min",
+ "title": "Загрузка процессора за 1 минуту",
+ "metric": "zvks_cpu1min",
+ "description": "Загрузка процессора за 1 минуту"
},
{
"id": "191",
@@ -399,16 +401,18 @@
"id": "media_server_2",
"items": [
{
- "id": "182",
+ "id": "device$19",
"title": "Graviton S2082I (device$19)",
"items": [
{
- "id": "42",
- "title": "OS Linux (module$6) АО",
+ "id": "module$13",
+ "title": "OS Linux (module$13) АО",
"items": [
{
- "id": "371",
- "title": "Загрузка процессора за 1 минуту"
+ "id": "zvks_cpu1min",
+ "title": "Загрузка процессора за 1 минуту",
+ "metric": "zvks_cpu1min",
+ "description": "Загрузка процессора за 1 минуту"
},
{
"id": "372",
diff --git a/src/Components/TreeChart/tabContent.jsx b/src/Components/TreeChart/tabContent.jsx
index 6021f3a..f21b002 100755
--- a/src/Components/TreeChart/tabContent.jsx
+++ b/src/Components/TreeChart/tabContent.jsx
@@ -1,39 +1,22 @@
import React, { lazy, Suspense } from "react";
-import Skeleton from '@mui/material/Skeleton';
-import Box from '@mui/material/Box';
+import Skeleton from "@mui/material/Skeleton";
+import Box from "@mui/material/Box";
-const PrometheusChart = lazy(() => import('../../Charts2/PrometheusChart'));
+const PrometheusChart = lazy(() => import("../../Charts2/PrometheusChart"));
import LazyChartBatchRenderer from "../hooks/LazyChartBatchRender";
-const getMetricName = (id) => {
- return `zvks_apiforsnmp_measure_${id}`;
-};
-
-const getAllChildIds = (node) => {
- let ids = [];
- if (node.id) {
- ids.push(node.id);
- }
- if (node.items && node.items.length > 0) {
- node.items.forEach((child) => {
- ids = ids.concat(getAllChildIds(child));
- });
- }
- return ids;
-};
-
// Компонент Skeleton для графика
const ChartSkeleton = () => (
-
+
);
-// Компонент Skeleton для родительского контейнера
+// Компонент Skeleton для контейнера
const ContainerSkeleton = () => (
-
- {/* Заголовок */}
+
+
{[...Array(3)].map((_, i) => (
@@ -43,62 +26,130 @@ const ContainerSkeleton = () => (
);
-const tabContent = (data) => {
- const tabContent = {};
+// Утилита для извлечения контекста из пути
+const parseContextFromPath = (node) => {
+ const context = {};
+ let current = node;
- // Функция для рекурсивного обхода и сбора данных
- const generateContent = (nodes) => {
- nodes.forEach((node) => {
- if (node.items && node.items.length > 0) {
- const childrenContent = generateContent(node.items);
-
- const content = (
-
-
{node.title}
-
}>
-
tabContent[child.id].content)} />
-
- Контент для {node.title}.
-
- );
-
- tabContent[node.id] = {
- title: node.title,
- content: content,
- };
- } else {
- const metricName = getMetricName(node.id);
- const content = (
-
-
{node.title}
-
}>
-
-
-
- );
- tabContent[node.id] = {
- title: node.title,
- content: content,
- };
- }
- });
-
- return (
-
- {nodes.map((node) => (
-
{tabContent[node.id].content}
- ))}
-
- );
- };
-
- if (data.items && data.items.length > 0) {
- generateContent(data.items);
- } else {
- console.warn("Данные отсутствуют или массив items пуст");
+ while (current) {
+ if (current.id.startsWith("device$")) {
+ context.device = current.id.split("$")[1];
+ context.deviceId = current.id;
+ }
+ if (current.id.startsWith("module$")) {
+ context.module = current.id;
+ context.source_id = current.id;
+ }
+ current = current.parent;
}
- return tabContent;
+ return context;
+};
+
+// Основная функция построения контента вкладок
+const tabContent = (data, cache = {}) => {
+ const tabContentMap = { ...cache };
+
+ if (!data || !data.items || data.items.length === 0) {
+ console.warn("Данные отсутствуют или массив items пуст", data);
+ return tabContentMap;
+ }
+
+ const processNode = (node, parentContext = {}) => {
+ // Получаем полный контекст из всей цепочки родителей
+ const pathContext = parseContextFromPath(node);
+ const currentContext = { ...parentContext, ...pathContext };
+
+ // Генерируем уникальный ключ на основе пути
+ const path = [];
+ let current = node;
+ while (current) {
+ path.unshift(current.id);
+ current = current.parent;
+ }
+ const pathId = path.join('_');
+
+ if (Array.isArray(node.items) && node.items.length > 0) {
+ const children = node.items
+ .map((child) => processNode(child, currentContext))
+ .filter(Boolean);
+
+ const content = (
+
+
{node.title}
+ }>
+ c.content)} />
+
+
+ );
+
+ tabContentMap[pathId] = {
+ title: node.title,
+ content,
+ context: currentContext,
+ };
+
+ return { content, context: currentContext };
+ }
+
+ if (node.metric) {
+ const chartKey = `${node.metric}-${currentContext.device || "all"}-${currentContext.module || "all"}-${pathId}`;
+
+ const content = (
+
+
{node.title}
+ {currentContext.device &&
Устройство: {currentContext.device}
}
+ {currentContext.module &&
Модуль: {currentContext.module}
}
+
}>
+
+
+
+ );
+
+ tabContentMap[pathId] = {
+ title: node.title,
+ content,
+ context: currentContext,
+ };
+
+ return { content, context: currentContext };
+ }
+
+ // Узел без метрики и без вложенных — просто заголовок
+ const content = (
+
+
{node.title}
+
+ );
+
+ tabContentMap[pathId] = {
+ title: node.title,
+ content,
+ context: currentContext,
+ };
+
+ return { content, context: currentContext };
+ };
+
+ try {
+ processNode(data);
+ } catch (error) {
+ console.error("Ошибка обработки данных:", error);
+ }
+
+ return tabContentMap;
};
export default tabContent;
\ No newline at end of file
diff --git a/src/Components/UI/MUItabs.jsx b/src/Components/UI/MUItabs.jsx
index 4a03b60..d4ae078 100644
--- a/src/Components/UI/MUItabs.jsx
+++ b/src/Components/UI/MUItabs.jsx
@@ -1,9 +1,11 @@
import React from "react";
-import { Tabs, Tab, Box, styled } from "@mui/material";
+import { Tabs, Tab, Box, styled, Typography } from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
const StyledTab = styled(Tab)(({ theme }) => ({
minHeight: 48,
+ padding: theme.spacing(1, 2),
+ textTransform: 'none',
'&.Mui-selected': {
color: theme.palette.primary.main,
fontWeight: theme.typography.fontWeightMedium,
@@ -14,9 +16,43 @@ const StyledTab = styled(Tab)(({ theme }) => ({
},
}));
+const TabLabel = ({ title, onClose }) => {
+ return (
+
+
+ {title}
+
+
+
+ );
+};
+
const CustomTabs = ({ tabs, activeTab, onTabClick, onCloseTab }) => {
const handleMouseDown = (e, id) => {
- if (e.button === 1) {
+ if (e.button === 1) { // Средняя кнопка мыши
e.preventDefault();
onCloseTab(id);
}
@@ -26,6 +62,12 @@ const CustomTabs = ({ tabs, activeTab, onTabClick, onCloseTab }) => {
onTabClick(newValue);
};
+ // Статические вкладки
+ const staticTabs = [
+ { id: "Главная", title: "Главная" },
+ { id: "Визуализация", title: "Визуализация" }
+ ];
+
return (
{
onChange={handleChange}
variant="scrollable"
scrollButtons="auto"
+ allowScrollButtonsMobile
aria-label="tabs"
>
{/* Статические вкладки */}
- handleMouseDown(e, "Главная")}
- />
- handleMouseDown(e, "Визуализация")}
- />
+ {staticTabs.map(tab => (
+ handleMouseDown(e, tab.id)}
+ />
+ ))}
{/* Динамические вкладки */}
{tabs.map((tab) => (
- {tab.title}
- {
- e.stopPropagation();
- onCloseTab(tab.id);
- }}
- />
-
+ {
+ e.stopPropagation();
+ onCloseTab(tab.id);
+ }}
+ />
}
value={tab.id}
onMouseDown={(e) => handleMouseDown(e, tab.id)}