diff --git a/package.json b/package.json index b7873e5..74c5e26 100755 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "@mui/icons-material": "^6.4.8", "reactflow": "^11.11.4", "vite-plugin-svgr": "^4.3.0", - "react-scripts": "^5.0.1" + "react-scripts": "^5.0.1", + "socket.io-client": "^4.8.1" }, "devDependencies": { "@eslint/js": "^9.17.0", diff --git a/src/Charts/Components/ConnectionStatusIndicator.jsx b/src/Charts/Components/ConnectionStatusIndicator.jsx new file mode 100644 index 0000000..941fed2 --- /dev/null +++ b/src/Charts/Components/ConnectionStatusIndicator.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +export const ConnectionStatusIndicator = ({ connectionStatus }) => { + return ( +
+ {connectionStatus === 'connected' ? 'Online' : + connectionStatus === 'error' ? 'Connection Error' : 'Offline'} +
+ ); +}; \ No newline at end of file diff --git a/src/Charts/Components/CurrentRangeDisplay.jsx b/src/Charts/Components/CurrentRangeDisplay.jsx new file mode 100644 index 0000000..41cfb4c --- /dev/null +++ b/src/Charts/Components/CurrentRangeDisplay.jsx @@ -0,0 +1,17 @@ +import React from 'react'; + +export const CurrentRangeDisplay = ({ useCustomRange, startDate, endDate, selectedRange }) => { + return ( +
+ Текущий диапазон: {useCustomRange + ? `${startDate.toLocaleString()} - ${endDate.toLocaleString()}` + : selectedRange.label} +
+ ); +}; \ No newline at end of file diff --git a/src/Charts/Components/LineChartComponent.jsx b/src/Charts/Components/LineChartComponent.jsx index b1d6963..caca080 100755 --- a/src/Charts/Components/LineChartComponent.jsx +++ b/src/Charts/Components/LineChartComponent.jsx @@ -1,103 +1,231 @@ -import React, { useState } from 'react'; -import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Line, ResponsiveContainer } from 'recharts'; +import React, { useState, useRef, useEffect } from 'react'; +import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Line, ResponsiveContainer, ReferenceArea } from 'recharts'; -const LineChartComponent = ({ chartData, metricName, colors, description, onRangeSelect, filteredData }) => { - const [selectionStart, setSelectionStart] = useState(null); - const [selectionEnd, setSelectionEnd] = useState(null); +const LineChartComponent = ({ + chartData, + metricName, + colors, + onRangeSelect, + filteredData +}) => { + const [selectionArea, setSelectionArea] = useState(null); + const [isSelecting, setIsSelecting] = useState(false); + const chartRef = useRef(null); - const allTimes = Object.values(chartData) + const allTimestamps = Object.values(chartData) .flat() - .map(point => point.time) - .filter((time, index, self) => self.indexOf(time) === index); + .map(point => point.timestamp) + .filter((timestamp, index, self) => self.indexOf(timestamp) === index) + .sort((a, b) => a - b); + + const data = allTimestamps.map(timestamp => { + const point = { timestamp }; + + const firstPoint = Object.values(chartData) + .flat() + .find(p => p.timestamp === timestamp); + + if (firstPoint) { + point.time = firstPoint.time; + point.fullTime = firstPoint.fullTime; + } - const data = allTimes.map(time => { - const point = { time }; Object.keys(chartData).forEach(key => { - const instanceData = chartData[key].find(p => p.time === time); + const instanceData = chartData[key].find(p => p.timestamp === timestamp); point[key] = instanceData ? instanceData.value : null; }); + return point; }); const displayData = filteredData || data; - const handleClick = (e) => { - if (!e || !e.activeLabel) return; + const instanceKeys = displayData.length + ? Object.keys(displayData[0]).filter(k => !['timestamp', 'time', 'fullTime'].includes(k)) + : []; - const clickedTime = e.activeLabel; + // Функция для определения оптимального формата времени в зависимости от диапазона + const getTimeFormat = () => { + if (!data.length) return 'HH:mm:ss'; - if (!selectionStart) { - setSelectionStart(clickedTime); - } else if (!selectionEnd) { - setSelectionEnd(clickedTime); + const first = data[0].timestamp; + const last = data[data.length - 1].timestamp; + const range = last - first; - const startIndex = data.findIndex(point => point.time === selectionStart); - const endIndex = data.findIndex(point => point.time === clickedTime); - - onRangeSelect({ startIndex, endIndex }); - - setSelectionStart(null); - setSelectionEnd(null); + // Если диапазон больше 24 часов - показываем дату + if (range > 86400000) { + return 'dd.MM HH:mm'; } + // Если больше 1 часа - показываем часы и минуты + if (range > 3600000) { + return 'HH:mm'; + } + // Для коротких диапазонов - показываем время с секундами + return 'HH:mm:ss'; + }; + + useEffect(() => { + const handleSelectStart = (e) => { + if (isSelecting) { + e.preventDefault(); + } + }; + + + document.addEventListener('selectstart', handleSelectStart); + return () => document.removeEventListener('selectstart', handleSelectStart); + }, [isSelecting]); + + const handleMouseDown = (e) => { + if (!e) return; + + // Получаем индекс точки по координатам + const activeIndex = e.activeTooltipIndex; + if (activeIndex === undefined || activeIndex < 0 || activeIndex >= data.length) return; + + setIsSelecting(true); + setSelectionArea({ + start: data[activeIndex].timestamp, + end: null, + startIndex: activeIndex, + endIndex: null + }); + }; + + const handleMouseMove = (e) => { + if (!isSelecting || !selectionArea?.start || !e) return; + + const activeIndex = e.activeTooltipIndex; + if (activeIndex === undefined || activeIndex < 0 || activeIndex >= data.length) return; + + setSelectionArea(prev => ({ + ...prev, + end: data[activeIndex].timestamp, + endIndex: activeIndex + })); + }; + + const handleMouseUp = () => { + if (!isSelecting || !selectionArea?.start || !selectionArea?.end) { + setIsSelecting(false); + setSelectionArea(null); + return; + } + + 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); + + onRangeSelect({ + startIndex: normalizedStart, + endIndex: normalizedEnd + }); + + setIsSelecting(false); + setSelectionArea(null); }; - // Упрощенный Tooltip без указания instance const CustomTooltip = ({ active, payload, label }) => { if (active && payload && payload.length) { + const currentPoint = data.find(point => point.timestamp === label); return ( -
-

{`Время: ${label}`}

-

{`Значение: ${payload[0].value}`}

+

+ {currentPoint?.fullTime || new Date(label).toLocaleString('ru-RU')} +

+ {payload.map((item, index) => ( +

+ {`Значение: ${item.value}`} +

+ ))}
); } return null; }; + if (!data.length) { + return
Нет данных для отображения
; + } + return ( -
- +
+ { + const date = new Date(timestamp); + const format = getTimeFormat(); + + if (format === 'dd.MM HH:mm') { + return date.toLocaleString('ru-RU', { + day: '2-digit', + month: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); + } else if (format === 'HH:mm') { + return date.toLocaleString('ru-RU', { + hour: '2-digit', + minute: '2-digit' + }); + } else { + return date.toLocaleString('ru-RU', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + } + }} /> - - } - cursor={{ stroke: '#ccc', strokeWidth: 1 }} - /> - {/* Убрали чтобы скрыть имена instance */} - {Object.keys(chartData).map((key, index) => ( + + + } /> + {instanceKeys.map((instance, index) => ( ))} + {selectionArea?.start && selectionArea?.end && ( + + )}
); }; -export default LineChartComponent; \ No newline at end of file +export default React.memo(LineChartComponent); \ No newline at end of file diff --git a/src/Charts/Components/TimeRangeSelector.jsx b/src/Charts/Components/TimeRangeSelector.jsx new file mode 100644 index 0000000..d5111dc --- /dev/null +++ b/src/Charts/Components/TimeRangeSelector.jsx @@ -0,0 +1,151 @@ +import React from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import { TIME_RANGES } from './constants'; + +export const TimeRangeSelector = ({ + selectedRange, + handleRangeChange, + startDate, + setStartDate, + endDate, + setEndDate, + useCustomRange, + handleCustomRangeChange, + handleResetZoom +}) => { + return ( +
+ {/* Стандартные диапазоны */} +
+
+ + +
+ + +
+ + {/* Кастомный диапазон */} +
+
+ Или укажите свой диапазон: +
+
+
+ setStartDate(date)} + showTimeSelect + timeFormat="HH:mm" + timeIntervals={15} + dateFormat="yyyy-MM-dd HH:mm" + placeholderText="Начальная дата" + customInput={ + + } + /> +
+
+ setEndDate(date)} + showTimeSelect + timeFormat="HH:mm" + timeIntervals={15} + dateFormat="yyyy-MM-dd HH:mm" + placeholderText="Конечная дата" + customInput={ + + } + /> +
+ +
+
+
+ ); +}; \ No newline at end of file diff --git a/src/Charts/Components/constants.jsx b/src/Charts/Components/constants.jsx new file mode 100644 index 0000000..68f92d9 --- /dev/null +++ b/src/Charts/Components/constants.jsx @@ -0,0 +1,20 @@ +export const TIME_RANGES = [ + { label: '1 минута', value: 60, interval: 3000 }, + { label: '5 минут', value: 300, interval: 15000 }, + { label: '30 минут', value: 1800, interval: 90000 }, + { label: '1 час', value: 3600, interval: 180000 }, + { label: '3 часа', value: 10800, interval: 540000 }, + { label: '6 часов', value: 21600, interval: 1080000 }, + { label: '12 часов', value: 43200, interval: 2160000 }, + { label: '24 часа', value: 86400, interval: 4320000 }, + { label: '2 дня', value: 172800, interval: 8640000 }, + { label: '7 дней', value: 604800, interval: 30240000 }, + { label: '30 дней', value: 2592000, interval: 129600000 }, + { label: '90 дней', value: 7776000, interval: 388800000 }, + { label: '6 месяцев', value: 15552000, interval: 777600000 }, + { label: '9 месяцев', value: 23328000, interval: 1166400000 }, + { label: '1 год', value: 31536000, interval: 1576800000 }, +]; + +export const COLORS = ['#3e95cd', '#8e5ea2', '#3cba9f', '#e8c3b9', '#c45850']; +export const MAX_POINTS = 20; \ No newline at end of file diff --git a/src/Charts/NegativeStatusChart.jsx b/src/Charts/NegativeStatusChart.jsx deleted file mode 100755 index 0ee7531..0000000 --- a/src/Charts/NegativeStatusChart.jsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; -import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; - -const SystemStatusChart = ({ data }) => { - // Обрезаем массив, оставляя только последние 20 точек - const trimmedData = data.slice(-20); - - return ( - - - - - - - - - - - ); -}; - -export default SystemStatusChart; \ No newline at end of file diff --git a/src/Charts/PrometheusChart.jsx b/src/Charts/PrometheusChart.jsx index c8a06ab..3d38a8a 100755 --- a/src/Charts/PrometheusChart.jsx +++ b/src/Charts/PrometheusChart.jsx @@ -1,369 +1,494 @@ -import React, { useEffect, useState, useRef } from 'react'; -import axios from 'axios'; -import DatePicker from 'react-datepicker'; -import 'react-datepicker/dist/react-datepicker.css'; +import React, { useEffect, useState, useRef, useCallback } from 'react'; +import { io } from 'socket.io-client'; import LineChartComponent from './Components/LineChartComponent'; - -const MAX_POINTS = 20; -const COLORS = ['#3e95cd', '#8e5ea2', '#3cba9f', '#e8c3b9', '#c45850']; -const TIME_RANGES = [ - { label: '1 минута', value: 60, interval: 3000 }, - { label: '5 минут', value: 300, interval: 15000 }, - { label: '30 минут', value: 1800, interval: 90000 }, - { label: '1 час', value: 3600, interval: 180000 }, - { label: '3 часа', value: 10800, interval: 540000 }, - { label: '6 часов', value: 21600, interval: 1080000 }, - { label: '12 часов', value: 43200, interval: 2160000 }, - { label: '24 часа', value: 86400, interval: 4320000 }, - { label: '2 дня', value: 172800, interval: 8640000 }, - { label: '7 дней', value: 604800, interval: 30240000 }, - { label: '30 дней', value: 2592000, interval: 129600000 }, - { label: '90 дней', value: 7776000, interval: 388800000 }, - { label: '6 месяцев', value: 15552000, interval: 777600000 }, - { label: '9 месяцев', value: 23328000, interval: 1166400000 }, - { label: '1 год', value: 31536000, interval: 1576800000 }, -]; +import { TimeRangeSelector } from './Components/TimeRangeSelector'; +import { ConnectionStatusIndicator } from './Components/ConnectionStatusIndicator'; +import { CurrentRangeDisplay } from './Components/CurrentRangeDisplay'; +import { TIME_RANGES, COLORS } from './Components/constants'; +import axios from 'axios'; const PrometheusChart = ({ metricName }) => { - const [chartData, setChartData] = useState({}); + const [chartData, setChartData] = useState(null); const [selectedRange, setSelectedRange] = useState(TIME_RANGES[0]); const [startDate, setStartDate] = useState(new Date()); const [endDate, setEndDate] = useState(new Date()); const [useCustomRange, setUseCustomRange] = useState(false); - const [selectedGraphRange, setSelectedGraphRange] = useState(null); // Выбранный диапазон - const [filteredData, setFilteredData] = useState(null); // Отфильтрованные данные + const [connectionStatus, setConnectionStatus] = useState('disconnected'); + const [selectedGraphRange, setSelectedGraphRange] = useState(null); + const [filteredData, setFilteredData] = useState(null); + const [isSelectingRange, setIsSelectingRange] = useState(false); + const [lastCustomRange, setLastCustomRange] = useState(null); const intervalRef = useRef(null); + const socketRef = useRef(null); + const debounceRef = useRef(null); - // Функция для интерполяции данных - const interpolateData = (data, minPoints = 15) => { - if (data.length >= minPoints) return data; + const formatTime = useCallback((timestamp, rangeSeconds) => { + const ts = typeof timestamp === 'number' ? timestamp : Date.now(); + const date = new Date(ts); - const interpolatedData = []; - for (let i = 0; i < data.length - 1; i++) { - interpolatedData.push(data[i]); + // Определяем формат в зависимости от диапазона + const showFullDate = rangeSeconds > 86400; // больше суток - const currentPoint = data[i]; - const nextPoint = data[i + 1]; + const timeOptions = { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false + }; - // Вычисляем разницу во времени между точками - const currentTime = new Date(currentPoint.time).getTime(); - const nextTime = new Date(nextPoint.time).getTime(); - const timeDiff = nextTime - currentTime; + const dateOptions = showFullDate ? { + month: '2-digit', + day: '2-digit', + ...timeOptions + } : timeOptions; - // Добавляем промежуточные точки - const steps = Math.ceil((minPoints - data.length) / (data.length - 1)); - for (let j = 1; j <= steps; j++) { - const interpolatedTime = new Date(currentTime + (timeDiff * j) / (steps + 1)).toLocaleString(); - const interpolatedPoint = { time: interpolatedTime }; + return { + display: date.toLocaleString('ru-RU', dateOptions), + fullDisplay: date.toLocaleString('ru-RU', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false + }), + timestamp: ts + }; + }, []); - // Интерполируем значения для каждой метрики - Object.keys(currentPoint).forEach(key => { - if (key !== 'time') { - const currentValue = currentPoint[key]; - const nextValue = nextPoint[key]; - interpolatedPoint[key] = currentValue + ((nextValue - currentValue) * j) / (steps + 1); - } + const calculateStep = useCallback((start, end) => { + const range = end - start; + if (range <= 60) return 1; // 1 мин + if (range <= 300) return 5; // 5 мин + if (range <= 1800) return 15; // 30 мин + if (range <= 3600) return 30; // 1 час + if (range <= 10800) return 60; // 3 часа + if (range <= 21600) return 120; // 6 часов + if (range <= 43200) return 300; // 12 часов + if (range <= 86400) return 600; // 24 часа + return 1800; // > 24 часов + }, []); + + const fetchData = useCallback(() => { + + if (isSelectingRange) return; + + const now = Math.floor(Date.now() / 1000); + const start = now - selectedRange.value; + const end = now; + const step = calculateStep(start, end); + + if (socketRef.current?.connected) { + socketRef.current.emit('get-metrics', { + metric: metricName, + start, + end, + step, + _t: Date.now() + }); + } + }, [metricName, selectedRange.value, isSelectingRange]); + + const processMetricsData = useCallback((response) => { + 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; + + 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(); + } + + const value = parseFloat(item.value); + const formattedTime = formatTime(timestamp, rangeSeconds); + + newData[instance].push({ + time: formattedTime.display, + fullTime: formattedTime.fullDisplay, + value: value, + timestamp: timestamp }); + }); - interpolatedData.push(interpolatedPoint); - } + // Сортируем и ограничиваем данные + Object.keys(newData).forEach(instance => { + newData[instance] = newData[instance] + .sort((a, b) => a.timestamp - b.timestamp) + .slice(-1000); + }); + return newData; + }); + }, [metricName, selectedRange.value, formatTime, useCustomRange, startDate, endDate]); + + const setupWebSocket = useCallback(() => { + if (socketRef.current) { + // Если соединение уже существует, возвращаем его + if (socketRef.current.connected) return socketRef.current; + // Если соединение в процессе переподключения, тоже возвращаем + if (socketRef.current.reconnecting) return socketRef.current; } - interpolatedData.push(data[data.length - 1]); // Добавляем последнюю точку - return interpolatedData.slice(0, minPoints); // Обрезаем до minPoints - }; + const socket = io('http://192.168.2.39:3000/api/metrics-ws', { + transports: ['websocket'], + reconnection: true, + reconnectionAttempts: Infinity, + reconnectionDelay: 1000, + reconnectionDelayMax: 5000, + }); + + socketRef.current = socket; + + socket.on('connect', () => { + console.log('WebSocket connected'); + setConnectionStatus('connected'); + fetchData(); + }); + + socket.on('disconnect', (reason) => { + console.log('WebSocket disconnected:', reason); + setConnectionStatus('disconnected'); + if (reason === 'io server disconnect') socket.connect(); + }); + + socket.on('connect_error', (error) => { + console.error('WebSocket connection error:', error); + setConnectionStatus('error'); + setTimeout(() => socket.connect(), 1000); + }); + + socket.on('metrics-data', (response) => { + console.log('Received raw metrics data:', response); + processMetricsData(response); + }); + + socket.on('metrics-error', (error) => { + console.error('Metrics error:', error); + setConnectionStatus('error'); + }); + + return socket; + }, []); + + const fetchCustomRangeData = useCallback(async () => { + const start = Math.floor(startDate.getTime() / 1000); + const end = Math.floor(endDate.getTime() / 1000); + const rangeSeconds = end - start; - const fetchData = async () => { try { - let start, end; - - if (useCustomRange) { - start = Math.floor(startDate.getTime() / 1000); - end = Math.floor(endDate.getTime() / 1000); - } else { - end = Math.floor(Date.now() / 1000); - start = end - selectedRange.value; - } - - let step; - const range = end - start; - if (range <= 3600) step = 5; - else if (range <= 21600) step = 30; - else if (range <= 86400) step = 120; - else step = 300; - - const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/metrics`, { + const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/metrics`, { params: { metric: metricName, start, end, - step + step: calculateStep(start, end) } }); - - const result = response.data; - let metrics = Array.isArray(result) ? result : result.data || []; - if (!Array.isArray(metrics)) { - metrics = []; - } - - const timePoints = []; - for (let t = start; t <= end; t += step) { - const date = new Date(t * 1000); - const formattedTime = range > 86400 - ? date.toLocaleString([], { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) - : date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); - - timePoints.push(formattedTime); - } - - const updatedData = {}; - metrics.forEach(m => { - const date = new Date(m.timestamp); - const formattedTime = range > 86400 - ? date.toLocaleString([], { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) - : date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); - - const key = m.instance; - if (!updatedData[key]) updatedData[key] = {}; - updatedData[key][formattedTime] = m.value; - }); - - const chartData = {}; - Object.keys(updatedData).forEach(key => { - chartData[key] = timePoints.map(time => ({ - time, - value: updatedData[key][time] ?? null, + if (response.data?.length) { + // Преобразуем данные перед передачей в processMetricsData + const processedData = response.data.map(item => ({ + ...item, + timestamp: item.timestamp, // оставляем в секундах - processMetricsData конвертирует + value: item.value.toString() })); + + processMetricsData({ + metric: metricName, + data: processedData + }); + } + } catch (error) { + console.error('Ошибка при получении кастомных данных:', error); + } + }, [metricName, startDate, endDate, calculateStep, processMetricsData]); + + + const handleRangeChange = useCallback((event) => { + // Очищаем текущий интервал + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + + const selectedValue = event.target.value; + const range = TIME_RANGES.find(r => r.value === parseInt(selectedValue, 10)); + + setSelectedRange(range); + setUseCustomRange(false); + setChartData(null); + setSelectedGraphRange(null); + setFilteredData(null); + + const now = new Date(); + setEndDate(now); + setStartDate(new Date(now.getTime() - range.value * 1000)); + + // Переподключение сокета + if (!socketRef.current?.connected) { + socketRef.current?.connect(); + } + }, []); + + const handleCustomRangeChange = useCallback(() => { + // Отключаем WebSocket соединение + if (socketRef.current?.connected) { + socketRef.current.disconnect(); + setConnectionStatus('disconnected'); + } + + // Очищаем интервал обновления + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + + setUseCustomRange(true); + setChartData(null); + setSelectedGraphRange(null); + setFilteredData(null); + fetchCustomRangeData(); + }, [fetchCustomRangeData]); + + const handleResetZoom = useCallback(() => { + setSelectedGraphRange(null); + setFilteredData(null); + setIsSelectingRange(false); + + if (useCustomRange) { + fetchCustomRangeData(); + } else { + if (!socketRef.current?.connected) { + socketRef.current?.connect(); + } + fetchData(); + } + + if (lastCustomRange) { + handleRangeSelect(lastCustomRange); + return; + } + }, [fetchData, fetchCustomRangeData, useCustomRange]); + + const interpolateData = useCallback((data, targetPointCount) => { + if (!data || data.length < 2) return data; + if (data.length >= targetPointCount) return data; + + const interpolated = []; + const step = (data.length - 1) / (targetPointCount - 1); + + for (let i = 0; i < targetPointCount; i++) { + const index = i * step; + const lowerIndex = Math.floor(index); + const upperIndex = Math.ceil(index); + + if (lowerIndex === upperIndex) { + interpolated.push(data[lowerIndex]); + continue; + } + + const fraction = index - lowerIndex; + const interpolatedPoint = {}; + + Object.keys(data[lowerIndex]).forEach(key => { + if (key === 'timestamp') { + interpolatedPoint[key] = data[lowerIndex][key] + + fraction * (data[upperIndex][key] - data[lowerIndex][key]); + + // Добавляем отображаемое время + const { display, fullDisplay } = formatTime(interpolatedPoint[key], + (endDate - startDate) / 1000); + interpolatedPoint.time = display; + interpolatedPoint.fullTime = fullDisplay; + } else if (typeof data[lowerIndex][key] === 'number') { + interpolatedPoint[key] = data[lowerIndex][key] + + fraction * (data[upperIndex][key] - data[lowerIndex][key]); + } else { + interpolatedPoint[key] = data[lowerIndex][key]; + } }); - setChartData(chartData); - } catch (error) { - console.error('Ошибка при загрузке метрик:', error); + interpolated.push(interpolatedPoint); } - }; + + return interpolated; + }, []); + + const handleRangeSelect = useCallback((range) => { + setLastCustomRange(range); + if (!range || !chartData) return; + + setIsSelectingRange(true); + setSelectedGraphRange(range); + + // Отключаем автоматические обновления + if (socketRef.current?.connected) { + socketRef.current.disconnect(); + } + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + + // Получаем все точки и сортируем по времени + const allPoints = Object.values(chartData).flat(); + const sortedPoints = allPoints.sort((a, b) => a.timestamp - b.timestamp); + + // Вычисляем абсолютные индексы + const startIndex = Math.floor(range.startIndex * (sortedPoints.length - 1)); + const endIndex = Math.floor(range.endIndex * (sortedPoints.length - 1)); + + // Фильтруем точки по выбранному диапазону + const filtered = sortedPoints.slice(startIndex, endIndex + 1); + + // Применяем интерполяцию только если точек меньше 100 + const interpolated = filtered.length < 100 ? + interpolateData(filtered, Math.min(100, filtered.length * 3)) : + filtered; + + setFilteredData(interpolated); + setIsSelectingRange(false); + }, [chartData, interpolateData, formatTime]); useEffect(() => { - fetchData(); + const socket = setupWebSocket(); + return () => { + clearInterval(intervalRef.current); + socket.disconnect(); + }; + }, [setupWebSocket]); - intervalRef.current = setInterval(() => { - fetchData(); - }, selectedRange.interval); + // Обновим useEffect для кастомного диапазона + useEffect(() => { + if (useCustomRange && !isSelectingRange) { + // Очищаем предыдущий таймер + if (debounceRef.current) { + clearTimeout(debounceRef.current); + } + + // Устанавливаем новый таймер с задержкой 500 мс + debounceRef.current = setTimeout(() => { + fetchCustomRangeData(); + }, 500); + } + + return () => { + if (debounceRef.current) { + clearTimeout(debounceRef.current); + } + }; + }, [useCustomRange, isSelectingRange, startDate, endDate, fetchCustomRangeData]); + + useEffect(() => { + if (useCustomRange || isSelectingRange) return; + + const fetchDataWrapper = () => { + try { + fetchData(); + } catch (error) { + console.error('Error in interval fetch:', error); + } + }; + + // Очищаем предыдущий интервал + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + + // Запускаем сразу и затем по интервалу + fetchDataWrapper(); + intervalRef.current = setInterval(fetchDataWrapper, selectedRange.interval); return () => { if (intervalRef.current) { clearInterval(intervalRef.current); } }; - }, [metricName, selectedRange, useCustomRange, startDate, endDate]); - - const handleRangeChange = (event) => { - const selectedValue = event.target.value; - const range = TIME_RANGES.find(range => range.value === parseInt(selectedValue, 10)); - setSelectedRange(range); - setUseCustomRange(false); - setSelectedGraphRange(null); // Сбрасываем выбранный диапазон - setFilteredData(null); // Сбрасываем отфильтрованные данные - }; - - const handleCustomRangeChange = () => { - setUseCustomRange(true); - setSelectedGraphRange(null); // Сбрасываем выбранный диапазон - setFilteredData(null); // Сбрасываем отфильтрованные данные - }; + }, [fetchData, selectedRange.interval, useCustomRange, isSelectingRange]); useEffect(() => { - if (selectedGraphRange) { - const { startIndex, endIndex } = selectedGraphRange; - const allTimes = Object.values(chartData) - .flat() - .map(point => point.time) - .filter((time, index, self) => self.indexOf(time) === index); - - const data = allTimes.map(time => { - const point = { time }; - Object.keys(chartData).forEach(key => { - const instanceData = chartData[key].find(p => p.time === time); - point[key] = instanceData ? instanceData.value : null; - }); - return point; - }); - - const filtered = data.slice(startIndex, endIndex + 1); - - // Интерполируем данные, если точек меньше 15 - const interpolated = interpolateData(filtered, 15); - setFilteredData(interpolated); // Сохраняем интерполированные данные - } else { - setFilteredData(null); // Сбрасываем фильтрацию, если диапазон не выбран + if (!selectedGraphRange || !chartData) { + setFilteredData(null); + return; } - }, [selectedGraphRange, chartData]); - if (!Object.keys(chartData).length) return

Loading...

; + const allPoints = Object.values(chartData).flat(); + const sortedPoints = allPoints.sort((a, b) => a.timestamp - b.timestamp); - const allTimes = Object.values(chartData) - .flat() - .map(point => point.time) - .filter((time, index, self) => self.indexOf(time) === index); + const startIndex = Math.floor(selectedGraphRange.startIndex * (sortedPoints.length - 1)); + const endIndex = Math.floor(selectedGraphRange.endIndex * (sortedPoints.length - 1)); - const data = allTimes.map(time => { - const point = { time }; - Object.keys(chartData).forEach(key => { - const instanceData = chartData[key].find(p => p.time === time); - point[key] = instanceData ? instanceData.value : null; - }); - return point; - }); + const filtered = sortedPoints.slice(startIndex, endIndex + 1); + const interpolated = filtered.length > 100 ? + interpolateData(filtered, 100) : + filtered; + + setFilteredData(interpolated); + }, [selectedGraphRange, chartData, interpolateData]); + + if (chartData === null) { + return
Loading data...
; + } + + if (Object.keys(chartData).length === 0) { + return
No data available
; + } return (
- {/* Заголовок графика */} -

-

+ - {/* Группа элементов управления */} -
- {/* Стандартные диапазоны */} -
- - -
+ - {/* Кастомный диапазон */} -
-
- Или укажите свой диапазон: -
-
-
- setStartDate(date)} - showTimeSelect - timeFormat="HH:mm" - timeIntervals={15} - dateFormat="yyyy-MM-dd HH:mm" - placeholderText="Начальная дата" - customInput={ - - } - /> -
-
- setEndDate(date)} - showTimeSelect - timeFormat="HH:mm" - timeIntervals={15} - dateFormat="yyyy-MM-dd HH:mm" - placeholderText="Конечная дата" - customInput={ - - } - /> -
- -
-
-
+ - {/* Индикатор текущего диапазона */} -
- Текущий диапазон: {useCustomRange - ? `${startDate.toLocaleString()} - ${endDate.toLocaleString()}` - : selectedRange.label} -
- - {/* График */}
); }; -export default PrometheusChart; \ No newline at end of file +export default React.memo(PrometheusChart); + diff --git a/src/Charts/SystemStatusChart.jsx b/src/Charts/SystemStatusChart.jsx index 0ee7531..5d82137 100755 --- a/src/Charts/SystemStatusChart.jsx +++ b/src/Charts/SystemStatusChart.jsx @@ -5,6 +5,22 @@ const SystemStatusChart = ({ data }) => { // Обрезаем массив, оставляя только последние 20 точек const trimmedData = data.slice(-20); + const CustomTooltip = ({ active, payload, label }) => { + if (active && payload && payload.length) { + return ( +
+

{`Время: ${label}`}

+

{`Значение: ${payload[0].value}`}

+
+ ); + } + return null; + }; + return ( { - - + } /> diff --git a/src/Charts/SystemStatusTable.jsx b/src/Charts/SystemStatusTable.jsx deleted file mode 100755 index ae03c92..0000000 --- a/src/Charts/SystemStatusTable.jsx +++ /dev/null @@ -1,84 +0,0 @@ -import React, { useState, useEffect } from "react"; -import "../Style/SystemStatusTable.css"; -import axios from "axios"; - -const SystemStatusTable = () => { - const [systemData, setSystemData] = useState([]); - const [expandedRow, setExpandedRow] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - // Загрузка данных с бэкенда - useEffect(() => { - const fetchData = async () => { - try { - const response = await axios.get("/trust.json"); // Укажите ваш endpoint - setSystemData(response.data); - setLoading(false); - } catch (err) { - setError(err.message); - setLoading(false); - } - }; - - fetchData(); - }, []); - - // Обработчик для кнопки "Подробнее" - const handleDetailsClick = (id) => { - setExpandedRow(expandedRow === id ? null : id); - }; - - if (loading) { - return

Загрузка данных...

; - } - - if (error) { - return

Ошибка: {error}

; - } - - return ( - - - - - - - - - - - - {systemData.map((item) => ( - - - - - - - - {expandedRow === item.id && ( - - - - )} - - ))} - -
-

Состояние системы

-
МетрикаЗначениеСтатусДетали
{item.name}{item.value}% - {item.status} - - -
-
-

{item.details}

-
-
- ); -}; - -export default SystemStatusTable; \ No newline at end of file diff --git a/src/Charts/SystemStatusTableSoftware.jsx b/src/Charts/SystemStatusTableSoftware.jsx deleted file mode 100755 index ba736b4..0000000 --- a/src/Charts/SystemStatusTableSoftware.jsx +++ /dev/null @@ -1,84 +0,0 @@ -import React, { useState, useEffect } from "react"; -import "../Style/SystemStatusTable.css"; -import axios from "axios"; - -const SystemStatusTableSoftware = () => { - const [systemData, setSystemData] = useState([]); - const [expandedRow, setExpandedRow] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - // Загрузка данных с бэкенда - useEffect(() => { - const fetchData = async () => { - try { - const response = await axios.get("/TrustSoftware.json"); // Укажите ваш endpoint - setSystemData(response.data); - setLoading(false); - } catch (err) { - setError(err.message); - setLoading(false); - } - }; - - fetchData(); - }, []); - - // Обработчик для кнопки "Подробнее" - const handleDetailsClick = (id) => { - setExpandedRow(expandedRow === id ? null : id); - }; - - if (loading) { - return

Загрузка данных...

; - } - - if (error) { - return

Ошибка: {error}

; - } - - return ( - - - - - - - - - - - - {systemData.map((item) => ( - - - - - - - - {expandedRow === item.id && ( - - - - )} - - ))} - -
-

Состояние ПО

-
МетрикаЗначениеСтатусДетали
{item.name}{item.value}% - {item.status} - - -
-
-

{item.details}

-
-
- ); -}; - -export default SystemStatusTableSoftware; \ No newline at end of file diff --git a/src/Components/Layout/Dashboard.jsx b/src/Components/Layout/Dashboard.jsx index 77b2715..c979b4c 100755 --- a/src/Components/Layout/Dashboard.jsx +++ b/src/Components/Layout/Dashboard.jsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from "react"; +import { Box, styled } from "@mui/material"; import SidebarMenu from "./SidebarMenu"; -import "../../Style/Dashboard.css"; import { statusManager1, statusManager2 } from "../TreeChart/dataUtils"; import generateTabContent from "../TreeChart/tabContent"; import CustomTabs from "../UI/MUItabs"; @@ -9,6 +9,53 @@ import useSidebarResize from "../hooks/useSidebarResize"; import TabContent from "../hooks/TabContent"; import menuData from "../TreeChart/menuData.json"; +// Создаем стилизованные компоненты +const DashboardContainer = styled(Box)(({ theme }) => ({ + display: 'flex', + height: '100vh', + width: '100vw', + overflow: 'hidden', + backgroundColor: theme.palette.background.default, + 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 + 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 { tabs, activeTab, handleOpenTab, handleCloseTab, setActiveTab } = useTabs("Главная"); const { sidebarWidth, startResizing } = useSidebarResize(250); @@ -20,13 +67,11 @@ const Dashboard = () => { history2: [], }); - // Генерация контента для вкладок useEffect(() => { const generatedTabContent = generateTabContent(menuData); setTabContent(generatedTabContent); }, []); - // Обновление статусов каждые 30 секунд useEffect(() => { const interval = setInterval(() => { const updatedData1 = JSON.parse(JSON.stringify(treeData1)); @@ -56,15 +101,20 @@ const Dashboard = () => { }, [treeData1, treeData2]); return ( -
+ {/* Сайдбар */} -
- -
-
+ + + + {/* Основной контент */} -
+ {/* Вкладки */} { /> {/* Контент вкладки */} -
+ { tabContent={tabContent} handleOpenTab={handleOpenTab} /> -
-
-
+ + +
); }; diff --git a/src/Components/Layout/SidebarMenu.jsx b/src/Components/Layout/SidebarMenu.jsx index 3c7f0d6..379d7db 100644 --- a/src/Components/Layout/SidebarMenu.jsx +++ b/src/Components/Layout/SidebarMenu.jsx @@ -1,49 +1,151 @@ -import React from "react"; -import { Drawer, List } from "@mui/material"; +import React, { useState } from "react"; +import { + Drawer, + List, + Typography, + styled, + IconButton, + Tooltip, + Box +} from "@mui/material"; +import { + ChevronLeft, + ChevronRight, + Menu as MenuIcon +} from "@mui/icons-material"; import MenuItem from "./SidebarMenuComponents/MenuItem"; import SidebarFooter from "./SidebarMenuComponents/SidebarFooter"; +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 [collapsed, setCollapsed] = useState(false); + const [hovered, setHovered] = useState(false); + + const handleToggleCollapse = () => { + setCollapsed(!collapsed); + }; + const handleSelectItem = (id, title, children) => { onOpenTab(id, title, children); }; + const drawerWidth = collapsed ? 64 : sidebarWidth; + return ( - setHovered(true)} + onMouseLeave={() => setHovered(false)} sx={{ - width: sidebarWidth, - flexShrink: 0, - "& .MuiDrawer-paper": { - width: sidebarWidth, - boxSizing: "border-box", - display: "flex", - flexDirection: "column", - }, + position: 'relative', + width: drawerWidth, + transition: 'width 0.3s ease', }} > - -

Меню

- -
- - {/* Ресайзер */} -
+ > + {/* Кнопка сворачивания/разворачивания */} + + + + {collapsed ? ( + hovered ? : + ) : ( + + )} + + + - - + {/* Содержимое меню */} + + + {!collapsed && ( + + Меню + + )} + + + + {/* Футер */} + {!collapsed && ( + + )} + + + {/* Ресайзер */} + {!collapsed && ( + + )} + + ); }; -export default SidebarMenu; +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 d8d48d6..69a105c 100644 --- a/src/Components/Layout/SidebarMenuComponents/MenuItem.jsx +++ b/src/Components/Layout/SidebarMenuComponents/MenuItem.jsx @@ -1,95 +1,90 @@ import React from "react"; -import { ListItem, ListItemIcon, ListItemText, Collapse, List } from "@mui/material"; +import { + ListItem, + ListItemIcon, + ListItemText, + Collapse, + List, + styled +} from "@mui/material"; import { ExpandLess, ExpandMore, Folder, FolderOpen } from "@mui/icons-material"; -// Функция для сбора всех потомков -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; -}; +const StyledListItem = styled(ListItem)(({ theme, level }) => ({ + cursor: "pointer", + paddingLeft: theme.spacing(2 + level * 2), + '&:hover': { + backgroundColor: theme.palette.action.hover, + }, + '&.Mui-selected': { + backgroundColor: theme.palette.custom.sidebarHover, + }, +})); -const MenuItem = ({ item, onSelectItem, level = 0 }) => { // Добавлен параметр level для отслеживания уровня вложенности +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 hasChildren = Array.isArray(item.items) && item.items.length > 0; - const handleToggle = () => { + + const handleToggle = (e) => { + e.stopPropagation(); setIsOpen(!isOpen); }; const handleOpenTab = (e) => { - e.stopPropagation(); // Останавливаем всплытие события - const allChildren = getAllChildren(item); // Собираем всех потомков - onSelectItem(item.id, item.title, allChildren); // Передаем данные в родительский компонент + e.stopPropagation(); + const allChildren = getAllChildren(item); + onSelectItem(item.id, item.title, allChildren); }; return ( <> - - - {hasChildren ? ( -
- {isOpen ? : } -
- ) : ( -
- {/* Здесь можно добавить другую иконку или оставить пустым */} -
- )} + + + {hasChildren ? (isOpen ? : ) : } + - - {hasChildren && (isOpen ? : )} -
- {hasChildren && ( + + {!collapsed && ( // Показываем текст только в развернутом состоянии + <> + + {hasChildren && (isOpen ? : )} + + )} + + + {hasChildren && !collapsed && ( // Показываем детей только в развернутом состоянии {item.items.map((child, index) => ( - ))} @@ -99,4 +94,16 @@ const MenuItem = ({ item, onSelectItem, level = 0 }) => { // Добавлен п ); }; +// Вспомогательная функция (остается без изменений) +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/SidebarMenuComponents/SidebarFooter.jsx b/src/Components/Layout/SidebarMenuComponents/SidebarFooter.jsx index ec62bda..0d4d4e4 100644 --- a/src/Components/Layout/SidebarMenuComponents/SidebarFooter.jsx +++ b/src/Components/Layout/SidebarMenuComponents/SidebarFooter.jsx @@ -1,16 +1,47 @@ import React from "react"; -import { List, ListItem, ListItemText } from "@mui/material"; +import { + List, + ListItem, + ListItemText, + styled +} from "@mui/material"; -const SidebarFooter = ({ sidebarWidth }) => { +const FooterList = styled(List)(({ theme }) => ({ + backgroundColor: theme.palette.custom.sidebar, + padding: theme.spacing(1, 0), + borderTop: `1px solid ${theme.palette.divider}`, + marginTop: 'auto' +})); + +const FooterListItem = styled(ListItem)(({ theme }) => ({ + '&:hover': { + backgroundColor: theme.palette.custom.sidebarHover, + }, + padding: theme.spacing(1, 2), +})); + +const SidebarFooter = () => { return ( - - - - - - - - + + + + + + + + ); }; diff --git a/src/Components/TreeChart/menuData222.json b/src/Components/TreeChart/menuData222.json deleted file mode 100755 index e8b99e6..0000000 --- a/src/Components/TreeChart/menuData222.json +++ /dev/null @@ -1,714 +0,0 @@ -{ - "title": "Сервис ЗВКС", - "id": "1", - "items": [ - { - "title": "Функциональные задачи", - "id": "functional_tasks", - "items": [ - { - "id": "system_control", - "title": "Контроль системы" - }, - { - "id": "system_management", - "title": "Система управления" - }, - { - "id": "conference", - "title": "Проведение ВКС" - }, - { - "id": "backup", - "title": "Резервное копирование" - }, - { - "id": "relay_info", - "title": "Ретрансляция информации" - } - ] - }, - { - "id": "18", - "title": "Graviton S2082I (device$18)", - "items": [ - { - "id": "4", - "title": "OS Linux (module$4) АО", - "items": [ - { - "id": "190", - "title": "Загрузка процессора за 1 минуту" - }, - { - "id": "191", - "title": "Загрузка процессора за 5 минут" - }, - { - "id": "192", - "title": "Загрузка процессора за 15 минут" - }, - { - "id": "197", - "title": "Общий объем SWAP-файла" - }, - { - "id": "198", - "title": "Используемый объем SWAP-файла" - }, - { - "id": "199", - "title": "Общий объем физической оперативной памяти" - }, - { - "id": "200", - "title": "Доступный объем физической оперативной памяти" - }, - { - "id": "201", - "title": "Свободный объем физической и виртуальной оперативной памяти" - }, - { - "id": "202", - "title": "Буферизованный объем оперативной памяти" - }, - { - "id": "203", - "title": "Кэшированый объем оперативной памяти" - }, - { - "id": "274", - "title": "Используемый объем SWAP-файла" - }, - { - "id": "275", - "title": "Время затраченное процессором на процессы с пониженным приоритетом" - }, - { - "id": "276", - "title": "Время затраченное процессором на процессы ядра ОС" - }, - { - "id": "277", - "title": "Время простоя процессора" - }, - { - "id": "278", - "title": "Общая емкость жестких дисков" - }, - { - "id": "279", - "title": "Доступная емкость жестких дисков" - } - ] - }, - { - "id": "5", - "title": "Vinteo (module$5) ПО", - "items": [ - { - "id": "31", - "title": "Общее количество участников" - }, - { - "id": "32", - "title": "Ожидание соединения" - }, - { - "id": "33", - "title": "Зарегистрированные абоненты" - }, - { - "id": "34", - "title": "Количество пользоватей HLS" - }, - { - "id": "35", - "title": "Общее количество P2P комнат" - }, - { - "id": "36", - "title": "Общее количество конференций" - }, - { - "id": "37", - "title": "Общее количество активных конференций" - }, - { - "id": "38", - "title": "Статус записи" - }, - { - "id": "39", - "title": "Общее количество сохранённых записей" - } - ] - }, - { - "id": "280", - "title": "Сетевой адаптер №1 (port$261) Eth_1", - "items": [ - { - "id": "207", - "title": "Скорость порта Eth_1" - }, - { - "id": "209", - "title": "Административное состояние порта Eth_1" - }, - { - "id": "210", - "title": "Оперативное состояние порта Eth_1" - }, - { - "id": "211", - "title": "Общее количество отправленных октетов Eth_1" - }, - { - "id": "212", - "title": "Количество входящих Multicast пакетов Eth_1" - }, - { - "id": "213", - "title": "Количество иcходящих Multiicast пакетов Eth_1" - }, - { - "id": "214", - "title": "Количество входящих Broadcast пакетов Eth_1" - }, - { - "id": "215", - "title": "Количество иcходящих Broadcast пакетов Eth_1" - }, - { - "id": "216", - "title": "Количество входящих Unicast пакетов Eth_1" - }, - { - "id": "217", - "title": "Количество иcходящих Unicast пакетов Eth_1" - }, - { - "id": "218", - "title": "Количество входящих пакетов помеченные как отброшенные Eth_1" - }, - { - "id": "219", - "title": "Количество иcходящих пакетов помеченные как отброшенные Eth_1" - }, - { - "id": "220", - "title": "Количество входящих пакетов с ошибкой Eth_1" - }, - { - "id": "221", - "title": "Количество исходящих пакетов с ошибкой Eth_1" - }, - { - "id": "222", - "title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_1" - } - ] - }, - { - "id": "281", - "title": "Сетевой адаптер №2 (port$262) Eth_2", - "items": [ - { - "id": "224", - "title": "Скорость порта Eth_2" - }, - { - "id": "226", - "title": "Административное состояние порта Eth_2" - }, - { - "id": "227", - "title": "Оперативное состояние порта Eth_2" - }, - { - "id": "228", - "title": "Общее количество отправленных октетов Eth_2" - }, - { - "id": "229", - "title": "Количество входящих Multicast пакетов Eth_2" - }, - { - "id": "230", - "title": "Количество иcходящих Multiicast пакетов Eth_2" - }, - { - "id": "231", - "title": "Количество входящих Broadcast пакетов Eth_2" - }, - { - "id": "232", - "title": "Количество иcходящих Broadcast пакетов Eth_2" - }, - { - "id": "233", - "title": "Количество входящих Unicast пакетов Eth_2" - }, - { - "id": "234", - "title": "Количество иcходящих Unicast пакетов Eth_2" - }, - { - "id": "235", - "title": "Количество входящих пакетов помеченные как отброшенные Eth_2" - }, - { - "id": "236", - "title": "Количество иcходящих пакетов помеченные как отброшенные Eth_2" - }, - { - "id": "237", - "title": "Количество входящих пакетов с ошибкой Eth_2" - }, - { - "id": "238", - "title": "Количество исходящих пакетов с ошибкой Eth_2" - }, - { - "id": "239", - "title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_2" - } - ] - }, - { - "id": "282", - "title": "Сетевой адаптер №3 (port$263) Eth_3", - "items": [ - { - "id": "241", - "title": "Скорость порта Eth_3" - }, - { - "id": "243", - "title": "Административное состояние порта Eth_3" - }, - { - "id": "244", - "title": "Оперативное состояние порта Eth_3" - }, - { - "id": "245", - "title": "Общее количество отправленных октетов Eth_3" - }, - { - "id": "246", - "title": "Количество входящих Multicast пакетов Eth_3" - }, - { - "id": "247", - "title": "Количество иcходящих Multiicast пакетов Eth_3" - }, - { - "id": "248", - "title": "Количество входящих Broadcast пакетов Eth_3" - }, - { - "id": "249", - "title": "Количество иcходящих Broadcast пакетов Eth_3" - }, - { - "id": "250", - "title": "Количество входящих Unicast пакетов Eth_3" - }, - { - "id": "251", - "title": "Количество иcходящих Unicast пакетов Eth_3" - }, - { - "id": "252", - "title": "Количество входящих пакетов помеченные как отброшенные Eth_3" - }, - { - "id": "253", - "title": "Количество иcходящих пакетов помеченные как отброшенные Eth_3" - }, - { - "id": "254", - "title": "Количество входящих пакетов с ошибкой Eth_3" - }, - { - "id": "255", - "title": "Количество исходящих пакетов с ошибкой Eth_3" - }, - { - "id": "256", - "title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_3" - } - ] - }, - { - "id": "283", - "title": "Сетевой адаптер №4 (port$264) Eth_4", - "items": [ - { - "id": "258", - "title": "Скорость порта Eth_4" - }, - { - "id": "260", - "title": "Административное состояние порта Eth_4" - }, - { - "id": "261", - "title": "Оперативное состояние порта Eth_4" - }, - { - "id": "262", - "title": "Общее количество отправленных октетов Eth_4" - }, - { - "id": "263", - "title": "Количество входящих Multicast пакетов Eth_4" - }, - { - "id": "264", - "title": "Количество иcходящих Multiicast пакетов Eth_4" - }, - { - "id": "265", - "title": "Количество входящих Broadcast пакетов Eth_4" - }, - { - "id": "266", - "title": "Количество иcходящих Broadcast пакетов Eth_4" - }, - { - "id": "267", - "title": "Количество входящих Unicast пакетов Eth_4" - }, - { - "id": "268", - "title": "Количество иcходящих Unicast пакетов Eth_4" - }, - { - "id": "269", - "title": "Количество входящих пакетов помеченные как отброшенные Eth_4" - }, - { - "id": "270", - "title": "Количество иcходящих пакетов помеченные как отброшенные Eth_4" - }, - { - "id": "271", - "title": "Количество входящих пакетов с ошибкой Eth_4" - }, - { - "id": "272", - "title": "Количество исходящих пакетов с ошибкой Eth_4" - }, - { - "id": "273", - "title": "Количество входящих пакетов с неизвестным или неподдерживаемым протоколом Eth_4" - } - ] - } - ] - }, - { - "title": "Медиа сервер", - "id": "media_server_1", - "items": [ - { - "title": "Аппаратное обеспечение", - "id": "system_software_1", - "items": [ - { - "id": "media_system_software_1_2", - "title": "Центральный процессор" - }, - { - "id": "media_system_software_2_2", - "title": "Оперативная память" - }, - { - "id": "media_system_software_3_2", - "title": "Жесткий диск" - }, - { - "id": "media_system_software_4_2", - "title": "Сетевые адаптеры" - } - ] - }, - { - "title": "Программное обеспечение", - "id": "software_1", - "items": [ - { - "id": "media_software_1_2", - "title": "ПО" - }, - { - "id": "media_software_2_2", - "title": "ПО" - }, - { - "id": "media_software_3_2", - "title": "ПО" - }, - { - "id": "media_software_4_2", - "title": "ПО" - } - ] - } - ] - }, - { - "title": "Медиа сервер", - "id": "media_server_2", - "items": [ - { - "title": "Аппаратное обеспечение", - "id": "system_software_2", - "items": [ - { - "id": "media_system_software_1_3", - "title": "Центральный процессор" - }, - { - "id": "media_system_software_2_3", - "title": "Оперативная память" - }, - { - "id": "media_system_software_3_3", - "title": "Жесткий диск" - }, - { - "id": "media_system_software_4_3", - "title": "Сетевые адаптеры" - } - ] - }, - { - "title": "Программное обеспечение", - "id": "software_2", - "items": [ - { - "id": "media_software_1_3", - "title": "ПО" - }, - { - "id": "media_software_2_3", - "title": "ПО" - }, - { - "id": "media_software_3_3", - "title": "ПО" - }, - { - "id": "media_software_4_3", - "title": "ПО" - } - ] - } - ] - }, - { - "title": "Медиа сервер", - "id": "media_server_3", - "items": [ - { - "title": "Аппаратное обеспечение", - "id": "system_software_3", - "items": [ - { - "id": "media_system_software_1_4", - "title": "Центральный процессор" - }, - { - "id": "media_system_software_2_4", - "title": "Оперативная память" - }, - { - "id": "media_system_software_3_4", - "title": "Жесткий диск" - }, - { - "id": "media_system_software_4_4", - "title": "Сетевые адаптеры" - } - ] - }, - { - "title": "Программное обеспечение", - "id": "software_3", - "items": [ - { - "id": "media_software_1_4", - "title": "ПО" - }, - { - "id": "media_software_2_4", - "title": "ПО" - }, - { - "id": "media_software_3_4", - "title": "ПО" - }, - { - "id": "media_software_4_4", - "title": "ПО" - } - ] - } - ] - }, - { - "title": "Медиа сервер", - "id": "media_server_4", - "items": [ - { - "title": "Аппаратное обеспечение", - "id": "system_software_4", - "items": [ - { - "id": "media_system_software_1_5", - "title": "Центральный процессор" - }, - { - "id": "media_system_software_2_5", - "title": "Оперативная память" - }, - { - "id": "media_system_software_3_5", - "title": "Жесткий диск" - }, - { - "id": "media_system_software_4_5", - "title": "Сетевые адаптеры" - } - ] - }, - { - "title": "Программное обеспечение", - "id": "software_4", - "items": [ - { - "id": "media_software_1_5", - "title": "ПО" - }, - { - "id": "media_software_2_5", - "title": "ПО" - }, - { - "id": "media_software_3_5", - "title": "ПО" - }, - { - "id": "media_software_4_5", - "title": "ПО" - } - ] - } - ] - }, - { - "title": "Сервер систем", - "id": "system_server_1", - "items": [ - { - "title": "Аппаратное обеспечение", - "id": "system_software_5", - "items": [ - { - "id": "copy_system_software_1", - "title": "Центральный процессор" - }, - { - "id": "copy_system_software_2", - "title": "Оперативная память" - }, - { - "id": "copy_system_software_3", - "title": "Жесткий диск" - }, - { - "id": "copy_system_software_4", - "title": "Сетевые адаптеры" - } - ] - }, - { - "title": "Программное обеспечение", - "id": "software_5", - "items": [ - { - "id": "copy_software_1", - "title": "ПО" - }, - { - "id": "copy_software_2", - "title": "ПО" - }, - { - "id": "copy_software_3", - "title": "ПО" - }, - { - "id": "copy_software_4", - "title": "ПО" - } - ] - } - ] - }, - { - "title": "Сервер систем", - "id": "system_server_2", - "items": [ - { - "title": "Аппаратное обеспечение", - "id": "system_software_6", - "items": [ - { - "id": "control_system_software_1", - "title": "Центральный процессор" - }, - { - "id": "control_system_software_2", - "title": "Оперативная память" - }, - { - "id": "control_system_software_3", - "title": "Жесткий диск" - }, - { - "id": "control_system_software_4", - "title": "Сетевые адаптеры" - } - ] - }, - { - "title": "Программное обеспечение", - "id": "software_6", - "items": [ - { - "id": "control_software_1", - "title": "ПО" - }, - { - "id": "control_software_2", - "title": "ПО" - }, - { - "id": "control_software_3", - "title": "ПО" - }, - { - "id": "control_software_4", - "title": "ПО" - } - ] - } - ] - } - ] -} \ No newline at end of file diff --git a/src/Components/TreeChart/tabContent.jsx b/src/Components/TreeChart/tabContent.jsx index 58e6e49..4072401 100755 --- a/src/Components/TreeChart/tabContent.jsx +++ b/src/Components/TreeChart/tabContent.jsx @@ -1,14 +1,13 @@ import React, { lazy, Suspense } from "react"; const PrometheusChart = lazy(() => import('../../Charts/PrometheusChart')); +import LazyChartBatchRenderer from "../hooks/LazyChartBatchRender"; // Функция для генерации названия метрики на основе id const getMetricName = (id) => { return `zvks_apiforsnmp_measure_${id}`; }; -//!!!!!!!!!!Пофиксить вкладуи с eth4, во всех eth 1-4 открывается именно 4 !!!!!!!!!!!!! - // Функция для рекурсивного сбора всех id потомков const getAllChildIds = (node) => { let ids = []; @@ -37,6 +36,7 @@ const tabContent = (data) => { const content = (

{node.title}

+ tabContent[child.id].content)} />

Контент для {node.title}.

{childrenContent}
diff --git a/src/Components/UI/Button.jsx b/src/Components/UI/Button.jsx new file mode 100644 index 0000000..378f5dd --- /dev/null +++ b/src/Components/UI/Button.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import Button from '@mui/material/Button'; +import { styled } from '@mui/material/styles'; +import CircularProgress from '@mui/material/CircularProgress'; + +const StyledButton = styled(Button)(({ theme }) => ({ + margin: theme.spacing(1), + // Дополнительные стили +})); + +const CustomButton = ({ + children, + variant = 'contained', + color = 'primary', + loading = false, + startIcon, + endIcon, + ...props +}) => { + return ( + + {loading ? : children} + + ); +}; + +export default CustomButton; \ No newline at end of file diff --git a/src/Components/UI/ErrorIndicator.jsx b/src/Components/UI/ErrorIndicator.jsx deleted file mode 100755 index 7215211..0000000 --- a/src/Components/UI/ErrorIndicator.jsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from "react"; -import criticalIcon from "../../assets/images/critical.png"; // Красный треугольник -import warningIcon from "../../assets/images/warning.png"; // Желтый треугольник -import "../../Style/ErrorIndicator.css"; // Подключаем стили - -const ErrorIndicator = ({ criticalCount, warningCount }) => { - return ( -
- {/* Красный индикатор (критические ошибки) */} -
- Критическая ошибка - {criticalCount} -
- - {/* Желтый индикатор (предупреждения) */} -
- Предупреждение - {warningCount} -
-
- ); -}; - -export default ErrorIndicator; \ No newline at end of file diff --git a/src/Components/UI/ExpandableInfo.jsx b/src/Components/UI/ExpandableInfo.jsx deleted file mode 100755 index 1c550c0..0000000 --- a/src/Components/UI/ExpandableInfo.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import React, { useState } from "react"; -import "../Style/Expandable.css" - -const ExpandableInfo = ({ details }) => { - const [isExpanded, setIsExpanded] = useState(false); - - const toggleExpand = () => { - setIsExpanded(!isExpanded); - }; - - return ( -
- - {isExpanded && ( -
- {details.map((detail, index) => ( -
- {detail.label}: - {detail.value} -
- ))} -
- )} -
- ); -}; - -export default ExpandableInfo; \ No newline at end of file diff --git a/src/Components/UI/LoginModal.jsx b/src/Components/UI/LoginModal.jsx index ada6823..3e29de2 100755 --- a/src/Components/UI/LoginModal.jsx +++ b/src/Components/UI/LoginModal.jsx @@ -17,7 +17,7 @@ const LoginModal = ({ onLogin, onClose }) => { try { // Отправляем данные на бэкенд - const response = await fetch(`${import.meta.env.VITE_BACK_URL}/auth/login`, { + const response = await fetch(`${import.meta.env.VITE_BACK_URL}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/Components/UI/MUItabs.jsx b/src/Components/UI/MUItabs.jsx index 06ef06f..0ea89aa 100644 --- a/src/Components/UI/MUItabs.jsx +++ b/src/Components/UI/MUItabs.jsx @@ -1,10 +1,22 @@ import React from "react"; -import { Tabs, Tab, Box } from "@mui/material"; +import { Tabs, Tab, Box, styled } from "@mui/material"; import CloseIcon from "@mui/icons-material/Close"; +const StyledTab = styled(Tab)(({ theme }) => ({ + minHeight: 48, + '&.Mui-selected': { + color: theme.palette.primary.main, + fontWeight: theme.typography.fontWeightMedium, + }, + '&:focus-visible': { + outline: `2px solid ${theme.palette.primary.main}`, + outlineOffset: '-2px', + }, +})); + const CustomTabs = ({ tabs, activeTab, onTabClick, onCloseTab }) => { const handleMouseDown = (e, id) => { - if (e.button === 1) { + if (e.button === 1) { // Middle mouse button e.preventDefault(); onCloseTab(id); } @@ -15,7 +27,13 @@ const CustomTabs = ({ tabs, activeTab, onTabClick, onCloseTab }) => { }; return ( - + { scrollButtons="auto" aria-label="tabs" > - {/* Всегда отображаемые вкладки */} - handleMouseDown(e, "Главная")} /> - handleMouseDown(e, "Визуализация")} /> - {/* Динамически добавляемые вкладки */} + {/* Динамические вкладки */} {tabs.map((tab) => ( - {tab.title} { e.stopPropagation(); onCloseTab(tab.id); diff --git a/src/Components/UI/Tabs.jsx b/src/Components/UI/Tabs.jsx deleted file mode 100755 index 78ec099..0000000 --- a/src/Components/UI/Tabs.jsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from "react"; -import "../../Style/common.css"; // Общие стили для табов - -const Tabs = ({ tabs, activeTab, onTabClick, onCloseTab }) => { - const handleMouseDown = (e, id) => { - // Проверяем, была ли нажата средняя кнопка мыши (button === 1) - if (e.button === 1) { - e.preventDefault(); // Предотвращаем стандартное поведение (например, прокрутку) - onCloseTab(id); // Закрываем вкладку - } - }; - - - return ( -
- {/* Всегда отображаемые вкладки */} -
onTabClick("Главная")} - onMouseDown={(e) => handleMouseDown(e, "Главная")} // Добавляем обработчик для СКМ - > - Главная -
-
onTabClick("Визуализация")} - onMouseDown={(e) => handleMouseDown(e, "Визуализация")} // Добавляем обработчик для СКМ - > - Визуализация -
- - {/* Динамически добавляемые вкладки */} - {tabs.map((tab) => ( -
onTabClick(tab.id)} - onMouseDown={(e) => handleMouseDown(e, tab.id)} // Добавляем обработчик для СКМ - > - {tab.title} - -
- ))} -
- ); -}; - -export default Tabs; \ No newline at end of file diff --git a/src/Components/UI/TreeTable.jsx b/src/Components/UI/TreeTable.jsx index ab856cc..bbc86b1 100755 --- a/src/Components/UI/TreeTable.jsx +++ b/src/Components/UI/TreeTable.jsx @@ -1,12 +1,27 @@ import React, { useEffect, useRef, useState } from "react"; -import "../../Style/TreeTable.css"; +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Button, + Collapse, + Box, + Typography, + useTheme, + Tooltip +} from '@mui/material'; import { statusManager1, statusManager2 } from "../TreeChart/dataUtils"; const TreeTable = ({ data }) => { + const theme = useTheme(); const tableRef = useRef(null); const [fontSize, setFontSize] = useState(16); const [log, setLog] = useState([]); - const [isLogVisible, setIsLogVisible] = useState(true); + const [isLogVisible, setIsLogVisible] = useState(false); const adjustFontSize = () => { if (tableRef.current) { @@ -27,6 +42,13 @@ const TreeTable = ({ data }) => { } }; + useEffect(() => { + adjustFontSize(); + window.addEventListener('resize', adjustFontSize); + return () => window.removeEventListener('resize', adjustFontSize); + }, [data]); + + // Логирование статусов useEffect(() => { const newLog = []; const traverse = (items) => { @@ -35,7 +57,7 @@ const TreeTable = ({ data }) => { newLog.push({ title: item.title, status: item.status, - time: new Date().toLocaleTimeString(), // Добавляем время + time: new Date().toLocaleTimeString(), }); } if (item.items) { @@ -44,204 +66,285 @@ const TreeTable = ({ data }) => { }); }; traverse(data.items); - - // Ограничиваем количество сообщений до 50 - setLog((prevLog) => [...newLog, ...prevLog].slice(0, 50)); + setLog(prevLog => [...newLog, ...prevLog].slice(0, 50)); }, [data]); - const filteredData = data.items.filter((item) => item.title !== "Функциональные задачи"); + const filteredData = data.items.filter(item => item.title !== "Функциональные задачи"); - // Функция для отображения заголовков - const renderHeaders = (items) => { + // Компонент индикаторов статуса + const StatusIndicators = ({ status }) => ( + <> + + + + ); + + // Ячейка с тултипом + const TableCellWithTooltip = ({ children, title, ...props }) => ( + + + {children} + + + ); + + // Рендер заголовков (первый уровень) + const renderMainHeaders = (items) => { return items.map((item) => { const colSpan = item.items ? item.items.length : 1; return ( - -
-
-
+ + + {item.title} -
- + + ); }); }; - // Функция для отображения подзаголовков + // Рендер подзаголовков (второй уровень) const renderSubHeaders = (items) => { - return items.map((item) => { + return items.flatMap((item) => { if (item.items) { return item.items.map((child) => ( - -
-
-
+ + + {child.title} -
- + + )); - } else { - return ( - -
-
-
- {item.title} -
- - ); } + return ( + + + + {item.title} + + + ); }); }; - // Функция для отображения данных - const renderData = (items) => { - return items.map((item) => { + // Рендер данных (третий уровень) + const renderDataCells = (items) => { + return items.flatMap((item) => { if (item.items) { - return item.items.map((child) => { + return item.items.flatMap((child) => { if (child.items) { return child.items.map((subChild) => ( - -
-
-
- {subChild.title} -
- - )); - } else { - return ( - -
-
-
- {child.title} -
- - ); - } - }); - } else { - return ( - -
-
-
- {item.title} -
- - ); + > + + + {subChild.title} + + + )); + } + return ( + + + + {child.title} + + + ); + }); } + return ( + + + + {item.title} + + + ); }); }; return ( -
- - - - - - {renderHeaders(filteredData)} - {renderSubHeaders(filteredData)} - - - {renderData(filteredData)} - -
acc + (item.items ? item.items.length : 1), 0)} - className="tree-table-header" - title={data.title} - > -
-
-
- {data.title} -
-
- - {isLogVisible && ( -
-

Лог статусов

-
    + + + + + {/* Основной заголовок таблицы */} + + acc + (item.items ? item.items.length : 1), 0)} + align="center" + title={data.title} + sx={{ + backgroundColor: theme.palette.background.paper, + border: `1px solid ${theme.palette.divider}`, + padding: '8px' + }} + > + + + {data.title} + + + + + {/* Строка с основными заголовками */} + + {renderMainHeaders(filteredData)} + + + {/* Строка с подзаголовками (которая пропала в предыдущей версии) */} + + {renderSubHeaders(filteredData)} + + + + + + {renderDataCells(filteredData)} + + +
    +
    + + + + + + + История изменения статусов + + {log.map((entry, index) => ( -
  • + [{entry.time}] {entry.status}: {entry.title} -
  • +
    ))} -
-
- )} -
+ + + + ); }; diff --git a/src/Components/hooks/LazyChartBatchRender.jsx b/src/Components/hooks/LazyChartBatchRender.jsx new file mode 100644 index 0000000..a12f491 --- /dev/null +++ b/src/Components/hooks/LazyChartBatchRender.jsx @@ -0,0 +1,29 @@ +import { useEffect, useState } from "react"; + +const LazyChartBatchRenderer = ({ charts, batchSize = 3, delay = 150 }) => { + const [visibleCharts, setVisibleCharts] = useState([]); + + useEffect(() => { + let index = 0; + const timer = setInterval(() => { + setVisibleCharts((prev) => [ + ...prev, + ...charts.slice(index, index + batchSize), + ]); + index += batchSize; + if (index >= charts.length) clearInterval(timer); + }, delay); + + return () => clearInterval(timer); + }, [charts]); + + return ( + <> + {visibleCharts.map((chart, idx) => ( +
{chart}
+ ))} + + ); +}; + +export default LazyChartBatchRenderer; diff --git a/src/Components/hooks/TabContent.jsx b/src/Components/hooks/TabContent.jsx index 10e58a0..bdb6226 100644 --- a/src/Components/hooks/TabContent.jsx +++ b/src/Components/hooks/TabContent.jsx @@ -1,6 +1,5 @@ import SystemStatusChart from "../../Charts/SystemStatusChart"; import TreeTable from "../UI/TreeTable"; - import FlowChart from "../TreeChart/FlowChart"; const TabContent = ({ activeTab, statusHistories, treeData1, tabContent, handleOpenTab }) => { diff --git a/src/Style/Dashboard.css b/src/Style/Dashboard.css deleted file mode 100755 index 4f9eb18..0000000 --- a/src/Style/Dashboard.css +++ /dev/null @@ -1,52 +0,0 @@ -/* Основной контейнер */ -.dashboard-container { - display: flex; - height: 100vh; - width: 100vw; - overflow: hidden; - background-color: var(--background-color); - color: var(--text-color); -} - -/* Сайдбар */ -.sidebar { - flex-shrink: 0; - height: 100vh; - overflow-y: auto; - background-color: var(--sidebar-color); - color: var(--sidebar-text-color); - transition: width 0.2s ease; -} - -/* Основной контент */ -.main-content { - flex-grow: 1; - display: flex; - flex-direction: column; - padding: 20px; - overflow: auto; - background-color: var(--background-color); - color: var(--text-color); -} - - -/* Контент */ -.content { - background-color: var(--modal-background); - padding: 20px; - border-radius: 10px; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.521); - max-width: 100%; - overflow: auto; - color: var(--text-color); -} - -/* Заголовки */ -h2 { - color: var(--text-color); - text-align: center; -} - -p { - color: var(--text-color); -} \ No newline at end of file diff --git a/src/Style/ErrorIndicator.css b/src/Style/ErrorIndicator.css deleted file mode 100755 index 115f992..0000000 --- a/src/Style/ErrorIndicator.css +++ /dev/null @@ -1,38 +0,0 @@ -.error-indicator { - display: flex; - align-items: center; - gap: 15px; - padding-bottom: 20px; -} - -.error-item { - display: flex; - align-items: center; - gap: 5px; -} - -.error-item img { - width: 30px; - height: 30px; -} - -.error-item span { - font-size: 18px; - font-weight: bold; -} - -.critical span { - color: red; -} - -.warning span { - color: orange; -} - -.indicator-container { - display: flex; - align-items: center; - gap: 15px; - justify-content: center; - -} \ No newline at end of file diff --git a/src/Style/Expandable.css b/src/Style/Expandable.css deleted file mode 100755 index 6550184..0000000 --- a/src/Style/Expandable.css +++ /dev/null @@ -1,39 +0,0 @@ -.expandable-info { - margin-top: 10px; -} - -.expand-button { - background-color: #444; - color: white; - border: none; - padding: 8px 16px; - cursor: pointer; - border-radius: 4px; -} - -.expand-button:hover { - background-color: #333; -} - -.details-menu { - margin-top: 10px; - padding: 10px; - border: 1px solid #333; - border-radius: 4px; - background-color: white; -} - -.detail-item { - display: flex; - justify-content: space-between; - margin-bottom: 5px; -} - -.label { - font-weight: bold; - color: #333 -} - -.value { - color: #333; -} \ No newline at end of file diff --git a/src/Style/SidebarMenu.css b/src/Style/SidebarMenu.css deleted file mode 100755 index d4670ec..0000000 --- a/src/Style/SidebarMenu.css +++ /dev/null @@ -1,143 +0,0 @@ -/* Сайдбар */ -.sidebar { - height: 100vh; - background-color: var(--sidebar-color); - color: var(--sidebar-text-color); - position: fixed; - left: 0; - top: 0; - z-index: 999; - overflow: hidden; - transition: width 0.2s ease; - display: flex; - flex-direction: column; -} - -/* Контейнер для основного контента меню */ -.sidebar-content { - flex: 1; - overflow-y: auto; - overflow-x: hidden; - padding-bottom: 20px; - padding-right: 10px; - /* Отступ справа для скроллбара */ -} - -/* Заголовок меню */ -.sidebar-title { - margin-bottom: 20px; - font-size: 1.5em; - font-weight: bold; - color: var(--sidebar-text-color); - padding: 10px; - text-align: center; - /* font-size: 2vh; */ -} - -/* Элементы меню */ -.menu-item { - margin-bottom: 10px; - color: var(--sidebar-text-color); - width: 100%; -} - -/* Элемент для перетаскивания */ -.sidebar-resizer { - width: 5px; - height: 100%; - background-color: rgba(255, 255, 255, 0.1); - position: absolute; - right: 0; - top: 0; - cursor: ew-resize; - transition: background-color 0.2s ease; - z-index: 1000; -} - -.sidebar-resizer:hover { - background-color: rgba(255, 255, 255, 0.3); -} - -/* Стили для заголовка элемента меню */ -.menu-item-header { - display: flex; - align-items: center; - justify-content: space-between; - /* Распределяем пространство между элементами */ - padding: 10px; - border-radius: 5px; - cursor: pointer; - transition: background-color 0.3s ease; - width: 100%; - /* Занимаем всю доступную ширину */ - box-sizing: border-box; - /* Учитываем padding в ширине */ -} - -/* Стили для текста элемента меню */ -.menu-item-header span { - flex: 1; - /* Текст занимает все доступное пространство */ - margin-right: 14px; - /* Отступ справа для текста */ - overflow: hidden; - /* Скрываем текст, который не помещается */ - text-overflow: ellipsis; - /* Добавляем многоточие, если текст не помещается */ -} - -/* Стили для иконок */ -.menu-item-header .open-parent-icon, -.menu-item-header .toggle-icon { - flex-shrink: 0; - /* Запрещаем сжатие иконок */ - margin-left: 1px; - /* Отступ между иконками */ - cursor: pointer; -} - -.menu-item-header:hover { - background-color: rgba(255, 255, 255, 0.1); -} - -/* Круглый индикатор статуса */ -.status-indicator { - width: 10px; - height: 10px; - border-radius: 50%; - margin-right: 10px; - flex-shrink: 0; -} - -/* Подменю */ -.submenu { - margin-left: 20px; - /* Отступ слева для вложенных элементов */ - margin-top: 10px; -} - -/* Стили для элементов нижнего уровня вложенности */ - -/* Дополнительные отступы для элементов без иконок */ -.menu-item:not(.has-children) .menu-item-header { - padding-right: 25px; - /* Добавляем отступ справа для элементов без иконок */ -} - -/* Футер сайдбара */ -.sidebar-footer { - padding: 10px; - background-color: var(--sidebar-color); - text-align: center; - border-top: 1px solid rgba(255, 255, 255, 0.1); - flex-shrink: 0; - width: 100%; -} - -.help, -.settings { - color: var(--sidebar-text-color); - margin: 5px 0; - overflow-x: hidden; - text-align: left; -} \ No newline at end of file diff --git a/src/Style/TreeTable.css b/src/Style/TreeTable.css deleted file mode 100755 index d31dad5..0000000 --- a/src/Style/TreeTable.css +++ /dev/null @@ -1,62 +0,0 @@ -.tree-table-container { - width: 100%; - overflow-x: hidden; - /* Убираем горизонтальный скролл */ -} - -.tree-table { - width: 100%; - border-collapse: collapse; - text-align: center; - table-layout: fixed; - /* Фиксированная ширина колонок */ - background-color: var(--table-cell-background); - color: var(--table-text-color); -} - -.tree-table-header { - padding: 10px; - border: 1px solid black; - font-weight: bold; - white-space: nowrap; - /* Текст не переносится */ - overflow: hidden; - /* Скрываем текст, который не помещается */ - text-overflow: ellipsis; - /* Добавляем многоточие */ - background-color: var(--table-header-background); -} - -.tree-table-cell { - padding: 8px; - border: 1px solid black; - white-space: nowrap; - /* Текст не переносится */ - overflow: hidden; - /* Скрываем текст, который не помещается */ - text-overflow: ellipsis; - /* Добавляем многоточие */ -} - -.cell-content, -.header-content { - display: flex; - align-items: center; - gap: 2px; - width: 100%; - overflow: hidden; - text-overflow: ellipsis; -} - -.cell-text { - flex: 1; - overflow: hidden; - text-overflow: ellipsis; -} - -.status-indicator-bar { - width: 6px; - height: 20px; - border-radius: 3px; - flex-shrink: 0; -} \ No newline at end of file diff --git a/src/Style/common.css b/src/Style/common.css deleted file mode 100755 index d0184b1..0000000 --- a/src/Style/common.css +++ /dev/null @@ -1,53 +0,0 @@ -/* Контейнер для вкладок */ -.tabs { - display: flex; - gap: 5px; - padding: 5px; - background-color: var(--sidebar-color); - border-bottom: 2px solid var(--accent-color); - overflow-x: auto; - border-radius: 5px; - white-space: nowrap; -} - -/* Стили для отдельной вкладки */ -.tab { - display: flex; - align-items: center; - background-color: var(--sidebar-color); - color: var(--sidebar-text-color); - /* Используем переменную для цвета текста */ - padding: 5px 15px; - border-radius: 5px 5px 0 0; - cursor: pointer; - flex-shrink: 0; - transition: background-color 0.3s ease; -} - -/* Активная вкладка */ -.tab.active { - background-color: var(--accent-color); -} - -/* Кнопка закрытия вкладки */ -.close-tab { - background: none; - border: none; - color: var(--sidebar-text-color); - /* Используем переменную для цвета текста */ - cursor: pointer; - font-size: 16px; - margin-left: 10px; - padding: 0; - transition: color 0.3s ease; -} - -/* Эффект при наведении на кнопку закрытия */ -.close-tab:hover { - color: #ff6b6b; -} - -/* Эффект при наведении на вкладку */ -.tab:hover { - background-color: var(--accent-hover-color); -} \ No newline at end of file diff --git a/src/Style/dark-theme.css b/src/Style/dark-theme.css deleted file mode 100644 index fc21f59..0000000 --- a/src/Style/dark-theme.css +++ /dev/null @@ -1,25 +0,0 @@ -/* Темная тема, если пользователь предпочитает ее */ -@media (prefers-color-scheme: dark) { - :root { - --background-color: #1E1E1E; - --text-color: #E0E0E0; - --header-color: #FFFFFF; - /* Основной цвет текста (светлый) */ - --sidebar-color: #2d2d2d; - /* Темный цвет сайдбара */ - --sidebar-text-color: #E0E0E0; - /* Светлый текст в сайдбаре */ - --modal-background: #2d2d2d; - --modal--btn-background: #333333; - --modal-text: #FFFFFF; - --table-border: #c70a0a; - --table-header-background: #2d2d2d; - --table-cell-background: #333333; - --table-text-color: #E0E0E0; - /* Светлый текст в таблице */ - --TreeChart-text-color: #ffffff; - --scrollbar-track-color: #333; - /* hover for buttons */ - --hover-button: #333d4d; - } -} \ No newline at end of file diff --git a/src/Style/light-theme.css b/src/Style/light-theme.css deleted file mode 100644 index b4f6d03..0000000 --- a/src/Style/light-theme.css +++ /dev/null @@ -1,23 +0,0 @@ -/* Светлая тема по умолчанию */ -:root { - --background-color: #FFFFFF; - --text-color: #000000; - --header-color: #333333; - /* Основной цвет текста (черный) */ - --sidebar-color: #3d74c7; - /* Синий цвет сайдбара */ - --sidebar-text-color: #FFFFFF; - /* Белый текст в сайдбаре и вкладках */ - --modal-background: #FFFFFF; - --modal--btn-background: #0f55bec2; - --modal-text: #333333; - --table-border: #ddd; - --table-header-background: #f9f9f9; - --table-cell-background: #FFFFFF; - --table-text-color: #000000; - /* Черный текст в таблице */ - - /* hover for buttons */ - --hover-button: #2d62b1; - --hover-text-color: #FFFFFF -} \ No newline at end of file diff --git a/src/Style/range-selector.css b/src/Style/range-selector.css deleted file mode 100644 index e69de29..0000000 diff --git a/src/Style/theme.jsx b/src/Style/theme.jsx index 02b38ed..40c405d 100644 --- a/src/Style/theme.jsx +++ b/src/Style/theme.jsx @@ -1,73 +1,191 @@ import { createTheme } from "@mui/material/styles"; +/** + * Общие настройки темы, применяемые для обеих тем (светлой и темной) + */ +const commonThemeSettings = { + // Настройки формы элементов + shape: { + borderRadius: 8, // Базовый радиус скругления углов для всех компонентов + }, + + // Переопределения стилей конкретных MUI компонентов + components: { + // Стили для компонента Drawer (боковое меню) + MuiDrawer: { + styleOverrides: { + paper: { + borderRight: 'none', // Убираем правую границу у бокового меню + } + }, + MuiTab: { + styleOverrides: { + root: { + textTransform: 'none', // Убираем uppercase + minWidth: 'unset', // Убираем минимальную ширину + padding: '6px 16px', + '&:hover': { + color: 'primary.main', + opacity: 1, + }, + '&.Mui-selected': { + color: 'primary.main', + }, + '&.Mui-focusVisible': { + backgroundColor: 'action.selected', + }, + }, + }, + }, + MuiTabs: { + styleOverrides: { + indicator: { + height: 3, // Толщина индикатора + }, + }, + }, + }, + + // Стили для кнопок-элементов списка + MuiListItemButton: { + styleOverrides: { + root: { + // Стиль для выбранного элемента + '&.Mui-selected': { + backgroundColor: 'rgba(255, 255, 255, 0.16)', + }, + // Стиль при наведении на выбранный элемент + '&.Mui-selected:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.24)', + }, + } + } + } + } +}; + +/** + * Светлая тема приложения + */ export const lightTheme = createTheme({ + ...commonThemeSettings, // Распаковываем общие настройки + + // Цветовая палитра для светлой темы palette: { - mode: "light", + mode: "light", // Режим светлой темы + + // Фоновые цвета background: { - default: "#FFFFFF", - paper: "#FFFFFF", + default: "#6CACE4", // Основной фон приложения + paper: "#FFFFFF", // Фон "бумажных" поверхностей (карточек, панелей) }, + + // Текстовые цвета text: { - primary: "#000000", + primary: "#000000", // Основной цвет текста + secondary: "#333333", // Вторичный цвет текста }, + + // Основные цвета UI primary: { - main: "#3d74c7", + main: "#3d74c7", // Основной брендовый цвет + contrastText: "#FFFFFF", // Цвет текста на кнопках primary цвета }, + + // Дополнительные цвета UI secondary: { - main: "#0f55bec2", + main: "#0f55bec2", // Вторичный брендовый цвет }, + + divider: "#e0e0e0", // Цвет разделителей + + // Кастомные цвета для специфических элементов custom: { - background: "#FFFFFF", - text: "#000000", - sidebar: "#3d74c7", - sidebarText: "#FFFFFF", - modalBackground: "#FFFFFF", - modalBtnBackground: "#0f55bec2", - modalText: "#333333", - tableBorder: "#ddd", - tableHeaderBackground: "#f9f9f9", - tableCellBackground: "#FFFFFF", - tableText: "#000000", - treeChartText: "#000000", - scrollbarTrack: "#f1f1f1", - hoverButton: "#2d62b1", - hoverText: "#FFFFFF", + background: "#D4EFFC", // Кастомный фоновый цвет + text: "#000000", // Кастомный цвет текста + sidebar: "#025EA1", // Фон боковой панели + sidebarText: "#FFFFFF", // Текст в боковой панели + sidebarHover: "rgba(255, 255, 255, 0.08)", // Цвет при наведении в боковой панели + modalBackground: "#FFFFFF", // Фон модальных окон + modalBtnBackground: "#0f55bec2", // Фон кнопок в модальных окнах + modalText: "#333333", // Текст в модальных окнах + tableBorder: "#ddd", // Границы таблиц + tableHeaderBackground: "#f9f9f9", // Фон заголовков таблиц + tableCellBackground: "#FFFFFF", // Фон ячеек таблиц + tableText: "#000000", // Текст в таблицах + treeChartText: "#000000", // Текст в древовидных диаграммах + scrollbarTrack: "#f1f1f1", // Цвет трека скроллбара + hoverButton: "#2d62b1", // Цвет кнопок при наведении + hoverText: "#FFFFFF", // Цвет текста при наведении }, + + // Цвета для различных состояний + action: { + hover: "rgba(0, 0, 0, 0.04)", // Цвет при наведении на интерактивные элементы + selected: "rgba(0, 0, 0, 0.08)", // Цвет выбранных элементов + } }, }); +/** + * Темная тема приложения + */ export const darkTheme = createTheme({ + ...commonThemeSettings, // Распаковываем общие настройки + + // Цветовая палитра для темной темы palette: { - mode: "dark", + mode: "dark", // Режим темной темы + + // Фоновые цвета background: { - default: "#1E1E1E", - paper: "#2d2d2d", + default: "#1E1E1E", // Основной фон приложения + paper: "#2d2d2d", // Фон "бумажных" поверхностей }, + + // Текстовые цвета text: { - primary: "#E0E0E0", + primary: "#E0E0E0", // Основной цвет текста + secondary: "#B0B0B0", // Вторичный цвет текста }, + + // Основные цвета UI primary: { - main: "#2d2d2d", + main: "#3d74c7", // Основной брендовый цвет (может совпадать со светлой темой) + contrastText: "#FFFFFF", // Цвет текста на кнопках primary цвета }, + + // Дополнительные цвета UI secondary: { - main: "#333333", + main: "#0f55bec2", // Вторичный брендовый цвет }, + + divider: "#444444", // Цвет разделителей + + // Кастомные цвета для специфических элементов custom: { - background: "#1E1E1E", - text: "#E0E0E0", - sidebar: "#2d2d2d", - sidebarText: "#E0E0E0", - modalBackground: "#2d2d2d", - modalBtnBackground: "#333333", - modalText: "#FFFFFF", - tableBorder: "#444444", - tableHeaderBackground: "#2d2d2d", - tableCellBackground: "#333333", - tableText: "#E0E0E0", - treeChartText: "#FFFFFF", - scrollbarTrack: "#333", - hoverButton: "#333d4d", - hoverText: "#E0E0E0", + background: "#1E1E1E", // Кастомный фоновый цвет + text: "#E0E0E0", // Кастомный цвет текста + sidebar: "#2d2d2d", // Фон боковой панели + sidebarText: "#E0E0E0", // Текст в боковой панели + sidebarHover: "rgba(255, 255, 255, 0.16)", // Цвет при наведении в боковой панели + modalBackground: "#2d2d2d", // Фон модальных окон + modalBtnBackground: "#333333", // Фон кнопок в модальных окнах + modalText: "#FFFFFF", // Текст в модальных окнах + tableBorder: "#444444", // Границы таблиц + tableHeaderBackground: "#2d2d2d", // Фон заголовков таблиц + tableCellBackground: "#333333", // Фон ячеек таблиц + tableText: "#E0E0E0", // Текст в таблицах + treeChartText: "#FFFFFF", // Текст в древовидных диаграммах + scrollbarTrack: "#333", // Цвет трека скроллбара + hoverButton: "#333d4d", // Цвет кнопок при наведении + hoverText: "#E0E0E0", // Цвет текста при наведении }, + + // Цвета для различных состояний + action: { + hover: "rgba(255, 255, 255, 0.08)", // Цвет при наведении на интерактивные элементы + selected: "rgba(255, 255, 255, 0.16)", // Цвет выбранных элементов + } }, -}); +}); \ No newline at end of file diff --git a/src/index.css b/src/index.css index 1134bba..7ef3275 100755 --- a/src/index.css +++ b/src/index.css @@ -83,7 +83,7 @@ button:focus-visible { /* Фон скроллбара */ ::-webkit-scrollbar-track { - background: var(--scrollbar-track-color, #f1f1f1); + background: var(--scrollbar-track-color, #025EA1); /* Цвет фона */ border-radius: 10px; /* Скругление углов */ @@ -91,7 +91,7 @@ button:focus-visible { /* Ползунок */ ::-webkit-scrollbar-thumb { - background: #3d74c7; + background: #D4EFFC; /* Основной цвет */ border-radius: 10px; /* Скругляем края */