diff --git a/src/App.jsx b/src/App.jsx
index c2372e5..798e186 100755
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,11 +1,10 @@
import React, { useState, useMemo, useEffect } from "react";
-import { ThemeProvider, CssBaseline, Switch, Box, CircularProgress } from "@mui/material";
+import { ThemeProvider, CssBaseline, Switch, Box, CircularProgress, Typography } from "@mui/material";
import Dashboard from "./Components/Layout/Dashboard";
import LoginModal from "./Components/UI/LoginModal";
import { lightTheme, darkTheme } from "./Style/theme";
import Logo from './assets/images/logo.svg?react';
-import { checkAuth } from "./Components/UI/auth";
-import axios from 'axios';
+import { checkAuth } from "./Components/UI/auth";
function App() {
const [authState, setAuthState] = useState({
@@ -24,13 +23,11 @@ function App() {
const verifyAuth = async () => {
try {
const authStatus = await checkAuth();
-
setAuthState({
isAuthenticated: authStatus.isAuthenticated,
isLoading: false,
user: authStatus.user || null
});
-
setShowLoginModal(!authStatus.isAuthenticated);
} catch (error) {
console.error('Auth verification error:', error);
@@ -42,7 +39,6 @@ function App() {
setShowLoginModal(true);
}
};
-
verifyAuth();
}, []);
@@ -56,37 +52,42 @@ function App() {
};
const handleLogout = async () => {
- try {
- await axios.get(`${import.meta.env.VITE_BACK_URL}/api/metrics`, {
- method: 'POST',
- credentials: 'include'
- });
- localStorage.removeItem('access_token');
- setAuthState({
- isAuthenticated: false,
- isLoading: false,
- user: null
- });
- setShowLoginModal(true);
- } catch (error) {
- console.error('Logout failed:', error);
- }
- };
+ try {
+ await axios.post(`${import.meta.env.VITE_BACK_URL}/api/auth/logout`, null, {
+ withCredentials: true, // чтобы отправлялись куки
+ });
+ localStorage.removeItem('access_token');
+ setAuthState({
+ isAuthenticated: false,
+ isLoading: false,
+ user: null,
+ });
+ setShowLoginModal(true);
+ } catch (error) {
+ console.error('Logout failed:', error);
+ }
+};
+
+ // Полноэкранный лоадер во время проверки авторизации
if (authState.isLoading) {
return (
- Проверка авторизации...
+
+ Проверка авторизации...
+
);
@@ -95,26 +96,20 @@ function App() {
return (
- {!authState.isAuthenticated && showLoginModal ? (
+ {!authState.isAuthenticated ? (
<>
-
+
-
setShowLoginModal(false)}
/>
@@ -124,19 +119,14 @@ function App() {
display: "flex",
height: "100vh",
overflow: "hidden",
- bgcolor: "background.default",
- color: "text.primary"
+ bgcolor: "background.default"
}}>
-
- setIsDarkMode((prev) => !prev)}
- />
-
)}
diff --git a/src/Charts/Components/ChartSkeleton.jsx b/src/Charts/Components/ChartSkeleton.jsx
new file mode 100644
index 0000000..090f1e7
--- /dev/null
+++ b/src/Charts/Components/ChartSkeleton.jsx
@@ -0,0 +1,26 @@
+const ChartSkeleton = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {[1, 2, 3, 4].map((_, i) => (
+
+ ))}
+
+
+);
\ No newline at end of file
diff --git a/src/Charts/Components/LineChartComponent.jsx b/src/Charts/Components/LineChartComponent.jsx
index c48d46f..149f1fb 100755
--- a/src/Charts/Components/LineChartComponent.jsx
+++ b/src/Charts/Components/LineChartComponent.jsx
@@ -1,6 +1,8 @@
import React, { useState, useRef, useEffect } from 'react';
import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Line, ResponsiveContainer, ReferenceArea } from 'recharts';
+import { Skeleton } from '@mui/material';
import { HOUR, DAY } from './constants';
+
const TIME_FORMATS = {
LONG: 'dd.MM HH:mm', // Для диапазона > 24 часов
MEDIUM: 'HH:mm', // Для диапазона > 1 часа
@@ -51,7 +53,6 @@ const LineChartComponent = ({
? Object.keys(displayData[0]).filter(k => !['timestamp', 'time', 'fullTime'].includes(k))
: [];
- // Функция для определения оптимального формата времени в зависимости от диапазона
const getTimeFormat = () => {
if (!data.length) return TIME_FORMATS.SHORT;
@@ -77,7 +78,6 @@ const LineChartComponent = ({
const handleMouseDown = (e) => {
if (!e) return;
- // Получаем индекс точки по координатам
const activeIndex = e.activeTooltipIndex;
if (activeIndex === undefined || activeIndex < 0 || activeIndex >= data.length) return;
@@ -113,7 +113,6 @@ const LineChartComponent = ({
const startIndex = Math.min(selectionArea.startIndex, selectionArea.endIndex);
const endIndex = Math.max(selectionArea.startIndex, selectionArea.endIndex);
- // Нормализуем индексы к диапазону [0, 1] для родительского компонента
const normalizedStart = startIndex / (data.length - 1);
const normalizedEnd = endIndex / (data.length - 1);
@@ -152,7 +151,18 @@ const LineChartComponent = ({
};
if (!data.length) {
- return
Нет данных для отображения
;
+ return (
+
+
+
+
+ );
}
return (
diff --git a/src/Charts/Components/hooks/useMetricsData.jsx b/src/Charts/Components/hooks/useMetricsData.jsx
new file mode 100644
index 0000000..e69de29
diff --git a/src/Charts/Components/hooks/useTimeHandlers.jsx b/src/Charts/Components/hooks/useTimeHandlers.jsx
new file mode 100644
index 0000000..e69de29
diff --git a/src/Charts/Components/hooks/useWebSocket.jsx b/src/Charts/Components/hooks/useWebSocket.jsx
new file mode 100644
index 0000000..e69de29
diff --git a/src/Charts/PrometheusChart.jsx b/src/Charts/PrometheusChart.jsx
index 420fd75..1a8b20c 100755
--- a/src/Charts/PrometheusChart.jsx
+++ b/src/Charts/PrometheusChart.jsx
@@ -6,6 +6,37 @@ import { ConnectionStatusIndicator } from './Components/ConnectionStatusIndicato
import { CurrentRangeDisplay } from './Components/CurrentRangeDisplay';
import { TIME_RANGES, COLORS, SECOND, MINUTE, HOUR, DAY } from './Components/constants';
import axios from 'axios';
+import Skeleton from '@mui/material/Skeleton';
+import Box from '@mui/material/Box';
+
+
+// Компонент Skeleton для графика
+const ChartSkeleton = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {[1, 2, 3, 4].map((_, i) => (
+
+ ))}
+
+
+);
const PrometheusChart = ({ metricName }) => {
const [chartData, setChartData] = useState(null);
@@ -70,53 +101,63 @@ const PrometheusChart = ({ metricName }) => {
}, []);
- const processMetricsData = useCallback((response) => {
+ const processMetricsData = useCallback((response, replace = false) => {
console.log('Processing metrics data:', response);
if (response.metric !== metricName) return;
const dataArray = Array.isArray(response.data) ? response.data : [response.data];
if (!dataArray.length) return;
- setChartData(prev => {
- const newData = { ...(prev || {}) };
- const rangeSeconds = useCustomRange
- ? (endDate.getTime() - startDate.getTime()) / 1000
- : selectedRange.value;
+ const newData = {};
+ const rangeSeconds = useCustomRange
+ ? (endDate.getTime() - startDate.getTime()) / 1000
+ : selectedRange.value;
- dataArray.forEach(item => {
- const instance = item.instance || 'default';
- if (!newData[instance]) newData[instance] = [];
+ dataArray.forEach(item => {
+ const instance = item.instance || 'default';
+ if (!newData[instance]) newData[instance] = [];
- // Унифицированная конвертация timestamp
- let timestamp;
- if (typeof item.timestamp === 'number') {
- // Определяем, в секундах или миллисекундах пришел timestamp
- timestamp = item.timestamp > 1e12 ? item.timestamp : item.timestamp * 1000;
- } else {
- timestamp = Date.now();
- }
+ let timestamp;
+ if (typeof item.timestamp === 'number') {
+ timestamp = item.timestamp > 1e12 ? item.timestamp : item.timestamp * 1000;
+ } else {
+ timestamp = Date.now();
+ }
- const value = parseFloat(item.value);
- const formattedTime = formatTime(timestamp, rangeSeconds);
+ const value = parseFloat(item.value);
+ const formattedTime = formatTime(timestamp, rangeSeconds);
- newData[instance].push({
- time: formattedTime.display,
- fullTime: formattedTime.fullDisplay,
- value: value,
- timestamp: timestamp
- });
+ newData[instance].push({
+ time: formattedTime.display,
+ fullTime: formattedTime.fullDisplay,
+ value,
+ timestamp
});
-
- // Сортируем и ограничиваем данные
- Object.keys(newData).forEach(instance => {
- newData[instance] = newData[instance]
- .sort((a, b) => a.timestamp - b.timestamp)
- .slice(-1000);
- });
- return newData;
});
+
+ Object.keys(newData).forEach(instance => {
+ newData[instance] = newData[instance]
+ .sort((a, b) => a.timestamp - b.timestamp)
+ .slice(-1000);
+ });
+
+ if (replace) {
+ setChartData(newData); // Заменяем полностью
+ } else {
+ setChartData(prev => {
+ const merged = { ...(prev || {}) };
+ Object.keys(newData).forEach(instance => {
+ if (!merged[instance]) merged[instance] = [];
+ merged[instance] = [...merged[instance], ...newData[instance]]
+ .sort((a, b) => a.timestamp - b.timestamp)
+ .slice(-1000);
+ });
+ return merged;
+ });
+ }
}, [metricName, selectedRange.value, formatTime, useCustomRange, startDate, endDate]);
+
const fetchData = useCallback(() => {
if (isSelectingRange) return;
@@ -166,7 +207,7 @@ const PrometheusChart = ({ metricName }) => {
processMetricsData({
metric: metricName,
data: processedData
- });
+ }, true);
}
} catch (error) {
console.error('Ошибка при получении кастомных данных:', error);
@@ -309,7 +350,9 @@ const PrometheusChart = ({ metricName }) => {
useEffect(() => {
// Обработчик данных с сервера
const handleMetricsData = (data) => {
- processMetricsData({ metric: metricName, data });
+ if (!useCustomRange) {
+ processMetricsData({ metric: metricName, data });
+ }
};
// Подписываемся на обновления метрики
@@ -328,7 +371,8 @@ const PrometheusChart = ({ metricName }) => {
clearInterval(intervalRef.current);
}
};
- }, [metricName, processMetricsData]);
+ }, [metricName, useCustomRange, processMetricsData]);
+
useEffect(() => {
if (useCustomRange && !isSelectingRange) {
@@ -395,11 +439,21 @@ const PrometheusChart = ({ metricName }) => {
}, [selectedGraphRange, chartData, interpolateData]);
if (chartData === null) {
- return Loading data...
;
+ return ;
}
if (Object.keys(chartData).length === 0) {
- return No data available
;
+ return (
+
+ No data available
+
+ );
}
return (
diff --git a/src/Charts/WebSocketManager.jsx b/src/Charts/WebSocketManager.jsx
index aa4a46b..ff542a0 100644
--- a/src/Charts/WebSocketManager.jsx
+++ b/src/Charts/WebSocketManager.jsx
@@ -1,4 +1,3 @@
-// src/services/WebSocketManager.js
import { io } from 'socket.io-client';
class WebSocketManager {
@@ -7,13 +6,16 @@ class WebSocketManager {
this.subscribers = new Map();
this.connectionStatus = 'disconnected';
this.connectionCallbacks = new Set();
+ this.connecting = false;
}
connect() {
- if (this.socket && (this.socket.connected || this.socket.reconnecting)) {
+ if (this.socket?.connected || this.connecting) {
return this.socket;
}
+ this.connecting = true;
+
this.socket = io(`${import.meta.env.VITE_BACK_WS_URL}/api/metrics-ws`, {
transports: ['websocket'],
reconnection: true,
@@ -24,11 +26,13 @@ class WebSocketManager {
this.socket.on('connect', () => {
this.connectionStatus = 'connected';
+ this.connecting = false;
this.notifyConnectionStatus();
});
this.socket.on('disconnect', (reason) => {
this.connectionStatus = 'disconnected';
+ this.connecting = false;
this.notifyConnectionStatus();
if (reason === 'io server disconnect') this.socket.connect();
});
@@ -50,7 +54,9 @@ class WebSocketManager {
}
subscribe(metricName, callback) {
- this.connect();
+ if (!this.socket?.connected) {
+ this.connect();
+ }
if (!this.subscribers.has(metricName)) {
this.subscribers.set(metricName, new Set());
diff --git a/src/Charts2/Components/DateRangeSelector.jsx b/src/Charts2/Components/DateRangeSelector.jsx
new file mode 100644
index 0000000..4cec48e
--- /dev/null
+++ b/src/Charts2/Components/DateRangeSelector.jsx
@@ -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 (
+
+
+ Укажите диапазон дат:
+
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+
+ );
+};
+
+export default DateRangeSelector;
\ No newline at end of file
diff --git a/src/Charts2/Components/LineChartComponent.jsx b/src/Charts2/Components/LineChartComponent.jsx
new file mode 100644
index 0000000..2f990f6
--- /dev/null
+++ b/src/Charts2/Components/LineChartComponent.jsx
@@ -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 (
+
+ {title &&
{title}
}
+ {description && (
+
{description}
+ )}
+ {metaInfo && (
+
+ {metaInfo}
+
+ )}
+
+
+
+ {showGrid && }
+ new Date(timestamp).toLocaleTimeString())}
+ />
+ {customYAxis || }
+ new Date(timestamp).toLocaleString()}
+ />
+ {showLegend && }
+
+ {additionalLines.map((lineProps, index) => (
+
+ ))}
+
+
+
+ );
+};
+
+export default LineChartComponent;
\ No newline at end of file
diff --git a/src/Charts2/Components/metricsService.jsx b/src/Charts2/Components/metricsService.jsx
new file mode 100644
index 0000000..90d9261
--- /dev/null
+++ b/src/Charts2/Components/metricsService.jsx
@@ -0,0 +1,155 @@
+import { io } from 'socket.io-client';
+
+class MetricsService {
+ constructor(baseUrl) {
+ this.baseUrl = baseUrl || window.location.origin;
+ this.socket = null;
+ this.subscriptions = new Map();
+ this.pendingRequests = new Map();
+
+ window.addEventListener('beforeunload', () => {
+ this.cleanupAll();
+ });
+ }
+
+ connectWebSocket() {
+ 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,
+ });
+
+ this.socket.on('connect', () => {
+ console.log('WebSocket connected');
+ // Восстанавливаем подписки при переподключении
+ this.subscriptions.forEach((_, metricKey) => {
+ const [metric, query] = metricKey.split('?');
+ const filters = this.parseFiltersFromKey(metricKey);
+ this.socket.emit('subscribe-metric', { metric, filters });
+ });
+ });
+
+ 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);
+ this.pendingRequests.delete(requestId);
+ return;
+ }
+
+ const callbacks = this.subscriptions.get(metric) || [];
+ callbacks.forEach(cb => cb(data));
+ });
+
+ this.socket.on('metrics-error', ({ error, requestId }) => {
+ if (requestId && this.pendingRequests.has(requestId)) {
+ const { reject } = this.pendingRequests.get(requestId);
+ reject(new Error(error));
+ this.pendingRequests.delete(requestId);
+ }
+ });
+ }
+
+ async fetchMetricsRange(metric, start, end, step = 15, filters = {}) {
+ return new Promise((resolve, reject) => {
+ this.connectWebSocket();
+
+ const requestId = `range-${Date.now()}`;
+ this.pendingRequests.set(requestId, { resolve, reject });
+
+ this.socket.emit('get-metrics', {
+ metric,
+ start,
+ end,
+ step,
+ filters,
+ isRangeQuery: true,
+ requestId
+ });
+
+ setTimeout(() => {
+ if (this.pendingRequests.has(requestId)) {
+ reject(new Error('Request timeout'));
+ this.pendingRequests.delete(requestId);
+ }
+ }, 30000);
+ });
+ }
+
+ subscribeToMetric(metricKey, callback, interval = 5000, filters = {}) {
+ this.connectWebSocket();
+
+ const alreadySubscribed = this.subscriptions.has(metricKey);
+ const callbacks = this.subscriptions.get(metricKey) || [];
+ callbacks.push(callback);
+ this.subscriptions.set(metricKey, callbacks);
+
+ if (!alreadySubscribed) {
+ // Разделяем metricKey на метрику и фильтры
+ const [metric] = metricKey.split('?');
+ this.socket.emit('subscribe-metric', {
+ metric,
+ interval,
+ filters
+ });
+ }
+
+ return () => this.unsubscribeFromMetric(metricKey, callback);
+ }
+
+ unsubscribeFromMetric(metricKey, callback) {
+ const callbacks = this.subscriptions.get(metricKey) || [];
+ const filtered = callbacks.filter(cb => cb !== callback);
+
+ if (filtered.length === 0) {
+ this.subscriptions.delete(metricKey);
+ if (this.socket && this.socket.connected) {
+ const [metric] = metricKey.split('?');
+ this.socket.emit('unsubscribe-metric', { metric });
+ }
+ } else {
+ this.subscriptions.set(metricKey, filtered);
+ }
+ }
+
+ parseFiltersFromKey(metricKey) {
+ const parts = metricKey.split('?');
+ if (parts.length < 2) return {};
+
+ return parts[1].split('&').reduce((acc, pair) => {
+ const [key, value] = pair.split('=');
+ if (key && value) acc[key] = value;
+ return acc;
+ }, {});
+ }
+
+ cleanupAll() {
+ if (this.socket && this.socket.connected) {
+ this.socket.emit('unsubscribe-all');
+ }
+ this.subscriptions.clear();
+ this.disconnectWebSocket();
+ }
+
+ disconnectWebSocket() {
+ if (this.socket) {
+ this.socket.disconnect();
+ this.socket = null;
+ }
+ }
+}
+
+// Создаем экземпляр сервиса
+const metricsService = new MetricsService(import.meta.env.VITE_BACK_URL);
+
+export default metricsService;
\ No newline at end of file
diff --git a/src/Charts2/PrometheusChart.jsx b/src/Charts2/PrometheusChart.jsx
new file mode 100644
index 0000000..5d7fe46
--- /dev/null
+++ b/src/Charts2/PrometheusChart.jsx
@@ -0,0 +1,208 @@
+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, Tag } from 'antd';
+import moment from 'moment';
+
+const PrometheusChart = ({ metricInfo, chartHeight = 560 }) => {
+ const {
+ name: metricName,
+ filters = {},
+ title = metricName,
+ description,
+ context = {}
+ } = metricInfo || {};
+
+ const { device, source_id: module } = context;
+
+ const [chartData, setChartData] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+ 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 getSubscriptionKey = () => {
+ const filterParts = [];
+ if (device) filterParts.push(`device=${device}`);
+ if (module) filterParts.push(`source_id=${module}`);
+ return `${metricName}${filterParts.length ? `?${filterParts.join('&')}` : ''}`;
+ };
+
+ const formatMetricData = (dataArray) => {
+ return dataArray
+ .map(item => ({
+ timestamp: item.timestamp,
+ value: parseFloat(item.value),
+ name: item.__name__ || metricName,
+ status: item.status,
+ device: item.device?.trim() || null,
+ source_id: item.source_id || null
+ }))
+ .sort((a, b) => a.timestamp - b.timestamp);
+ };
+
+ const fetchHistoricalData = async (start, end) => {
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ const extendedFilters = {
+ ...filters,
+ ...(device && { device: device.toString() }),
+ ...(module && { source_id: module.toString() })
+ };
+
+ 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
+ });
+ }
+
+ 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(
+ getSubscriptionKey(),
+ (newData) => {
+ const formattedData = formatMetricData(newData);
+ setChartData(prev => {
+ const newChartData = [...prev, ...formattedData]
+ .filter((v, i, a) => a.findIndex(t => t.timestamp === v.timestamp) === i)
+ .slice(-200);
+ return newChartData;
+ });
+ },
+ 5000,
+ {
+ ...filters,
+ ...(device && { device }),
+ ...(module && { source_id: module })
+ }
+ );
+ };
+
+ const stopRealtimeUpdates = () => {
+ setIsLiveUpdating(false);
+ metricsService.unsubscribeFromMetric(getSubscriptionKey());
+ };
+
+ 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, device, module]);
+
+ const metaInfo = [
+ 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 }}
+ >
+ Режим реального времени
+ Исторические данные
+
+
+ {mode === 'historical' && (
+
+ )}
+
+ {mode === 'realtime' && isLiveUpdating && (
+
+ )}
+
+
+ {device &&
Устройство: {device}}
+ {module &&
Модуль: {module.split('$')[1]}}
+
+ {isLoading ? (
+
Загрузка графика...
+ ) : error ? (
+
Ошибка: {error}
+ ) : chartData.length === 0 ? (
+
Нет данных для метрики: {metricName}
+ ) : (
+
+ )}
+
+ );
+};
+
+export default PrometheusChart;
\ No newline at end of file
diff --git a/src/Components/Layout/Dashboard.jsx b/src/Components/Layout/Dashboard.jsx
index 9b66e58..8841bc7 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,8 +7,10 @@ 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";
+import MetricTabContent from "./MetricTabContent"
-// Создаем стилизованные компоненты
+// Стилизованные компоненты
const DashboardContainer = styled(Box)(({ theme }) => ({
display: 'flex',
height: '100vh',
@@ -19,46 +20,25 @@ 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',
flexDirection: 'column',
- padding: theme.spacing(2.5), // 20px
+ padding: theme.spacing(2.5),
overflow: 'auto',
backgroundColor: theme.palette.background.default,
}));
const Content = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.custom.modalBackground,
- //padding: theme.spacing(2.5),
borderRadius: '10px',
- //boxShadow: theme.shadows[2],
maxWidth: '100%',
overflow: 'auto',
color: theme.palette.custom.modalText,
}));
-const Dashboard = () => {
+const Dashboard = ({ isDarkMode, setIsDarkMode }) => {
const { tabs, activeTab, handleOpenTab, handleCloseTab, setActiveTab } = useTabs("Главная");
- const { sidebarWidth, startResizing } = useSidebarResize(250);
const [tabContent, setTabContent] = useState({});
const [treeData1, setTreeData1] = useState(menuData);
const [treeData2, setTreeData2] = useState(menuData);
@@ -100,40 +80,103 @@ const Dashboard = () => {
return () => clearInterval(interval);
}, [treeData1, treeData2]);
+ const handleMenuSelect = (item) => {
+ const tabId = `tab_${item.id}`;
+ const tabTitle = item.title || 'Новая вкладка';
+
+ // Если это метрика, создаём специальный контент с графиком
+ const tabContent = item.metric
+ ?
+ : Контент для {item.title}
;
+
+ const existingTab = tabs.find(tab => tab.id === tabId);
+
+ if (!existingTab) {
+ const newTab = {
+ id: tabId,
+ title: tabTitle,
+ content: tabContent,
+ type: item.metric ? 'metric' : 'menuItem',
+ metric: item.metric,
+ filters: item.filters
+ };
+ handleOpenTab(newTab);
+ } else {
+ setActiveTab(tabId);
+ }
+ };
+
+ // Вспомогательная функция для получения всех дочерних элементов
+ 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;
+ };
+
return (
{/* Сайдбар */}
-
-
-
-
+
{/* Основной контент */}
-
+
{/* Вкладки */}
-
-
- {/* Контент вкладки */}
-
-
+
-
-
+
+
+ {/* Остальной контент */}
+
+ {/* Контент вкладки */}
+
+
+
+
+
);
};
diff --git a/src/Components/Layout/MetricTabContent.jsx b/src/Components/Layout/MetricTabContent.jsx
new file mode 100644
index 0000000..13fff6c
--- /dev/null
+++ b/src/Components/Layout/MetricTabContent.jsx
@@ -0,0 +1,25 @@
+import React, { useEffect } from 'react';
+import PrometheusChart from '../../Charts2/PrometheusChart';
+import metricsService from '../../Charts2/Components/metricsService';
+
+const MetricTabContent = ({ metricInfo }) => {
+ // Очистка подписок при закрытии вкладки
+ useEffect(() => {
+ return () => {
+ if (metricInfo?.name) {
+ metricsService.unsubscribeFromMetric(metricInfo.name);
+ }
+ };
+ }, [metricInfo?.name]);
+
+ return (
+
+ );
+};
+
+export default MetricTabContent;
\ No newline at end of file
diff --git a/src/Components/Layout/SidebarMenu.jsx b/src/Components/Layout/SidebarMenu.jsx
index 7efd192..e37f326 100644
--- a/src/Components/Layout/SidebarMenu.jsx
+++ b/src/Components/Layout/SidebarMenu.jsx
@@ -1,3 +1,4 @@
+// SidebarMenu.jsx
import React, { useState, useEffect } from "react";
import {
Drawer,
@@ -6,56 +7,54 @@ import {
styled,
IconButton,
Tooltip,
- Box
+ Box,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ Button,
+ TextField
} from "@mui/material";
-import {
- ChevronLeft,
- ChevronRight,
- Menu as MenuIcon
-} from "@mui/icons-material";
import MenuItem from "./SidebarMenuComponents/MenuItem";
import SidebarFooter from "./SidebarMenuComponents/SidebarFooter";
-import { statusManager1 } from "../TreeChart/dataUtils";
+import useSidebarResize from "../hooks/useSidebarResize";
+import ChevronLeft from '@mui/icons-material/ChevronLeft';
+import ChevronRight from '@mui/icons-material/ChevronRight';
+import LogoFull from '../../assets/images/logo.svg?react';
+import LogoSmall from '../../assets/images/system_monitor_icon.svg?react';
-const SidebarResizer = styled('div')(({ theme }) => ({
- 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 }) => {
+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);
-
- // Обновляем статусы при изменении данных
- 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, children);
- };
-
- 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 (
{
onMouseLeave={() => setHovered(false)}
sx={{
position: 'relative',
- width: drawerWidth,
+ width: collapsed ? 64 : sidebarWidth,
transition: 'width 0.3s ease',
}}
>
{
},
}}
>
- {/* Кнопка сворачивания/разворачивания */}
+ {/* Заголовок с логотипом */}
+ {/* Логотип (занимает все пространство) */}
+
+ {collapsed ? (
+
+ ) : (
+
+ )}
+
+
+ {/* Кнопка сворачивания (абсолютное позиционирование) */}
- {collapsed ? (
- hovered ? :
- ) : (
-
- )}
+ {collapsed ? : }
- {/* Содержимое меню */}
-
-
- {!collapsed && (
-
- Меню
-
- )}
- {menuData && (
+ {/* Основное содержимое меню */}
+
+
+ {data && (
)}
- {/* Футер */}
- {!collapsed && (
-
- )}
+
- {/* Ресайзер */}
{!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 6d0eba6..6fd6607 100644
--- a/src/Components/Layout/SidebarMenuComponents/MenuItem.jsx
+++ b/src/Components/Layout/SidebarMenuComponents/MenuItem.jsx
@@ -1,21 +1,23 @@
-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 { getStatusColor } from "../../TreeChart/dataUtils";
-
-
+import { ExpandLess, ExpandMore, Folder, FolderOpen, Edit } from "@mui/icons-material";
+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 +26,56 @@ 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) => {
- e.stopPropagation();
- const allChildren = getAllChildren(item);
- onSelectItem(item.id, item.title, allChildren);
+ const handleClick = () => {
+ if (onSelectItem) {
+ onSelectItem(item);
+ }
};
return (
<>
- {/* Индикатор статуса */}
{!collapsed && }
-
- {hasChildren ? (isOpen ? : ) : }
-
+ {hasChildren ? (isOpen ? : ) : }
{!collapsed && (
@@ -88,18 +87,47 @@ const MenuItem = ({ item, onSelectItem, level = 0, collapsed }) => {
}}
/>
{hasChildren && (isOpen ? : )}
+
+ {level > 0 && (
+ {
+ e.stopPropagation();
+ handleEditClick();
+ }}
+ sx={{ ml: 1 }}
+ >
+
+
+ )}
>
)}
+
+
{hasChildren && !collapsed && (
{item.items.map((child, index) => (
@@ -111,15 +139,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
+export default MenuItem;
diff --git a/src/Components/Layout/SidebarMenuComponents/SidebarFooter.jsx b/src/Components/Layout/SidebarMenuComponents/SidebarFooter.jsx
index 0d4d4e4..54a2b7b 100644
--- a/src/Components/Layout/SidebarMenuComponents/SidebarFooter.jsx
+++ b/src/Components/Layout/SidebarMenuComponents/SidebarFooter.jsx
@@ -1,9 +1,13 @@
import React from "react";
+import { Brightness4, Brightness7 } from "@mui/icons-material";
+import { IconButton, Tooltip } from "@mui/material";
import {
List,
ListItem,
ListItemText,
- styled
+ styled,
+ Switch,
+ Box
} from "@mui/material";
const FooterList = styled(List)(({ theme }) => ({
@@ -18,31 +22,57 @@ const FooterListItem = styled(ListItem)(({ theme }) => ({
backgroundColor: theme.palette.custom.sidebarHover,
},
padding: theme.spacing(1, 2),
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center'
}));
-const SidebarFooter = () => {
+const SidebarFooter = ({ collapsed, isDarkMode, setIsDarkMode }) => {
return (
-
-
-
-
-
+ {!collapsed && (
+
+
+
+ )}
+
+ {!collapsed && (
+
+ )}
+
+
+
+ setIsDarkMode(!isDarkMode)}
+ sx={{ color: 'custom.sidebarText' }}
+ >
+ {isDarkMode ? : }
+
+
+ {!collapsed && (
+ setIsDarkMode(!isDarkMode)}
+ size="small"
+ />
+ )}
+
);
};
-export default SidebarFooter;
\ No newline at end of file
+export default SidebarFooter;
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/FlowChart.jsx b/src/Components/TreeChart/FlowChart.jsx
index bf8fe4a..0b0dd09 100644
--- a/src/Components/TreeChart/FlowChart.jsx
+++ b/src/Components/TreeChart/FlowChart.jsx
@@ -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 {
diff --git a/src/Components/TreeChart/FlowChartComponents/DataParser.jsx b/src/Components/TreeChart/FlowChartComponents/DataParser.jsx
index 916bc97..92717f6 100644
--- a/src/Components/TreeChart/FlowChartComponents/DataParser.jsx
+++ b/src/Components/TreeChart/FlowChartComponents/DataParser.jsx
@@ -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] }
diff --git a/src/Components/TreeChart/FlowChartComponents/NodeWrapper.jsx b/src/Components/TreeChart/FlowChartComponents/NodeWrapper.jsx
index d64d391..52ccd94 100644
--- a/src/Components/TreeChart/FlowChartComponents/NodeWrapper.jsx
+++ b/src/Components/TreeChart/FlowChartComponents/NodeWrapper.jsx
@@ -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}
>
{/* Хендл для входящих соединений */}
{
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) => ({
diff --git a/src/Components/TreeChart/FlowChartComponents/useNodeHandlers.jsx b/src/Components/TreeChart/FlowChartComponents/useNodeHandlers.jsx
index 80e4fd0..efc6480 100644
--- a/src/Components/TreeChart/FlowChartComponents/useNodeHandlers.jsx
+++ b/src/Components/TreeChart/FlowChartComponents/useNodeHandlers.jsx
@@ -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)
diff --git a/src/Components/TreeChart/dataUtils.jsx b/src/Components/TreeChart/dataUtils.jsx
index 1a1c70d..b9cd7e0 100755
--- a/src/Components/TreeChart/dataUtils.jsx
+++ b/src/Components/TreeChart/dataUtils.jsx
@@ -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":
diff --git a/src/Components/TreeChart/menuData.json b/src/Components/TreeChart/menuData.json
index 1d4c364..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,118 +401,74 @@
"id": "media_server_2",
"items": [
{
- "id": "182",
- "title": "Graviton S2082I (device$18)",
+ "id": "device$19",
+ "title": "Graviton S2082I (device$19)",
"items": [
{
- "id": "42",
- "title": "OS Linux (module$4) АО",
+ "id": "module$13",
+ "title": "OS Linux (module$13) АО",
"items": [
{
- "id": "1902",
- "title": "Загрузка процессора за 1 минуту"
- },
- {
- "id": "1912",
- "title": "Загрузка процессора за 5 минут"
- },
- {
- "id": "1922",
- "title": "Загрузка процессора за 15 минут"
- },
- {
- "id": "1972",
- "title": "Общий объем SWAP-файла"
- },
- {
- "id": "1982",
- "title": "Используемый объем SWAP-файла"
- },
- {
- "id": "1992",
- "title": "Общий объем физической оперативной памяти"
- },
- {
- "id": "2002",
- "title": "Доступный объем физической оперативной памяти"
- },
- {
- "id": "2012",
- "title": "Свободный объем физической и виртуальной оперативной памяти"
- },
- {
- "id": "2022",
- "title": "Буферизованный объем оперативной памяти"
- },
- {
- "id": "2032",
- "title": "Кэшированый объем оперативной памяти"
- },
- {
- "id": "2742",
- "title": "Используемый объем SWAP-файла"
- },
- {
- "id": "2752",
- "title": "Время затраченное процессором на процессы с пониженным приоритетом"
- },
- {
- "id": "2762",
- "title": "Время затраченное процессором на процессы ядра ОС"
- },
- {
- "id": "2772",
- "title": "Время простоя процессора"
- },
- {
- "id": "2782",
- "title": "Общая емкость жестких дисков"
- },
- {
- "id": "2792",
- "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": "zvks_cpu1min",
+ "title": "Загрузка процессора за 1 минуту",
+ "metric": "zvks_cpu1min",
+ "description": "Загрузка процессора за 1 минуту"
},
{
"id": "372",
- "title": "Общее количество активных конференций"
+ "title": "Загрузка процессора за 5 минут"
+ },
+ {
+ "id": "373",
+ "title": "Загрузка процессора за 15 минут"
+ },
+ {
+ "id": "378",
+ "title": "Общий объем SWAP-файла"
+ },
+ {
+ "id": "379",
+ "title": "Используемый объем SWAP-файла"
+ },
+ {
+ "id": "380",
+ "title": "Общий объем физической оперативной памяти"
+ },
+ {
+ "id": "381",
+ "title": "Доступный объем физической оперативной памяти"
},
{
"id": "382",
- "title": "Статус записи"
+ "title": "Свободный объем физической и виртуальной оперативной памяти"
},
{
- "id": "392",
- "title": "Общее количество сохранённых записей"
+ "id": "383",
+ "title": "Буферизованный объем оперативной памяти"
+ },
+ {
+ "id": "384",
+ "title": "Кэшированый объем оперативной памяти"
+ },
+ {
+ "id": "375",
+ "title": "Время затраченное процессором на процессы с пониженным приоритетом"
+ },
+ {
+ "id": "376",
+ "title": "Время затраченное процессором на процессы ядра ОС"
+ },
+ {
+ "id": "377",
+ "title": "Время простоя процессора"
+ },
+ {
+ "id": "385",
+ "title": "Общая емкость жестких дисков"
+ },
+ {
+ "id": "386",
+ "title": "Доступная емкость жестких дисков"
}
]
},
@@ -519,63 +477,63 @@
"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 +543,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 +609,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 +675,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 +847,15 @@
"title": "Общее количество конференций"
},
{
- "id": "373",
+ "id": "373000",
"title": "Общее количество активных конференций"
},
{
- "id": "383",
+ "id": "38300",
"title": "Статус записи"
},
{
- "id": "393",
+ "id": "39300",
"title": "Общее количество сохранённых записей"
}
]
@@ -1281,11 +1239,11 @@
"title": "Общее количество активных конференций"
},
{
- "id": "384",
+ "id": "38400",
"title": "Статус записи"
},
{
- "id": "394",
+ "id": "39400",
"title": "Общее количество сохранённых записей"
}
]
@@ -1671,7 +1629,7 @@
"title": "Общее количество конференций"
},
{
- "id": "379",
+ "id": "37900",
"title": "Общее количество активных конференций"
},
{
@@ -1679,7 +1637,7 @@
"title": "Статус записи"
},
{
- "id": "399",
+ "id": "39900",
"title": "Общее количество сохранённых записей"
}
]
@@ -2447,15 +2405,15 @@
"title": "Общее количество конференций"
},
{
- "id": "378",
+ "id": "37800",
"title": "Общее количество активных конференций"
},
{
- "id": "388",
+ "id": "38800",
"title": "Статус записи"
},
{
- "id": "398",
+ "id": "39800",
"title": "Общее количество сохранённых записей"
}
]
@@ -2841,15 +2799,15 @@
"title": "Общее количество конференций"
},
{
- "id": "375",
+ "id": "37500",
"title": "Общее количество активных конференций"
},
{
- "id": "385",
+ "id": "38500",
"title": "Статус записи"
},
{
- "id": "395",
+ "id": "39500",
"title": "Общее количество сохранённых записей"
}
]
@@ -3229,15 +3187,15 @@
"title": "Общее количество конференций"
},
{
- "id": "376",
+ "id": "37600",
"title": "Общее количество активных конференций"
},
{
- "id": "386",
+ "id": "38600",
"title": "Статус записи"
},
{
- "id": "396",
+ "id": "39600",
"title": "Общее количество сохранённых записей"
}
]
@@ -3617,7 +3575,7 @@
"title": "Общее количество конференций"
},
{
- "id": "377",
+ "id": "37700",
"title": "Общее количество активных конференций"
},
{
@@ -3625,7 +3583,7 @@
"title": "Статус записи"
},
{
- "id": "397",
+ "id": "39700",
"title": "Общее количество сохранённых записей"
}
]
diff --git a/src/Components/TreeChart/tabContent.jsx b/src/Components/TreeChart/tabContent.jsx
index a7845f8..f21b002 100755
--- a/src/Components/TreeChart/tabContent.jsx
+++ b/src/Components/TreeChart/tabContent.jsx
@@ -1,43 +1,23 @@
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('../../Charts/PrometheusChart'));
+const PrometheusChart = lazy(() => import("../../Charts2/PrometheusChart"));
import LazyChartBatchRenderer from "../hooks/LazyChartBatchRender";
-// Функция для генерации названия метрики на основе id
-const getMetricName = (id) => {
- return `zvks_apiforsnmp_measure_${id}`;
-};
-
-// Функция для рекурсивного сбора всех id потомков
-const getAllChildIds = (node) => {
- let ids = [];
- if (node.id) {
- ids.push(node.id); // Добавляем id текущего узла
- }
- if (node.items && node.items.length > 0) {
- node.items.forEach((child) => {
- ids = ids.concat(getAllChildIds(child)); // Рекурсивно собираем id потомков
- });
- }
- return ids;
-};
-
// Компонент Skeleton для графика
const ChartSkeleton = () => (
-
- {/* Заголовок */}
- {/* График */}
+
+
+
);
-// Компонент Skeleton для родительского контейнера
+// Компонент Skeleton для контейнера
const ContainerSkeleton = () => (
-
- {/* Заголовок */}
- {/* Описание */}
- {/* Место для дочерних элементов */}
+
+
+
{[...Array(3)].map((_, i) => (
@@ -46,72 +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}.
- {/*childrenContent*/}
-
- );
-
- // Сохраняем контент для текущего id
- tabContent[node.id] = {
- title: node.title,
- content: content,
- };
- } else {
- // Если у узла нет вложенных элементов, это самый нижний уровень
- const metricName = getMetricName(node.id);
- const content = (
-
-
{node.title}
{/* Используем title узла */}
-
}>
-
-
-
- );
-
- // Сохраняем контент для текущего id
- 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/LoginModal.jsx b/src/Components/UI/LoginModal.jsx
index d38d7b7..f76b7b3 100755
--- a/src/Components/UI/LoginModal.jsx
+++ b/src/Components/UI/LoginModal.jsx
@@ -17,28 +17,27 @@ const LoginModal = ({ onLogin, onClose }) => {
e.preventDefault();
try {
- const response = await axios.post(
- `${import.meta.env.VITE_BACK_URL}/api/auth/login`, {
- method: 'POST',
- credentials: 'include',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ login: username, password }),
- });
-
- const data = await response.json();
+ const { data } = await axios.post(
+ `${import.meta.env.VITE_BACK_URL}/api/auth/login`,
+ { login: username, password },
+ {
+ withCredentials: true,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ }
+ );
if (data.success) {
localStorage.setItem('access_token', data.access_token);
- onLogin(data.user); // Передаем данные пользователя
+ onLogin(data.user);
onClose();
} else {
setError(data.message || "Неверный логин или пароль");
}
} catch (err) {
console.error('Ошибка при отправке запроса:', err);
- setError("Ошибка при подключении к серверу");
+ setError(err.response?.data?.message || "Ошибка при подключении к серверу");
}
};
diff --git a/src/Components/UI/MUItabs.jsx b/src/Components/UI/MUItabs.jsx
index 0ea89aa..173b492 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) { // Middle mouse button
+ if (e.button === 1) { // Средняя кнопка мыши
e.preventDefault();
onCloseTab(id);
}
@@ -26,6 +62,12 @@ const CustomTabs = ({ tabs, activeTab, onTabClick, onCloseTab }) => {
onTabClick(newValue);
};
+ // Статические вкладки (сохраняем оригинальные id)
+ 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)}
diff --git a/src/Components/UI/TreeTable.jsx b/src/Components/UI/TreeTable.jsx
index bbc86b1..3087ddb 100755
--- a/src/Components/UI/TreeTable.jsx
+++ b/src/Components/UI/TreeTable.jsx
@@ -324,7 +324,7 @@ const TreeTable = ({ data }) => {
diff --git a/src/Components/UI/auth.jsx b/src/Components/UI/auth.jsx
index 7532538..c0beca8 100644
--- a/src/Components/UI/auth.jsx
+++ b/src/Components/UI/auth.jsx
@@ -2,22 +2,17 @@ import axios from 'axios';
export const checkAuth = async () => {
try {
- const response = await axios.get(
+ const { data } = await axios.get(
`${import.meta.env.VITE_BACK_URL}/api/auth/check`,
{
- withCredentials: true, // аналог `credentials: 'include'` в fetch
+ withCredentials: true,
headers: {
- Authorization: `Bearer ${localStorage.getItem('access_token') || ''}`,
+ 'Authorization': `Bearer ${localStorage.getItem('access_token') || ''}`,
},
}
);
- // У axios нет свойства .ok, проверяем статус 200-299
- if (response.status >= 200 && response.status < 300) {
- return response.data; // Данные уже в JSON, не нужно .json()
- } else {
- throw new Error('Not authenticated');
- }
+ return data;
} catch (err) {
console.error('Auth check failed:', err);
return { isAuthenticated: false };
diff --git a/src/Components/hooks/LazyChartBatchRender.jsx b/src/Components/hooks/LazyChartBatchRender.jsx
index a12f491..ed351e7 100644
--- a/src/Components/hooks/LazyChartBatchRender.jsx
+++ b/src/Components/hooks/LazyChartBatchRender.jsx
@@ -1,29 +1,167 @@
-import { useEffect, useState } from "react";
+import React, { useEffect, useRef, useState, useCallback } from 'react';
+import Box from '@mui/material/Box';
+import Skeleton from '@mui/material/Skeleton';
-const LazyChartBatchRenderer = ({ charts, batchSize = 3, delay = 150 }) => {
- const [visibleCharts, setVisibleCharts] = useState([]);
+const LazyChartBatchRenderer = ({ charts }) => {
+ const [visibleIndices, setVisibleIndices] = useState(new Set());
+ const placeholderRefs = useRef([]);
+ const observerRef = useRef(null);
+ const cleanupTimeoutRef = useRef(null);
+
+ const ChartSkeleton = () => (
+
+
+
+
+
+
+
+
+
+
+ {[1, 2, 3, 4].map((_, i) => (
+
+ ))}
+
+
+ );
+
+
+ const isElementFarFromViewport = useCallback((element) => {
+ if (!element) return true;
+
+ const rect = element.getBoundingClientRect();
+ const buffer = window.innerHeight * 1.5;
+
+
+ return rect.bottom < -buffer || rect.top > window.innerHeight + buffer;
+ }, []);
+
+
+ const updateVisibleIndices = useCallback(() => {
+ const newVisibleIndices = new Set();
+
+ placeholderRefs.current.forEach((ref, index) => {
+ if (ref && !isElementFarFromViewport(ref)) {
+ newVisibleIndices.add(index);
+ }
+ });
+
+ setVisibleIndices(prev => {
+ if (newVisibleIndices.size === prev.size &&
+ Array.from(newVisibleIndices).every(i => prev.has(i))) {
+ return prev;
+ }
+ return newVisibleIndices;
+ });
+ }, [isElementFarFromViewport]);
useEffect(() => {
- let index = 0;
- const timer = setInterval(() => {
- setVisibleCharts((prev) => [
- ...prev,
- ...charts.slice(index, index + batchSize),
- ]);
- index += batchSize;
- if (index >= charts.length) clearInterval(timer);
- }, delay);
+ observerRef.current = new IntersectionObserver(
+ (entries) => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ updateVisibleIndices();
+ }
+ });
+ },
+ {
+ root: null,
+ rootMargin: '500px 0px',
+ threshold: 0.01
+ }
+ );
- return () => clearInterval(timer);
- }, [charts]);
+ placeholderRefs.current.forEach(ref => {
+ if (ref) observerRef.current.observe(ref);
+ });
+
+ const handleScroll = () => {
+ if (cleanupTimeoutRef.current) {
+ clearTimeout(cleanupTimeoutRef.current);
+ }
+
+ cleanupTimeoutRef.current = setTimeout(() => {
+ updateVisibleIndices();
+
+ setVisibleIndices(prev => {
+ const updated = new Set(prev);
+ let changed = false;
+
+ placeholderRefs.current.forEach((ref, index) => {
+ if (ref && isElementFarFromViewport(ref) && prev.has(index)) {
+ updated.delete(index);
+ changed = true;
+ }
+ });
+
+ return changed ? updated : prev;
+ });
+ }, 150);
+ };
+
+ window.addEventListener('scroll', handleScroll, { passive: true });
+ window.addEventListener('resize', updateVisibleIndices, { passive: true });
+
+ updateVisibleIndices();
+
+ return () => {
+ if (cleanupTimeoutRef.current) clearTimeout(cleanupTimeoutRef.current);
+ if (observerRef.current) observerRef.current.disconnect();
+ window.removeEventListener('scroll', handleScroll);
+ window.removeEventListener('resize', updateVisibleIndices);
+ };
+ }, [updateVisibleIndices]);
+
+ const shouldShowChart = (index) => {
+ return visibleIndices.has(index) ||
+ visibleIndices.has(index - 1) ||
+ visibleIndices.has(index + 1);
+ };
return (
- <>
- {visibleCharts.map((chart, idx) => (
- {chart}
+
+ {charts.map((chart, index) => (
+
(placeholderRefs.current[index] = el)}
+ data-index={index}
+ style={{
+ minHeight: '400px',
+ marginBottom: '20px',
+ transition: 'opacity 0.3s ease',
+ }}
+ >
+ {shouldShowChart(index) ? chart : }
+
))}
- >
+
);
};
-export default LazyChartBatchRenderer;
+export default React.memo(LazyChartBatchRenderer);
\ No newline at end of file
diff --git a/src/Components/hooks/TabContent.jsx b/src/Components/hooks/TabContent.jsx
index bdb6226..cfc050b 100644
--- a/src/Components/hooks/TabContent.jsx
+++ b/src/Components/hooks/TabContent.jsx
@@ -1,12 +1,33 @@
import SystemStatusChart from "../../Charts/SystemStatusChart";
import TreeTable from "../UI/TreeTable";
import FlowChart from "../TreeChart/FlowChart";
+import { getStatusColor } from "../TreeChart/dataUtils";
+
+
+const TabContent = ({ activeTab, tabs, statusHistories, treeData1, tabContent, handleOpenTab }) => {
+ // Функция для подсчета количества элементов каждого статуса
+ const countStatuses = (data) => {
+ const counts = { green: 0, yellow: 0, orange: 0, red: 0 };
+
+ const countRecursive = (node) => {
+ if (node.status) {
+ counts[node.status]++;
+ }
+ if (node.items && node.items.length > 0) {
+ node.items.forEach(child => countRecursive(child));
+ }
+ };
+
+ countRecursive(data);
+ return counts;
+ };
-const TabContent = ({ activeTab, statusHistories, treeData1, tabContent, handleOpenTab }) => {
if (activeTab === "Главная") {
+ const statusCounts = treeData1 ? countStatuses(treeData1) : { green: 0, yellow: 0, orange: 0, red: 0 };
+
return (
-
Общий мониторинг состояния системы
+
Общий мониторинг состояния системы
@@ -17,6 +38,32 @@ const TabContent = ({ activeTab, statusHistories, treeData1, tabContent, handleO
+
+ {/* Контейнер для индикаторов статусов */}
+
+ {Object.entries(statusCounts).map(([status, count]) => (
+
+ {count}
+
+ ))}
+
+
@@ -24,7 +71,7 @@ const TabContent = ({ activeTab, statusHistories, treeData1, tabContent, handleO
} else if (activeTab === "Визуализация") {
return handleOpenTab(id, title)} />;
} else {
- const tabData = tabContent[activeTab];
+ const tabData = tabs.find(t => t.id === activeTab);
return tabData ? tabData.content : Нет данных
;
}
};
diff --git a/src/Components/hooks/useTabs.jsx b/src/Components/hooks/useTabs.jsx
index 188cdf5..6c403fd 100644
--- a/src/Components/hooks/useTabs.jsx
+++ b/src/Components/hooks/useTabs.jsx
@@ -4,23 +4,43 @@ const useTabs = (initialTab) => {
const [tabs, setTabs] = useState([]);
const [activeTab, setActiveTab] = useState(initialTab);
- const handleOpenTab = useCallback((id, title) => {
- setTabs((prevTabs) =>
- prevTabs.some((tab) => tab.id === id)
- ? prevTabs
- : [...prevTabs, { id, title }]
- );
- setActiveTab(id);
+ const handleOpenTab = useCallback((newTab) => {
+ setTabs((prevTabs) => {
+ const exists = prevTabs.some((tab) => tab.id === newTab.id);
+ if (!exists) {
+ return [...prevTabs, newTab];
+ }
+ return prevTabs;
+ });
+ setActiveTab(newTab.id);
}, []);
const handleCloseTab = useCallback((id) => {
- setTabs((prevTabs) => prevTabs.filter((tab) => tab.id !== id));
- if (activeTab === id) {
- setActiveTab(tabs.length > 1 ? tabs[tabs.length - 2].id : initialTab);
- }
- }, [activeTab, tabs, initialTab]);
+ setTabs((prevTabs) => {
+ const newTabs = prevTabs.filter((tab) => tab.id !== id);
+ if (activeTab === id) {
+ setActiveTab(newTabs.length > 0 ? newTabs[newTabs.length - 1].id : initialTab);
+ }
+ return newTabs;
+ });
+ }, [activeTab, initialTab]);
- return { tabs, activeTab, handleOpenTab, handleCloseTab, setActiveTab };
+ const updateTabContent = useCallback((id, content) => {
+ setTabs(prevTabs =>
+ prevTabs.map(tab =>
+ tab.id === id ? { ...tab, content } : tab
+ )
+ );
+ }, []);
+
+ return {
+ tabs,
+ activeTab,
+ handleOpenTab,
+ handleCloseTab,
+ setActiveTab,
+ updateTabContent
+ };
};
-export default useTabs;
\ No newline at end of file
+export default useTabs;
diff --git a/src/Style/theme.jsx b/src/Style/theme.jsx
index 017bc36..6d33a29 100644
--- a/src/Style/theme.jsx
+++ b/src/Style/theme.jsx
@@ -139,7 +139,7 @@ export const darkTheme = createTheme({
// Фоновые цвета
background: {
- default: "#1E1E1E", // Основной фон приложения
+ default: "#2d2d2d", // Основной фон приложения
paper: "#2d2d2d", // Фон "бумажных" поверхностей
},
diff --git a/src/assets/images/system_monitor_icon.svg b/src/assets/images/system_monitor_icon.svg
new file mode 100644
index 0000000..ba02f29
--- /dev/null
+++ b/src/assets/images/system_monitor_icon.svg
@@ -0,0 +1,11 @@
+
diff --git a/src/main.jsx b/src/main.jsx
index e373476..b9a1a6d 100755
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -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(