From ed2e03e2023d6f9a89b284978e48373028014f01 Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Wed, 26 Mar 2025 06:53:41 -0400 Subject: [PATCH 01/13] adjusting the chart legend --- src/Charts/NegativeStatusChart.jsx | 11 ++++++++--- src/Charts/SystemStatusChart.jsx | 19 +++++++++++++++++-- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/Charts/NegativeStatusChart.jsx b/src/Charts/NegativeStatusChart.jsx index 0ee7531..d078f0c 100755 --- a/src/Charts/NegativeStatusChart.jsx +++ b/src/Charts/NegativeStatusChart.jsx @@ -2,13 +2,12 @@ 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 ( { - + ); 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 ( { - - + } /> -- 2.40.1 From bd962788954b86ca66027572f61588dc51b16153 Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Thu, 27 Mar 2025 10:09:58 -0400 Subject: [PATCH 02/13] redesign and fix graphics --- src/Charts/Components/LineChartComponent.jsx | 121 +++-- src/Charts/PrometheusChart.jsx | 89 ++-- src/Components/UI/Button.jsx | 34 ++ src/Components/UI/LoginModal.jsx | 2 +- src/Components/UI/TreeTable.jsx | 441 ++++++++++++------- src/Style/theme.jsx | 4 +- 6 files changed, 465 insertions(+), 226 deletions(-) create mode 100644 src/Components/UI/Button.jsx diff --git a/src/Charts/Components/LineChartComponent.jsx b/src/Charts/Components/LineChartComponent.jsx index b1d6963..1d0afbb 100755 --- a/src/Charts/Components/LineChartComponent.jsx +++ b/src/Charts/Components/LineChartComponent.jsx @@ -1,9 +1,11 @@ -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 [selectionArea, setSelectionArea] = useState(null); + const [isSelecting, setIsSelecting] = useState(false); + const chartRef = useRef(null); + const containerRef = useRef(null); const allTimes = Object.values(chartData) .flat() @@ -21,27 +23,48 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang const displayData = filteredData || data; - const handleClick = (e) => { + // Блокировка выделения текста при перетаскивании + useEffect(() => { + const handleSelectStart = (e) => { + if (isSelecting) { + e.preventDefault(); + } + }; + + document.addEventListener('selectstart', handleSelectStart); + return () => document.removeEventListener('selectstart', handleSelectStart); + }, [isSelecting]); + + const handleMouseDown = (e) => { if (!e || !e.activeLabel) return; - - const clickedTime = e.activeLabel; - - if (!selectionStart) { - setSelectionStart(clickedTime); - } else if (!selectionEnd) { - setSelectionEnd(clickedTime); - - const startIndex = data.findIndex(point => point.time === selectionStart); - const endIndex = data.findIndex(point => point.time === clickedTime); - - onRangeSelect({ startIndex, endIndex }); - - setSelectionStart(null); - setSelectionEnd(null); - } + setIsSelecting(true); + setSelectionArea({ start: e.activeLabel, end: null }); + }; + + const handleMouseMove = (e) => { + if (!selectionArea?.start || !e?.activeLabel) return; + setSelectionArea(prev => ({ ...prev, end: e.activeLabel })); + }; + + const handleMouseUp = () => { + setIsSelecting(false); + + if (!selectionArea?.start || !selectionArea?.end) { + setSelectionArea(null); + return; + } + + const startIndex = data.findIndex(point => point.time === selectionArea.start); + const endIndex = data.findIndex(point => point.time === selectionArea.end); + + onRangeSelect({ + startIndex: Math.min(startIndex, endIndex), + endIndex: Math.max(startIndex, endIndex) + }); + + setSelectionArea(null); }; - // Упрощенный Tooltip без указания instance const CustomTooltip = ({ active, payload, label }) => { if (active && payload && payload.length) { return ( @@ -49,10 +72,15 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang backgroundColor: '#fff', padding: '10px', border: '1px solid #ccc', - borderRadius: '4px' + borderRadius: '4px', + boxShadow: '0 2px 5px rgba(0,0,0,0.1)' }}>

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

-

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

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

+ {`${item.name}: ${item.value}`} +

+ ))} ); } @@ -60,12 +88,41 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang }; return ( -
+
+ + +
+
+ } cursor={{ stroke: '#ccc', strokeWidth: 1 }} /> - {/* Убрали чтобы скрыть имена instance */} {Object.keys(chartData).map((key, index) => ( ))} + + {selectionArea?.start && selectionArea?.end && ( + + )}
diff --git a/src/Charts/PrometheusChart.jsx b/src/Charts/PrometheusChart.jsx index c8a06ab..a16038f 100755 --- a/src/Charts/PrometheusChart.jsx +++ b/src/Charts/PrometheusChart.jsx @@ -92,7 +92,7 @@ const PrometheusChart = ({ metricName }) => { 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, @@ -100,7 +100,7 @@ const PrometheusChart = ({ metricName }) => { step } }); - + const result = response.data; let metrics = Array.isArray(result) ? result : result.data || []; @@ -161,10 +161,17 @@ const PrometheusChart = ({ metricName }) => { const handleRangeChange = (event) => { const selectedValue = event.target.value; const range = TIME_RANGES.find(range => range.value === parseInt(selectedValue, 10)); - setSelectedRange(range); + + // Принудительно сбрасываем состояние + setSelectedGraphRange(null); + setFilteredData(null); + + // Обновляем диапазон + setSelectedRange({ ...range }); // Создаем новый объект, чтобы React увидел изменение setUseCustomRange(false); - setSelectedGraphRange(null); // Сбрасываем выбранный диапазон - setFilteredData(null); // Сбрасываем отфильтрованные данные + + // Принудительно обновляем данные + fetchData(); }; const handleCustomRangeChange = () => { @@ -173,6 +180,12 @@ const PrometheusChart = ({ metricName }) => { setFilteredData(null); // Сбрасываем отфильтрованные данные }; + const handleResetZoom = () => { + setSelectedGraphRange(null); + setFilteredData(null); + fetchData(); // Принудительно обновляем данные + }; + useEffect(() => { if (selectedGraphRange) { const { startIndex, endIndex } = selectedGraphRange; @@ -236,32 +249,56 @@ const PrometheusChart = ({ metricName }) => { marginBottom: '15px' }}> {/* Стандартные диапазоны */} -
- - + {TIME_RANGES.map(range => ( + + ))} + +
+ + {/* Кнопка сброса */} +
+ + {/* Кастомный диапазон */}
{ Текущий диапазон: {useCustomRange ? `${startDate.toLocaleString()} - ${endDate.toLocaleString()}` : selectedRange.label} -
+
{/* График */} ({ + 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/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/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/Style/theme.jsx b/src/Style/theme.jsx index 02b38ed..7bcabf1 100644 --- a/src/Style/theme.jsx +++ b/src/Style/theme.jsx @@ -17,9 +17,9 @@ export const lightTheme = createTheme({ main: "#0f55bec2", }, custom: { - background: "#FFFFFF", + background: "#025EA1", text: "#000000", - sidebar: "#3d74c7", + sidebar: "#025EA1", sidebarText: "#FFFFFF", modalBackground: "#FFFFFF", modalBtnBackground: "#0f55bec2", -- 2.40.1 From 6a4640ba93734df9f10045f9009c03dc934939ed Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Fri, 28 Mar 2025 07:24:28 -0400 Subject: [PATCH 03/13] Deleted the trash files, added a button to minimize the side menu, and adjusted the styles --- src/Charts/Components/LineChartComponent.jsx | 2 +- src/Charts/NegativeStatusChart.jsx | 32 - src/Charts/PrometheusChart.jsx | 5 +- src/Charts/SystemStatusTable.jsx | 84 --- src/Charts/SystemStatusTableSoftware.jsx | 84 --- src/Components/Layout/Dashboard.jsx | 76 +- src/Components/Layout/SidebarMenu.jsx | 166 +++- .../Layout/SidebarMenuComponents/MenuItem.jsx | 145 ++-- .../SidebarMenuComponents/SidebarFooter.jsx | 51 +- src/Components/TreeChart/menuData222.json | 714 ------------------ src/Components/UI/ErrorIndicator.jsx | 24 - src/Components/UI/ExpandableInfo.jsx | 30 - src/Components/UI/LoginModal.jsx | 2 +- src/Components/UI/MUItabs.jsx | 42 +- src/Components/UI/Tabs.jsx | 56 -- src/Style/Dashboard.css | 52 -- src/Style/ErrorIndicator.css | 38 - src/Style/Expandable.css | 39 - src/Style/SidebarMenu.css | 143 ---- src/Style/TreeTable.css | 62 -- src/Style/common.css | 53 -- src/Style/dark-theme.css | 25 - src/Style/light-theme.css | 23 - src/Style/range-selector.css | 0 src/Style/theme.jsx | 204 +++-- src/index.css | 4 +- 26 files changed, 514 insertions(+), 1642 deletions(-) delete mode 100755 src/Charts/NegativeStatusChart.jsx delete mode 100755 src/Charts/SystemStatusTable.jsx delete mode 100755 src/Charts/SystemStatusTableSoftware.jsx delete mode 100755 src/Components/TreeChart/menuData222.json delete mode 100755 src/Components/UI/ErrorIndicator.jsx delete mode 100755 src/Components/UI/ExpandableInfo.jsx delete mode 100755 src/Components/UI/Tabs.jsx delete mode 100755 src/Style/Dashboard.css delete mode 100755 src/Style/ErrorIndicator.css delete mode 100755 src/Style/Expandable.css delete mode 100755 src/Style/TreeTable.css delete mode 100755 src/Style/common.css delete mode 100644 src/Style/dark-theme.css delete mode 100644 src/Style/light-theme.css delete mode 100644 src/Style/range-selector.css diff --git a/src/Charts/Components/LineChartComponent.jsx b/src/Charts/Components/LineChartComponent.jsx index 1d0afbb..898e12f 100755 --- a/src/Charts/Components/LineChartComponent.jsx +++ b/src/Charts/Components/LineChartComponent.jsx @@ -78,7 +78,7 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang

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

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

- {`${item.name}: ${item.value}`} + {`${item.value}`}

))}
diff --git a/src/Charts/NegativeStatusChart.jsx b/src/Charts/NegativeStatusChart.jsx deleted file mode 100755 index d078f0c..0000000 --- a/src/Charts/NegativeStatusChart.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; - -const SystemStatusChart = ({ data }) => { - 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 a16038f..5fb0b22 100755 --- a/src/Charts/PrometheusChart.jsx +++ b/src/Charts/PrometheusChart.jsx @@ -92,7 +92,7 @@ const PrometheusChart = ({ metricName }) => { else if (range <= 86400) step = 120; else step = 300; - const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/metrics`, { + const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/metrics`, { params: { metric: metricName, start, @@ -170,8 +170,7 @@ const PrometheusChart = ({ metricName }) => { setSelectedRange({ ...range }); // Создаем новый объект, чтобы React увидел изменение setUseCustomRange(false); - // Принудительно обновляем данные - fetchData(); + }; const handleCustomRangeChange = () => { 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/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 3e29de2..ada6823 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}/api/auth/login`, { + const response = await fetch(`${import.meta.env.VITE_BACK_URL}/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/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 index d4670ec..e69de29 100755 --- a/src/Style/SidebarMenu.css +++ b/src/Style/SidebarMenu.css @@ -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 7bcabf1..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: "#025EA1", - text: "#000000", - sidebar: "#025EA1", - 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; /* Скругляем края */ -- 2.40.1 From 4405c693aab39b3a6735d100a2a21c5092081b61 Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Tue, 1 Apr 2025 11:50:00 -0400 Subject: [PATCH 04/13] Established a connection to the back using a web socket --- package.json | 3 +- src/Charts/PrometheusChart.jsx | 319 ++++++++++++++++++++++----------- src/Style/SidebarMenu.css | 0 3 files changed, 213 insertions(+), 109 deletions(-) delete mode 100755 src/Style/SidebarMenu.css 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/PrometheusChart.jsx b/src/Charts/PrometheusChart.jsx index 5fb0b22..cd537da 100755 --- a/src/Charts/PrometheusChart.jsx +++ b/src/Charts/PrometheusChart.jsx @@ -1,8 +1,8 @@ import React, { useEffect, useState, useRef } from 'react'; -import axios from 'axios'; import DatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; import LineChartComponent from './Components/LineChartComponent'; +import { io } from 'socket.io-client'; const MAX_POINTS = 20; const COLORS = ['#3e95cd', '#8e5ea2', '#3cba9f', '#e8c3b9', '#c45850']; @@ -30,11 +30,142 @@ const PrometheusChart = ({ metricName }) => { 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 [selectedGraphRange, setSelectedGraphRange] = useState(null); + const [filteredData, setFilteredData] = useState(null); + const [connectionStatus, setConnectionStatus] = useState('disconnected'); const intervalRef = useRef(null); + const socketRef = useRef(null); + + const setupWebSocket = () => { + const socket = io('http://192.168.2.39:3000/metrics-ws', { + path: '/socket.io', + transports: ['websocket'], + reconnection: true, + reconnectionAttempts: 5, + reconnectionDelay: 1000, + }); + + socketRef.current = socket; + + socket.on('connect', () => { + setConnectionStatus('connected'); + fetchData(); + }); + + socket.on('disconnect', () => { + setConnectionStatus('disconnected'); + }); + + socket.on('connect_error', (err) => { + setConnectionStatus('error'); + }); + + socket.on('metrics-data', (response) => { + processMetricsData(response); + }); + + return socket; + }; + + const calculateStep = (start, end) => { + const range = end - start; + if (range <= 3600) return 5; + if (range <= 21600) return 30; + if (range <= 86400) return 120; + return 300; + }; + + const fetchData = () => { + try { + const now = Math.floor(Date.now() / 1000); + let start = useCustomRange + ? Math.floor(startDate.getTime() / 1000) + : now - selectedRange.value; + let end = useCustomRange + ? Math.floor(endDate.getTime() / 1000) + : now; + + // Проверка на корректность диапазона + if (start >= end) { + console.error('Invalid time range: start >= end'); + return; + } + + const step = calculateStep(start, end); + + if (socketRef.current?.connected) { + socketRef.current.emit('get-metrics', { + metric: metricName, + start, + end, + step + }); + } else { + console.error('WebSocket is not connected'); + } + } catch (error) { + console.error('Error in fetchData:', error); + } + }; + + const processMetricsData = (response) => { + const { metric, data } = response; + if (metric !== metricName) return; + + let metrics = Array.isArray(data) ? data : []; + let start, end; + + if (metrics.length === 0) { + console.warn('Received empty metrics data'); + return; + } + + 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; + } + + const step = calculateStep(start, end); + const range = end - start; + + // 1. Генерируем ВСЕ ожидаемые временные точки + 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); + } + + // 2. Создаем карту "время -> значение" для каждого инстанса + const timeValueMap = {}; + 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 (!timeValueMap[key]) timeValueMap[key] = {}; + timeValueMap[key][formattedTime] = m.value; + }); + + // 3. Строим финальные данные, гарантируя все временные точки + const newChartData = {}; + Object.keys(timeValueMap).forEach(instance => { + newChartData[instance] = timePoints.map(time => ({ + time, + value: timeValueMap[instance][time] ?? null // null если данных нет + })); + }); + + setChartData({ ...newChartData }); // Форсируем обновление + }; - // Функция для интерполяции данных const interpolateData = (data, minPoints = 15) => { if (data.length >= minPoints) return data; @@ -44,19 +175,15 @@ const PrometheusChart = ({ metricName }) => { const currentPoint = data[i]; const nextPoint = data[i + 1]; - - // Вычисляем разницу во времени между точками const currentTime = new Date(currentPoint.time).getTime(); const nextTime = new Date(nextPoint.time).getTime(); const timeDiff = nextTime - currentTime; - // Добавляем промежуточные точки 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 }; - // Интерполируем значения для каждой метрики Object.keys(currentPoint).forEach(key => { if (key !== 'time') { const currentValue = currentPoint[key]; @@ -69,122 +196,85 @@ const PrometheusChart = ({ metricName }) => { } } - interpolatedData.push(data[data.length - 1]); // Добавляем последнюю точку - return interpolatedData.slice(0, minPoints); // Обрезаем до minPoints + interpolatedData.push(data[data.length - 1]); + return interpolatedData.slice(0, minPoints); }; - 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`, { - params: { - metric: metricName, - start, - end, - step - } - }); - - 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, - })); - }); - - setChartData(chartData); - } catch (error) { - console.error('Ошибка при загрузке метрик:', error); - } - }; - - useEffect(() => { - fetchData(); - - intervalRef.current = setInterval(() => { - fetchData(); - }, 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)); + const range = TIME_RANGES.find(r => r.value === parseInt(selectedValue, 10)); - // Принудительно сбрасываем состояние - setSelectedGraphRange(null); + // Сбрасываем данные и состояние + setChartData({}); setFilteredData(null); + setSelectedGraphRange(null); - // Обновляем диапазон - setSelectedRange({ ...range }); // Создаем новый объект, чтобы React увидел изменение + // Обновляем диапазон и даты + setSelectedRange(range); setUseCustomRange(false); + const now = new Date(); + setEndDate(now); + setStartDate(new Date(now.getTime() - range.value * 1000)); + // Принудительно запрашиваем новые данные + // Используем setTimeout для гарантированного обновления состояния перед запросом + setTimeout(() => { + fetchData(); + }, 0); }; const handleCustomRangeChange = () => { + // Сбрасываем данные и состояние + setChartData({}); + setFilteredData(null); + setSelectedGraphRange(null); + setUseCustomRange(true); - setSelectedGraphRange(null); // Сбрасываем выбранный диапазон - setFilteredData(null); // Сбрасываем отфильтрованные данные + + // Принудительно запрашиваем новые данные + setTimeout(() => { + fetchData(); + }, 0); }; const handleResetZoom = () => { setSelectedGraphRange(null); setFilteredData(null); - fetchData(); // Принудительно обновляем данные + fetchData(); }; + useEffect(() => { + const socket = setupWebSocket(); + + // Первоначальная загрузка данных + fetchData(); + + // Настраиваем интервал обновления + const intervalId = setInterval(() => { + fetchData(); + }, selectedRange.interval); + + intervalRef.current = intervalId; + + return () => { + clearInterval(intervalRef.current); + socket.disconnect(); + }; + }, [metricName, selectedRange.value]); + + useEffect(() => { + // При изменении диапазона или дат перезапускаем интервал + if (socketRef.current?.connected) { + clearInterval(intervalRef.current); + fetchData(); + + intervalRef.current = setInterval(() => { + fetchData(); + }, selectedRange.interval); + } + }, [selectedRange, useCustomRange, startDate, endDate, metricName]); + useEffect(() => { if (selectedGraphRange) { const { startIndex, endIndex } = selectedGraphRange; @@ -203,12 +293,10 @@ const PrometheusChart = ({ metricName }) => { }); const filtered = data.slice(startIndex, endIndex + 1); - - // Интерполируем данные, если точек меньше 15 const interpolated = interpolateData(filtered, 15); - setFilteredData(interpolated); // Сохраняем интерполированные данные + setFilteredData(interpolated); } else { - setFilteredData(null); // Сбрасываем фильтрацию, если диапазон не выбран + setFilteredData(null); } }, [selectedGraphRange, chartData]); @@ -233,8 +321,23 @@ const PrometheusChart = ({ metricName }) => { backgroundColor: '#fff', borderRadius: '8px', padding: '20px', - marginBottom: '20px' + marginBottom: '20px', + position: 'relative' }}> +
+ {connectionStatus === 'connected' ? 'Online' : + connectionStatus === 'error' ? 'Connection Error' : 'Offline'} +
{/* Заголовок графика */}

diff --git a/src/Style/SidebarMenu.css b/src/Style/SidebarMenu.css deleted file mode 100755 index e69de29..0000000 -- 2.40.1 From 32ece2f0ff831ceaf16fe0946fc6d97245ebde3d Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Wed, 2 Apr 2025 19:50:08 -0400 Subject: [PATCH 05/13] refactoring, fixed bugs with the web socket --- .../Components/ConnectionStatusIndicator.jsx | 20 + src/Charts/Components/CurrentRangeDisplay.jsx | 17 + src/Charts/Components/LineChartComponent.jsx | 73 ++- src/Charts/Components/TimeRangeSelector.jsx | 151 +++++ src/Charts/Components/constants.jsx | 20 + src/Charts/PrometheusChart.jsx | 550 ++++++------------ 6 files changed, 424 insertions(+), 407 deletions(-) create mode 100644 src/Charts/Components/ConnectionStatusIndicator.jsx create mode 100644 src/Charts/Components/CurrentRangeDisplay.jsx create mode 100644 src/Charts/Components/TimeRangeSelector.jsx create mode 100644 src/Charts/Components/constants.jsx 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 898e12f..53d859f 100755 --- a/src/Charts/Components/LineChartComponent.jsx +++ b/src/Charts/Components/LineChartComponent.jsx @@ -1,7 +1,13 @@ 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 LineChartComponent = ({ + chartData, + metricName, + colors, + onRangeSelect, + filteredData +}) => { const [selectionArea, setSelectionArea] = useState(null); const [isSelecting, setIsSelecting] = useState(false); const chartRef = useRef(null); @@ -19,11 +25,14 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang point[key] = instanceData ? instanceData.value : null; }); return point; + }).sort((a, b) => { + const timeA = chartData[Object.keys(chartData)[0]].find(d => d.time === a.time)?.timestamp; + const timeB = chartData[Object.keys(chartData)[0]].find(d => d.time === b.time)?.timestamp; + return timeA - timeB; }); const displayData = filteredData || data; - // Блокировка выделения текста при перетаскивании useEffect(() => { const handleSelectStart = (e) => { if (isSelecting) { @@ -57,10 +66,12 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang const startIndex = data.findIndex(point => point.time === selectionArea.start); const endIndex = data.findIndex(point => point.time === selectionArea.end); - onRangeSelect({ - startIndex: Math.min(startIndex, endIndex), - endIndex: Math.max(startIndex, endIndex) - }); + if (startIndex >= 0 && endIndex >= 0) { + onRangeSelect({ + startIndex: Math.min(startIndex, endIndex), + endIndex: Math.max(startIndex, endIndex) + }); + } setSelectionArea(null); }; @@ -68,17 +79,17 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang const CustomTooltip = ({ active, payload, label }) => { if (active && payload && payload.length) { return ( -
-

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

+

{`${label}`}

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

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

))}
@@ -87,9 +98,13 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang return null; }; + if (!data.length) { + return
Нет данных для отображения
; + } + return (
@@ -102,20 +117,7 @@ const LineChartComponent = ({ chartData, metricName, colors, description, onRang `} -
-
- - + - - } - cursor={{ stroke: '#ccc', strokeWidth: 1 }} - /> - {Object.keys(chartData).map((key, index) => ( + + } /> + {Object.keys(chartData).map((instance, index) => ( ))} - {selectionArea?.start && selectionArea?.end && ( { + 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/PrometheusChart.jsx b/src/Charts/PrometheusChart.jsx index cd537da..632767d 100755 --- a/src/Charts/PrometheusChart.jsx +++ b/src/Charts/PrometheusChart.jsx @@ -1,81 +1,102 @@ -import React, { useEffect, useState, useRef } from 'react'; -import DatePicker from 'react-datepicker'; -import 'react-datepicker/dist/react-datepicker.css'; -import LineChartComponent from './Components/LineChartComponent'; +import React, { useEffect, useState, useRef, useCallback } from 'react'; import { io } from 'socket.io-client'; - -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 LineChartComponent from './Components/LineChartComponent'; +import { TimeRangeSelector } from './Components/TimeRangeSelector'; +import { ConnectionStatusIndicator } from './Components/ConnectionStatusIndicator'; +import { CurrentRangeDisplay } from './Components/CurrentRangeDisplay'; +import { TIME_RANGES, COLORS } from './Components/constants'; 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 [connectionStatus, setConnectionStatus] = useState('disconnected'); const [selectedGraphRange, setSelectedGraphRange] = useState(null); const [filteredData, setFilteredData] = useState(null); - const [connectionStatus, setConnectionStatus] = useState('disconnected'); const intervalRef = useRef(null); const socketRef = useRef(null); - const setupWebSocket = () => { + const formatTime = useCallback((timestamp, rangeSeconds) => { + const date = new Date(timestamp); + if (rangeSeconds > 86400) { + return { + display: date.toLocaleString([], { + day: '2-digit', + month: '2-digit', + year: '2-digit', + hour: '2-digit', + minute: '2-digit' + }), + timestamp: timestamp + }; + } + return { + display: date.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }), + timestamp: timestamp + }; + }, []); + + const setupWebSocket = useCallback(() => { + if (socketRef.current?.connected) return socketRef.current; + + if (socketRef.current) socketRef.current.disconnect(); + const socket = io('http://192.168.2.39:3000/metrics-ws', { - path: '/socket.io', transports: ['websocket'], reconnection: true, - reconnectionAttempts: 5, + reconnectionAttempts: Infinity, reconnectionDelay: 1000, + reconnectionDelayMax: 5000, }); socketRef.current = socket; socket.on('connect', () => { + console.log('WebSocket connected'); setConnectionStatus('connected'); fetchData(); }); - socket.on('disconnect', () => { + socket.on('disconnect', (reason) => { + console.log('WebSocket disconnected:', reason); setConnectionStatus('disconnected'); + if (reason === 'io server disconnect') socket.connect(); }); - socket.on('connect_error', (err) => { + 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); }); - return socket; - }; + socket.on('metrics-error', (error) => { + console.error('Metrics error:', error); + setConnectionStatus('error'); + }); - const calculateStep = (start, end) => { + return socket; + }, []); + + const calculateStep = useCallback((start, end) => { const range = end - start; if (range <= 3600) return 5; if (range <= 21600) return 30; if (range <= 86400) return 120; return 300; - }; + }, []); - const fetchData = () => { + const fetchData = useCallback(() => { try { const now = Math.floor(Date.now() / 1000); let start = useCustomRange @@ -85,13 +106,13 @@ const PrometheusChart = ({ metricName }) => { ? Math.floor(endDate.getTime() / 1000) : now; - // Проверка на корректность диапазона if (start >= end) { console.error('Invalid time range: start >= end'); return; } const step = calculateStep(start, end); + console.log(`Fetching data for ${metricName}`, { start, end, step }); if (socketRef.current?.connected) { socketRef.current.emit('get-metrics', { @@ -102,219 +123,163 @@ const PrometheusChart = ({ metricName }) => { }); } else { console.error('WebSocket is not connected'); + setupWebSocket(); } } catch (error) { console.error('Error in fetchData:', error); } - }; + }, [metricName, selectedRange, useCustomRange, startDate, endDate, calculateStep, setupWebSocket]); - const processMetricsData = (response) => { + const processMetricsData = useCallback((response) => { const { metric, data } = response; if (metric !== metricName) return; - let metrics = Array.isArray(data) ? data : []; - let start, end; - - if (metrics.length === 0) { - console.warn('Received empty metrics data'); + if (!Array.isArray(data)) { + console.error('Invalid data format:', data); return; } - 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; - } + console.log('Processing metrics data:', data); - const step = calculateStep(start, end); - const range = end - start; + const now = Math.floor(Date.now() / 1000); + const rangeSeconds = useCustomRange + ? Math.floor(endDate.getTime() / 1000) - Math.floor(startDate.getTime() / 1000) + : selectedRange.value; - // 1. Генерируем ВСЕ ожидаемые временные точки - 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 instancesData = {}; - // 2. Создаем карту "время -> значение" для каждого инстанса - const timeValueMap = {}; - 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' }); + data.forEach(item => { + const instance = item.instance || 'default'; + const timestamp = item.timestamp; + const value = parseFloat(item.value); - const key = m.instance; - if (!timeValueMap[key]) timeValueMap[key] = {}; - timeValueMap[key][formattedTime] = m.value; - }); - - // 3. Строим финальные данные, гарантируя все временные точки - const newChartData = {}; - Object.keys(timeValueMap).forEach(instance => { - newChartData[instance] = timePoints.map(time => ({ - time, - value: timeValueMap[instance][time] ?? null // null если данных нет - })); - }); - - setChartData({ ...newChartData }); // Форсируем обновление - }; - - const interpolateData = (data, minPoints = 15) => { - if (data.length >= minPoints) return data; - - const interpolatedData = []; - for (let i = 0; i < data.length - 1; i++) { - interpolatedData.push(data[i]); - - const currentPoint = data[i]; - const nextPoint = data[i + 1]; - const currentTime = new Date(currentPoint.time).getTime(); - const nextTime = new Date(nextPoint.time).getTime(); - const timeDiff = nextTime - currentTime; - - 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 }; - - Object.keys(currentPoint).forEach(key => { - if (key !== 'time') { - const currentValue = currentPoint[key]; - const nextValue = nextPoint[key]; - interpolatedPoint[key] = currentValue + ((nextValue - currentValue) * j) / (steps + 1); - } - }); - - interpolatedData.push(interpolatedPoint); + if (!instancesData[instance]) { + instancesData[instance] = []; } - } - interpolatedData.push(data[data.length - 1]); - return interpolatedData.slice(0, minPoints); - }; + const formatted = formatTime(timestamp, rangeSeconds); + instancesData[instance].push({ + time: formatted.display, // для отображения + value: value, + timestamp: timestamp, // для сортировки + originalTime: formatted // для группировки + }); + }); - const handleRangeChange = (event) => { + // Группируем точки с одинаковым временем (в пределах секунды) + Object.keys(instancesData).forEach(instance => { + const grouped = []; + const timeMap = {}; + + instancesData[instance].forEach(point => { + const timeKey = Math.floor(point.timestamp / 1000); // группируем по секундам + if (!timeMap[timeKey]) { + timeMap[timeKey] = { + ...point, + count: 1 + }; + grouped.push(timeMap[timeKey]); + } else { + // Усредняем значения для точек в одну секунду + timeMap[timeKey].value = (timeMap[timeKey].value * timeMap[timeKey].count + point.value) / + (timeMap[timeKey].count + 1); + timeMap[timeKey].count += 1; + } + }); + + instancesData[instance] = grouped.sort((a, b) => a.timestamp - b.timestamp); + }); + + console.log('Processed chart data:', instancesData); + setChartData(instancesData); + setSelectedGraphRange(null); + setFilteredData(null); + }, [metricName, formatTime, useCustomRange, startDate, endDate, selectedRange.value]); + const handleRangeChange = useCallback((event) => { const selectedValue = event.target.value; const range = TIME_RANGES.find(r => r.value === parseInt(selectedValue, 10)); - // Сбрасываем данные и состояние - setChartData({}); - setFilteredData(null); - setSelectedGraphRange(null); - - // Обновляем диапазон и даты setSelectedRange(range); setUseCustomRange(false); + setChartData(null); + setSelectedGraphRange(null); + setFilteredData(null); const now = new Date(); setEndDate(now); setStartDate(new Date(now.getTime() - range.value * 1000)); + }, []); - // Принудительно запрашиваем новые данные - // Используем setTimeout для гарантированного обновления состояния перед запросом - setTimeout(() => { - fetchData(); - }, 0); - }; - - const handleCustomRangeChange = () => { - // Сбрасываем данные и состояние - setChartData({}); - setFilteredData(null); - setSelectedGraphRange(null); - + const handleCustomRangeChange = useCallback(() => { setUseCustomRange(true); + setChartData(null); + setSelectedGraphRange(null); + setFilteredData(null); + }, []); - // Принудительно запрашиваем новые данные - setTimeout(() => { - fetchData(); - }, 0); - }; - - const handleResetZoom = () => { + const handleResetZoom = useCallback(() => { setSelectedGraphRange(null); setFilteredData(null); fetchData(); - }; + }, [fetchData]); useEffect(() => { const socket = setupWebSocket(); - - // Первоначальная загрузка данных - fetchData(); - - // Настраиваем интервал обновления - const intervalId = setInterval(() => { - fetchData(); - }, selectedRange.interval); - - intervalRef.current = intervalId; - return () => { clearInterval(intervalRef.current); socket.disconnect(); }; - }, [metricName, selectedRange.value]); + }, [setupWebSocket]); useEffect(() => { - // При изменении диапазона или дат перезапускаем интервал - if (socketRef.current?.connected) { - clearInterval(intervalRef.current); + if (!socketRef.current?.connected) return; + + clearInterval(intervalRef.current); + fetchData(); + + intervalRef.current = setInterval(() => { fetchData(); + }, selectedRange.interval); - intervalRef.current = setInterval(() => { - fetchData(); - }, selectedRange.interval); - } - }, [selectedRange, useCustomRange, startDate, endDate, metricName]); + return () => clearInterval(intervalRef.current); + }, [fetchData, selectedRange.interval]); 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); - const interpolated = interpolateData(filtered, 15); - setFilteredData(interpolated); - } else { + if (!chartData || !selectedGraphRange) { setFilteredData(null); + return; } + + 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; + }).sort((a, b) => { + const timeA = chartData[Object.keys(chartData)[0]].find(d => d.time === a.time)?.timestamp; + const timeB = chartData[Object.keys(chartData)[0]].find(d => d.time === b.time)?.timestamp; + return timeA - timeB; + }); + + const filtered = data.slice(startIndex, endIndex + 1); + setFilteredData(filtered); }, [selectedGraphRange, chartData]); - if (!Object.keys(chartData).length) return

Loading...

; + if (chartData === null) { + return
Loading data...
; + } - 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; - }); + if (Object.keys(chartData).length === 0) { + return
No data available
; + } return (
{ marginBottom: '20px', position: 'relative' }}> -
- {connectionStatus === 'connected' ? 'Online' : - connectionStatus === 'error' ? 'Connection Error' : 'Offline'} -
- {/* Заголовок графика */} -

-

+ - {/* Группа элементов управления */} -
- {/* Стандартные диапазоны */} -
-
- - -
+ - {/* Кнопка сброса */} - -
+ - - - {/* Кастомный диапазон */} -
-
- Или укажите свой диапазон: -
-
-
- 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} -
- - {/* График */} @@ -505,4 +321,4 @@ const PrometheusChart = ({ metricName }) => { ); }; -export default PrometheusChart; \ No newline at end of file +export default React.memo(PrometheusChart); \ No newline at end of file -- 2.40.1 From a24b89220ca0f5e80d7730d5eab61e8e44320237 Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Fri, 4 Apr 2025 15:31:20 -0400 Subject: [PATCH 06/13] fixed bugs --- src/Charts/PrometheusChart.jsx | 178 +++++++++++++++------------------ 1 file changed, 81 insertions(+), 97 deletions(-) diff --git a/src/Charts/PrometheusChart.jsx b/src/Charts/PrometheusChart.jsx index 632767d..cefc315 100755 --- a/src/Charts/PrometheusChart.jsx +++ b/src/Charts/PrometheusChart.jsx @@ -43,9 +43,12 @@ const PrometheusChart = ({ metricName }) => { }, []); const setupWebSocket = useCallback(() => { - if (socketRef.current?.connected) return socketRef.current; - - if (socketRef.current) socketRef.current.disconnect(); + if (socketRef.current) { + // Если соединение уже существует, возвращаем его + if (socketRef.current.connected) return socketRef.current; + // Если соединение в процессе переподключения, тоже возвращаем + if (socketRef.current.reconnecting) return socketRef.current; + } const socket = io('http://192.168.2.39:3000/metrics-ws', { transports: ['websocket'], @@ -97,104 +100,73 @@ const PrometheusChart = ({ metricName }) => { }, []); const fetchData = useCallback(() => { - try { - const now = Math.floor(Date.now() / 1000); - let start = useCustomRange - ? Math.floor(startDate.getTime() / 1000) - : now - selectedRange.value; - let end = useCustomRange - ? Math.floor(endDate.getTime() / 1000) - : now; - - if (start >= end) { - console.error('Invalid time range: start >= end'); - return; - } - - const step = calculateStep(start, end); - console.log(`Fetching data for ${metricName}`, { start, end, step }); - - if (socketRef.current?.connected) { - socketRef.current.emit('get-metrics', { - metric: metricName, - start, - end, - step - }); - } else { - console.error('WebSocket is not connected'); - setupWebSocket(); - } - } catch (error) { - console.error('Error in fetchData:', error); - } - }, [metricName, selectedRange, useCustomRange, startDate, endDate, calculateStep, setupWebSocket]); - - const processMetricsData = useCallback((response) => { - const { metric, data } = response; - if (metric !== metricName) return; - - if (!Array.isArray(data)) { - console.error('Invalid data format:', data); - return; - } - - console.log('Processing metrics data:', data); - const now = Math.floor(Date.now() / 1000); - const rangeSeconds = useCustomRange - ? Math.floor(endDate.getTime() / 1000) - Math.floor(startDate.getTime() / 1000) - : selectedRange.value; + const start = now - selectedRange.value; + const end = now; + const step = calculateStep(start, end); - const instancesData = {}; - - data.forEach(item => { - const instance = item.instance || 'default'; - const timestamp = item.timestamp; - const value = parseFloat(item.value); - - if (!instancesData[instance]) { - instancesData[instance] = []; - } - - const formatted = formatTime(timestamp, rangeSeconds); - instancesData[instance].push({ - time: formatted.display, // для отображения - value: value, - timestamp: timestamp, // для сортировки - originalTime: formatted // для группировки + if (socketRef.current?.connected) { + socketRef.current.emit('get-metrics', { + metric: metricName, + start, + end, + step, + _t: Date.now() // Добавляем timestamp для уникальности }); + } + }, [metricName, selectedRange.value]); // Только необходимые зависимости + + const groupBySecond = (points) => { + const grouped = []; + const timeMap = {}; + + points.forEach(point => { + const timeKey = Math.floor(point.timestamp / 1000); + if (!timeMap[timeKey]) { + timeMap[timeKey] = { ...point, count: 1 }; + grouped.push(timeMap[timeKey]); + } else { + timeMap[timeKey].value = (timeMap[timeKey].value * timeMap[timeKey].count + point.value) / + (timeMap[timeKey].count + 1); + timeMap[timeKey].count += 1; + } }); - // Группируем точки с одинаковым временем (в пределах секунды) - Object.keys(instancesData).forEach(instance => { - const grouped = []; - const timeMap = {}; + return grouped; + }; - instancesData[instance].forEach(point => { - const timeKey = Math.floor(point.timestamp / 1000); // группируем по секундам - if (!timeMap[timeKey]) { - timeMap[timeKey] = { - ...point, - count: 1 - }; - grouped.push(timeMap[timeKey]); - } else { - // Усредняем значения для точек в одну секунду - timeMap[timeKey].value = (timeMap[timeKey].value * timeMap[timeKey].count + point.value) / - (timeMap[timeKey].count + 1); - timeMap[timeKey].count += 1; + const processMetricsData = useCallback((response) => { + if (response.metric !== metricName || !Array.isArray(response.data)) return; + + setChartData(prev => { + const newData = { ...(prev || {}) }; + + // Добавление новых точек + response.data.forEach(item => { + const instance = item.instance || 'default'; + if (!newData[instance]) newData[instance] = []; + + if (!newData[instance].some(p => p.timestamp === item.timestamp)) { + newData[instance].push({ + time: formatTime(item.timestamp, selectedRange.value).display, + value: parseFloat(item.value), + timestamp: item.timestamp + }); } }); - instancesData[instance] = grouped.sort((a, b) => a.timestamp - b.timestamp); - }); + // Группировка и ограничение + Object.keys(newData).forEach(instance => { + newData[instance] = groupBySecond(newData[instance]) + .sort((a, b) => a.timestamp - b.timestamp) + .slice(-1000); + }); + + return Object.keys(newData).length ? newData : prev; + }); + }, [metricName, selectedRange.value, formatTime]); + - console.log('Processed chart data:', instancesData); - setChartData(instancesData); - setSelectedGraphRange(null); - setFilteredData(null); - }, [metricName, formatTime, useCustomRange, startDate, endDate, selectedRange.value]); const handleRangeChange = useCallback((event) => { const selectedValue = event.target.value; const range = TIME_RANGES.find(r => r.value === parseInt(selectedValue, 10)); @@ -234,14 +206,26 @@ const PrometheusChart = ({ metricName }) => { useEffect(() => { if (!socketRef.current?.connected) return; - clearInterval(intervalRef.current); - fetchData(); + const fetchDataWrapper = () => { + try { + fetchData(); + } catch (error) { + console.error('Error in interval fetch:', error); + } + }; - intervalRef.current = setInterval(() => { - fetchData(); - }, selectedRange.interval); + // Сразу запросить данные + fetchDataWrapper(); - return () => clearInterval(intervalRef.current); + // Установить интервал + intervalRef.current = setInterval(fetchDataWrapper, selectedRange.interval); + + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }; }, [fetchData, selectedRange.interval]); useEffect(() => { -- 2.40.1 From 64401cadbc5e63b0359740f7e784cf3459a1620b Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Mon, 7 Apr 2025 10:27:08 -0400 Subject: [PATCH 07/13] fixed data transmission via a web socket and left data transmission via http requests for historical data --- src/Charts/PrometheusChart.jsx | 239 ++++++++++++++++++++++----------- 1 file changed, 157 insertions(+), 82 deletions(-) diff --git a/src/Charts/PrometheusChart.jsx b/src/Charts/PrometheusChart.jsx index cefc315..ab30858 100755 --- a/src/Charts/PrometheusChart.jsx +++ b/src/Charts/PrometheusChart.jsx @@ -5,6 +5,7 @@ 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(null); @@ -15,6 +16,7 @@ const PrometheusChart = ({ metricName }) => { const [connectionStatus, setConnectionStatus] = useState('disconnected'); const [selectedGraphRange, setSelectedGraphRange] = useState(null); const [filteredData, setFilteredData] = useState(null); + const [isSelectingRange, setIsSelectingRange] = useState(false); const intervalRef = useRef(null); const socketRef = useRef(null); @@ -42,6 +44,88 @@ const PrometheusChart = ({ metricName }) => { }; }, []); + const calculateStep = useCallback((start, end) => { + const range = end - start; + if (range <= 3600) return 5; + if (range <= 21600) return 30; + if (range <= 86400) return 120; + return 300; + }, []); + + 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 groupBySecond = (points) => { + const grouped = []; + const timeMap = {}; + + points.forEach(point => { + const timeKey = Math.floor(point.timestamp / 1000); + if (!timeMap[timeKey]) { + timeMap[timeKey] = { ...point, count: 1 }; + grouped.push(timeMap[timeKey]); + } else { + timeMap[timeKey].value = (timeMap[timeKey].value * timeMap[timeKey].count + point.value) / + (timeMap[timeKey].count + 1); + timeMap[timeKey].count += 1; + } + }); + + return grouped; + }; + + const processMetricsData = useCallback((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 || {}) }; + + dataArray.forEach(item => { + const instance = item.instance || 'default'; + if (!newData[instance]) newData[instance] = []; + + const timestamp = item.timestamp > 1e12 ? item.timestamp : item.timestamp * 1000; + const value = parseFloat(item.value); + + if (!newData[instance].some(p => p.timestamp === timestamp)) { + newData[instance].push({ + time: formatTime(timestamp, selectedRange.value).display, + value: value, + timestamp: timestamp + }); + } + }); + + Object.keys(newData).forEach(instance => { + newData[instance] = groupBySecond(newData[instance]) + .sort((a, b) => a.timestamp - b.timestamp) + .slice(-1000); + }); + + return Object.keys(newData).length ? newData : prev; + }); + }, [metricName, selectedRange.value, formatTime]); + const setupWebSocket = useCallback(() => { if (socketRef.current) { // Если соединение уже существует, возвращаем его @@ -91,95 +175,55 @@ const PrometheusChart = ({ metricName }) => { return socket; }, []); - const calculateStep = useCallback((start, end) => { - const range = end - start; - if (range <= 3600) return 5; - if (range <= 21600) return 30; - if (range <= 86400) return 120; - return 300; - }, []); - - const fetchData = useCallback(() => { - const now = Math.floor(Date.now() / 1000); - const start = now - selectedRange.value; - const end = now; + const fetchCustomRangeData = useCallback(async () => { + const start = Math.floor(startDate.getTime() / 1000); + const end = Math.floor(endDate.getTime() / 1000); const step = calculateStep(start, end); - - if (socketRef.current?.connected) { - socketRef.current.emit('get-metrics', { - metric: metricName, - start, - end, - step, - _t: Date.now() // Добавляем timestamp для уникальности - }); - } - }, [metricName, selectedRange.value]); // Только необходимые зависимости - - const groupBySecond = (points) => { - const grouped = []; - const timeMap = {}; - - points.forEach(point => { - const timeKey = Math.floor(point.timestamp / 1000); - if (!timeMap[timeKey]) { - timeMap[timeKey] = { ...point, count: 1 }; - grouped.push(timeMap[timeKey]); - } else { - timeMap[timeKey].value = (timeMap[timeKey].value * timeMap[timeKey].count + point.value) / - (timeMap[timeKey].count + 1); - timeMap[timeKey].count += 1; - } - }); - - return grouped; - }; - - const processMetricsData = useCallback((response) => { - if (response.metric !== metricName || !Array.isArray(response.data)) return; - - setChartData(prev => { - const newData = { ...(prev || {}) }; - - // Добавление новых точек - response.data.forEach(item => { - const instance = item.instance || 'default'; - if (!newData[instance]) newData[instance] = []; - - if (!newData[instance].some(p => p.timestamp === item.timestamp)) { - newData[instance].push({ - time: formatTime(item.timestamp, selectedRange.value).display, - value: parseFloat(item.value), - timestamp: item.timestamp - }); + + try { + const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/metrics`, { + params: { + metric: metricName, + start, + end, + step } }); - - // Группировка и ограничение - Object.keys(newData).forEach(instance => { - newData[instance] = groupBySecond(newData[instance]) - .sort((a, b) => a.timestamp - b.timestamp) - .slice(-1000); - }); - - return Object.keys(newData).length ? newData : prev; - }); - }, [metricName, selectedRange.value, formatTime]); + + if (response.data) { // Изменили условие, так как бэкенд возвращает массив напрямую + processMetricsData({ + metric: metricName, + data: response.data.map(item => ({ + ...item, + timestamp: item.timestamp / 1000, // или item.timestamp если уже в секундах + value: item.value.toString() // преобразуем в строку, как ожидает processMetricsData + })) + }); + } + } catch (error) { + console.error('Ошибка при получении кастомных данных:', error); + } + }, [metricName, startDate, endDate, calculateStep, processMetricsData]); const handleRangeChange = useCallback((event) => { 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(() => { @@ -192,9 +236,35 @@ const PrometheusChart = ({ metricName }) => { const handleResetZoom = useCallback(() => { setSelectedGraphRange(null); setFilteredData(null); + setIsSelectingRange(false); fetchData(); }, [fetchData]); + const handleRangeSelect = useCallback((range) => { + if (range) { + // Начало выделения - останавливаем обновления + setIsSelectingRange(true); + setSelectedGraphRange(range); + + // Отключаем сокет + if (socketRef.current?.connected) { + socketRef.current.disconnect(); + } + // Очищаем интервал + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + } else { + // Окончание выделения - возобновляем соединение + setIsSelectingRange(false); + + if (!useCustomRange && socketRef.current && !socketRef.current.connected) { + socketRef.current.connect(); + } + } + }, [useCustomRange]); + useEffect(() => { const socket = setupWebSocket(); return () => { @@ -204,8 +274,16 @@ const PrometheusChart = ({ metricName }) => { }, [setupWebSocket]); useEffect(() => { - if (!socketRef.current?.connected) return; - + if (useCustomRange) { + if (socketRef.current?.connected) { + socketRef.current.disconnect(); + } + fetchCustomRangeData(); + return; + } + + if (!socketRef.current?.connected || isSelectingRange) return; + const fetchDataWrapper = () => { try { fetchData(); @@ -213,20 +291,17 @@ const PrometheusChart = ({ metricName }) => { console.error('Error in interval fetch:', error); } }; - - // Сразу запросить данные + fetchDataWrapper(); - - // Установить интервал intervalRef.current = setInterval(fetchDataWrapper, selectedRange.interval); - + return () => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }; - }, [fetchData, selectedRange.interval]); + }, [fetchData, fetchCustomRangeData, selectedRange.interval, useCustomRange, isSelectingRange]); useEffect(() => { if (!chartData || !selectedGraphRange) { @@ -298,7 +373,7 @@ const PrometheusChart = ({ metricName }) => { chartData={chartData} metricName={metricName} colors={COLORS} - onRangeSelect={setSelectedGraphRange} + onRangeSelect={handleRangeSelect} // Используем модифицированный обработчик filteredData={filteredData} />
-- 2.40.1 From 46da90fbb6873911e5d081a0434d792b1d261955 Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Tue, 8 Apr 2025 01:42:47 -0400 Subject: [PATCH 08/13] Fixed the date and time display --- src/Charts/Components/LineChartComponent.jsx | 58 +++++++++---- src/Charts/PrometheusChart.jsx | 91 +++++++++++--------- 2 files changed, 92 insertions(+), 57 deletions(-) diff --git a/src/Charts/Components/LineChartComponent.jsx b/src/Charts/Components/LineChartComponent.jsx index 53d859f..1973bac 100755 --- a/src/Charts/Components/LineChartComponent.jsx +++ b/src/Charts/Components/LineChartComponent.jsx @@ -13,24 +13,33 @@ const LineChartComponent = ({ const chartRef = useRef(null); const containerRef = 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; - }).sort((a, b) => { - const timeA = chartData[Object.keys(chartData)[0]].find(d => d.time === a.time)?.timestamp; - const timeB = chartData[Object.keys(chartData)[0]].find(d => d.time === b.time)?.timestamp; - return timeA - timeB; }); + const displayData = filteredData || data; useEffect(() => { @@ -47,7 +56,7 @@ const LineChartComponent = ({ const handleMouseDown = (e) => { if (!e || !e.activeLabel) return; setIsSelecting(true); - setSelectionArea({ start: e.activeLabel, end: null }); + setSelectionArea({ start: e.activeLabel, end: null }); // activeLabel — это timestamp }; const handleMouseMove = (e) => { @@ -63,8 +72,8 @@ const LineChartComponent = ({ return; } - const startIndex = data.findIndex(point => point.time === selectionArea.start); - const endIndex = data.findIndex(point => point.time === selectionArea.end); + const startIndex = data.findIndex(point => point.timestamp === selectionArea.start); + const endIndex = data.findIndex(point => point.timestamp === selectionArea.end); if (startIndex >= 0 && endIndex >= 0) { onRangeSelect({ @@ -78,6 +87,9 @@ const LineChartComponent = ({ const CustomTooltip = ({ active, payload, label }) => { if (active && payload && payload.length) { + const currentPoint = data.find(point => point.timestamp === label); + const displayTime = currentPoint?.fullTime || new Date(label).toLocaleString(); + return (
-

{`${label}`}

+

{displayTime}

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

{`Значение: ${item.value}`} @@ -98,6 +110,7 @@ const LineChartComponent = ({ return null; }; + if (!data.length) { return

Нет данных для отображения
; } @@ -128,9 +141,21 @@ const LineChartComponent = ({ > { + const point = data.find(p => p.timestamp === timestamp); + return point?.fullTime || new Date(timestamp).toLocaleString('ru-RU', { + day: '2-digit', + month: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); + }} /> + } /> {Object.keys(chartData).map((instance, index) => ( @@ -152,6 +177,7 @@ const LineChartComponent = ({ fill="#4a6baf" /> )} +
diff --git a/src/Charts/PrometheusChart.jsx b/src/Charts/PrometheusChart.jsx index ab30858..dce480d 100755 --- a/src/Charts/PrometheusChart.jsx +++ b/src/Charts/PrometheusChart.jsx @@ -22,20 +22,21 @@ const PrometheusChart = ({ metricName }) => { const formatTime = useCallback((timestamp, rangeSeconds) => { const date = new Date(timestamp); - if (rangeSeconds > 86400) { - return { - display: date.toLocaleString([], { - day: '2-digit', - month: '2-digit', - year: '2-digit', - hour: '2-digit', - minute: '2-digit' - }), - timestamp: timestamp - }; - } + const showDate = rangeSeconds > 86400; // Показывать дату если диапазон > 24 часов + return { - display: date.toLocaleTimeString([], { + display: date.toLocaleString([], { + + month: showDate ? '2-digit' : undefined, + day: showDate ? '2-digit' : undefined, + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }), + fullDisplay: date.toLocaleString([], { + + month: '2-digit', + day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' @@ -53,9 +54,9 @@ const PrometheusChart = ({ metricName }) => { }, []); const fetchData = useCallback(() => { - + if (isSelectingRange) return; - + const now = Math.floor(Date.now() / 1000); const start = now - selectedRange.value; const end = now; @@ -70,7 +71,7 @@ const PrometheusChart = ({ metricName }) => { _t: Date.now() }); } - }, [metricName, selectedRange.value, isSelectingRange]); + }, [metricName, selectedRange.value, isSelectingRange]); const groupBySecond = (points) => { const grouped = []; @@ -106,10 +107,21 @@ const PrometheusChart = ({ metricName }) => { const timestamp = item.timestamp > 1e12 ? item.timestamp : item.timestamp * 1000; const value = parseFloat(item.value); + const formattedTime = formatTime(timestamp, selectedRange.value); - if (!newData[instance].some(p => p.timestamp === timestamp)) { + const existingPointIndex = newData[instance].findIndex(p => p.timestamp === timestamp); + + if (existingPointIndex >= 0) { + newData[instance][existingPointIndex] = { + time: formattedTime.display, + fullTime: formattedTime.fullDisplay, + value: value, + timestamp: timestamp + }; + } else { newData[instance].push({ - time: formatTime(timestamp, selectedRange.value).display, + time: formattedTime.display, + fullTime: formattedTime.fullDisplay, value: value, timestamp: timestamp }); @@ -117,7 +129,7 @@ const PrometheusChart = ({ metricName }) => { }); Object.keys(newData).forEach(instance => { - newData[instance] = groupBySecond(newData[instance]) + newData[instance] = newData[instance] .sort((a, b) => a.timestamp - b.timestamp) .slice(-1000); }); @@ -179,7 +191,7 @@ const PrometheusChart = ({ metricName }) => { const start = Math.floor(startDate.getTime() / 1000); const end = Math.floor(endDate.getTime() / 1000); const step = calculateStep(start, end); - + try { const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/metrics`, { params: { @@ -189,7 +201,7 @@ const PrometheusChart = ({ metricName }) => { step } }); - + if (response.data) { // Изменили условие, так как бэкенд возвращает массив напрямую processMetricsData({ metric: metricName, @@ -209,17 +221,17 @@ const PrometheusChart = ({ metricName }) => { const handleRangeChange = useCallback((event) => { 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(); @@ -236,7 +248,7 @@ const PrometheusChart = ({ metricName }) => { const handleResetZoom = useCallback(() => { setSelectedGraphRange(null); setFilteredData(null); - setIsSelectingRange(false); + setIsSelectingRange(false); fetchData(); }, [fetchData]); @@ -245,7 +257,7 @@ const PrometheusChart = ({ metricName }) => { // Начало выделения - останавливаем обновления setIsSelectingRange(true); setSelectedGraphRange(range); - + // Отключаем сокет if (socketRef.current?.connected) { socketRef.current.disconnect(); @@ -258,7 +270,7 @@ const PrometheusChart = ({ metricName }) => { } else { // Окончание выделения - возобновляем соединение setIsSelectingRange(false); - + if (!useCustomRange && socketRef.current && !socketRef.current.connected) { socketRef.current.connect(); } @@ -274,16 +286,8 @@ const PrometheusChart = ({ metricName }) => { }, [setupWebSocket]); useEffect(() => { - if (useCustomRange) { - if (socketRef.current?.connected) { - socketRef.current.disconnect(); - } - fetchCustomRangeData(); - return; - } - - if (!socketRef.current?.connected || isSelectingRange) return; - + if (useCustomRange || isSelectingRange) return; + const fetchDataWrapper = () => { try { fetchData(); @@ -291,17 +295,22 @@ const PrometheusChart = ({ metricName }) => { 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); - intervalRef.current = null; } }; - }, [fetchData, fetchCustomRangeData, selectedRange.interval, useCustomRange, isSelectingRange]); + }, [fetchData, selectedRange.interval, useCustomRange, isSelectingRange]); useEffect(() => { if (!chartData || !selectedGraphRange) { -- 2.40.1 From b6b3b36f5a6f2c549c11ed218f861fd28391af04 Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Wed, 9 Apr 2025 07:47:51 -0400 Subject: [PATCH 09/13] fixed data interpolation and range allocation --- src/Charts/Components/LineChartComponent.jsx | 132 ++++--- src/Charts/PrometheusChart.jsx | 347 ++++++++++++------- src/Components/TreeChart/tabContent.jsx | 2 - src/Components/hooks/TabContent.jsx | 1 - 4 files changed, 312 insertions(+), 170 deletions(-) diff --git a/src/Charts/Components/LineChartComponent.jsx b/src/Charts/Components/LineChartComponent.jsx index 1973bac..caca080 100755 --- a/src/Charts/Components/LineChartComponent.jsx +++ b/src/Charts/Components/LineChartComponent.jsx @@ -11,7 +11,6 @@ const LineChartComponent = ({ const [selectionArea, setSelectionArea] = useState(null); const [isSelecting, setIsSelecting] = useState(false); const chartRef = useRef(null); - const containerRef = useRef(null); const allTimestamps = Object.values(chartData) .flat() @@ -39,9 +38,32 @@ const LineChartComponent = ({ return point; }); - const displayData = filteredData || data; + const instanceKeys = displayData.length + ? Object.keys(displayData[0]).filter(k => !['timestamp', 'time', 'fullTime'].includes(k)) + : []; + + // Функция для определения оптимального формата времени в зависимости от диапазона + const getTimeFormat = () => { + if (!data.length) return 'HH:mm:ss'; + + const first = data[0].timestamp; + const last = data[data.length - 1].timestamp; + const range = last - first; + + // Если диапазон больше 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) { @@ -49,47 +71,66 @@ const LineChartComponent = ({ } }; + document.addEventListener('selectstart', handleSelectStart); return () => document.removeEventListener('selectstart', handleSelectStart); }, [isSelecting]); const handleMouseDown = (e) => { - if (!e || !e.activeLabel) return; + if (!e) return; + + // Получаем индекс точки по координатам + const activeIndex = e.activeTooltipIndex; + if (activeIndex === undefined || activeIndex < 0 || activeIndex >= data.length) return; + setIsSelecting(true); - setSelectionArea({ start: e.activeLabel, end: null }); // activeLabel — это timestamp + setSelectionArea({ + start: data[activeIndex].timestamp, + end: null, + startIndex: activeIndex, + endIndex: null + }); }; const handleMouseMove = (e) => { - if (!selectionArea?.start || !e?.activeLabel) return; - setSelectionArea(prev => ({ ...prev, end: e.activeLabel })); + 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 = () => { - setIsSelecting(false); - - if (!selectionArea?.start || !selectionArea?.end) { + if (!isSelecting || !selectionArea?.start || !selectionArea?.end) { + setIsSelecting(false); setSelectionArea(null); return; } - const startIndex = data.findIndex(point => point.timestamp === selectionArea.start); - const endIndex = data.findIndex(point => point.timestamp === selectionArea.end); + const startIndex = Math.min(selectionArea.startIndex, selectionArea.endIndex); + const endIndex = Math.max(selectionArea.startIndex, selectionArea.endIndex); - if (startIndex >= 0 && endIndex >= 0) { - onRangeSelect({ - startIndex: Math.min(startIndex, endIndex), - endIndex: Math.max(startIndex, endIndex) - }); - } + // Нормализуем индексы к диапазону [0, 1] для родительского компонента + const normalizedStart = startIndex / (data.length - 1); + const normalizedEnd = endIndex / (data.length - 1); + onRangeSelect({ + startIndex: normalizedStart, + endIndex: normalizedEnd + }); + + setIsSelecting(false); setSelectionArea(null); }; const CustomTooltip = ({ active, payload, label }) => { if (active && payload && payload.length) { const currentPoint = data.find(point => point.timestamp === label); - const displayTime = currentPoint?.fullTime || new Date(label).toLocaleString(); - return (
-

{displayTime}

+

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

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

{`Значение: ${item.value}`} @@ -110,26 +153,12 @@ const LineChartComponent = ({ return null; }; - if (!data.length) { return

Нет данных для отображения
; } return ( -
- - +
{ - const point = data.find(p => p.timestamp === timestamp); - return point?.fullTime || new Date(timestamp).toLocaleString('ru-RU', { - day: '2-digit', - month: '2-digit', - hour: '2-digit', - minute: '2-digit' - }); + 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' + }); + } }} /> } /> - {Object.keys(chartData).map((instance, index) => ( + {instanceKeys.map((instance, index) => ( )} -
diff --git a/src/Charts/PrometheusChart.jsx b/src/Charts/PrometheusChart.jsx index dce480d..991a606 100755 --- a/src/Charts/PrometheusChart.jsx +++ b/src/Charts/PrometheusChart.jsx @@ -17,40 +17,57 @@ const PrometheusChart = ({ metricName }) => { 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 formatTime = useCallback((timestamp, rangeSeconds) => { - const date = new Date(timestamp); - const showDate = rangeSeconds > 86400; // Показывать дату если диапазон > 24 часов - + const ts = typeof timestamp === 'number' ? timestamp : Date.now(); + const date = new Date(ts); + + // Определяем формат в зависимости от диапазона + const showFullDate = rangeSeconds > 86400; // больше суток + + const timeOptions = { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false + }; + + const dateOptions = showFullDate ? { + month: '2-digit', + day: '2-digit', + ...timeOptions + } : timeOptions; + return { - display: date.toLocaleString([], { - - month: showDate ? '2-digit' : undefined, - day: showDate ? '2-digit' : undefined, - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }), - fullDisplay: date.toLocaleString([], { - + 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' + second: '2-digit', + hour12: false }), - timestamp: timestamp + timestamp: ts }; }, []); const calculateStep = useCallback((start, end) => { const range = end - start; - if (range <= 3600) return 5; - if (range <= 21600) return 30; - if (range <= 86400) return 120; - return 300; + 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(() => { @@ -73,70 +90,52 @@ const PrometheusChart = ({ metricName }) => { } }, [metricName, selectedRange.value, isSelectingRange]); - const groupBySecond = (points) => { - const grouped = []; - const timeMap = {}; - - points.forEach(point => { - const timeKey = Math.floor(point.timestamp / 1000); - if (!timeMap[timeKey]) { - timeMap[timeKey] = { ...point, count: 1 }; - grouped.push(timeMap[timeKey]); - } else { - timeMap[timeKey].value = (timeMap[timeKey].value * timeMap[timeKey].count + point.value) / - (timeMap[timeKey].count + 1); - timeMap[timeKey].count += 1; - } - }); - - return grouped; - }; - 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] = []; - - const timestamp = item.timestamp > 1e12 ? item.timestamp : item.timestamp * 1000; - const value = parseFloat(item.value); - const formattedTime = formatTime(timestamp, selectedRange.value); - - const existingPointIndex = newData[instance].findIndex(p => p.timestamp === timestamp); - - if (existingPointIndex >= 0) { - newData[instance][existingPointIndex] = { - time: formattedTime.display, - fullTime: formattedTime.fullDisplay, - value: value, - timestamp: timestamp - }; + + // Унифицированная конвертация timestamp + let timestamp; + if (typeof item.timestamp === 'number') { + // Определяем, в секундах или миллисекундах пришел timestamp + timestamp = item.timestamp > 1e12 ? item.timestamp : item.timestamp * 1000; } else { - newData[instance].push({ - time: formattedTime.display, - fullTime: formattedTime.fullDisplay, - value: value, - timestamp: timestamp - }); + 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 + }); }); - + + // Сортируем и ограничиваем данные Object.keys(newData).forEach(instance => { newData[instance] = newData[instance] .sort((a, b) => a.timestamp - b.timestamp) .slice(-1000); }); - - return Object.keys(newData).length ? newData : prev; + return newData; }); - }, [metricName, selectedRange.value, formatTime]); + }, [metricName, selectedRange.value, formatTime, useCustomRange, startDate, endDate]); const setupWebSocket = useCallback(() => { if (socketRef.current) { @@ -190,7 +189,7 @@ const PrometheusChart = ({ metricName }) => { const fetchCustomRangeData = useCallback(async () => { const start = Math.floor(startDate.getTime() / 1000); const end = Math.floor(endDate.getTime() / 1000); - const step = calculateStep(start, end); + const rangeSeconds = end - start; try { const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/metrics`, { @@ -198,18 +197,21 @@ const PrometheusChart = ({ metricName }) => { metric: metricName, start, end, - step + step: calculateStep(start, end) } }); - if (response.data) { // Изменили условие, так как бэкенд возвращает массив напрямую + if (response.data?.length) { + // Преобразуем данные перед передачей в processMetricsData + const processedData = response.data.map(item => ({ + ...item, + timestamp: item.timestamp, // оставляем в секундах - processMetricsData конвертирует + value: item.value.toString() + })); + processMetricsData({ metric: metricName, - data: response.data.map(item => ({ - ...item, - timestamp: item.timestamp / 1000, // или item.timestamp если уже в секундах - value: item.value.toString() // преобразуем в строку, как ожидает processMetricsData - })) + data: processedData }); } } catch (error) { @@ -219,6 +221,12 @@ const PrometheusChart = ({ metricName }) => { 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)); @@ -232,50 +240,131 @@ const PrometheusChart = ({ metricName }) => { 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); - fetchData(); - }, [fetchData]); + + 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]; + } + }); + + interpolated.push(interpolatedPoint); + } + + return interpolated; + }, []); const handleRangeSelect = useCallback((range) => { - if (range) { - // Начало выделения - останавливаем обновления - setIsSelectingRange(true); - setSelectedGraphRange(range); + setLastCustomRange(range); + if (!range || !chartData) return; - // Отключаем сокет - if (socketRef.current?.connected) { - socketRef.current.disconnect(); - } - // Очищаем интервал - if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } - } else { - // Окончание выделения - возобновляем соединение - setIsSelectingRange(false); + setIsSelectingRange(true); + setSelectedGraphRange(range); - if (!useCustomRange && socketRef.current && !socketRef.current.connected) { - socketRef.current.connect(); - } + // Отключаем автоматические обновления + if (socketRef.current?.connected) { + socketRef.current.disconnect(); } - }, [useCustomRange]); + 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(() => { const socket = setupWebSocket(); @@ -285,9 +374,30 @@ const PrometheusChart = ({ metricName }) => { }; }, [setupWebSocket]); + // Обновим 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(); @@ -295,16 +405,16 @@ const PrometheusChart = ({ metricName }) => { 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); @@ -313,33 +423,24 @@ const PrometheusChart = ({ metricName }) => { }, [fetchData, selectedRange.interval, useCustomRange, isSelectingRange]); useEffect(() => { - if (!chartData || !selectedGraphRange) { + if (!selectedGraphRange || !chartData) { setFilteredData(null); return; } - const { startIndex, endIndex } = selectedGraphRange; - const allTimes = Object.values(chartData) - .flat() - .map(point => point.time) - .filter((time, index, self) => self.indexOf(time) === index); + const allPoints = Object.values(chartData).flat(); + const sortedPoints = allPoints.sort((a, b) => a.timestamp - b.timestamp); - 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; - }).sort((a, b) => { - const timeA = chartData[Object.keys(chartData)[0]].find(d => d.time === a.time)?.timestamp; - const timeB = chartData[Object.keys(chartData)[0]].find(d => d.time === b.time)?.timestamp; - return timeA - timeB; - }); + const startIndex = Math.floor(selectedGraphRange.startIndex * (sortedPoints.length - 1)); + const endIndex = Math.floor(selectedGraphRange.endIndex * (sortedPoints.length - 1)); - const filtered = data.slice(startIndex, endIndex + 1); - setFilteredData(filtered); - }, [selectedGraphRange, chartData]); + 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...
; @@ -382,7 +483,7 @@ const PrometheusChart = ({ metricName }) => { chartData={chartData} metricName={metricName} colors={COLORS} - onRangeSelect={handleRangeSelect} // Используем модифицированный обработчик + onRangeSelect={handleRangeSelect} filteredData={filteredData} />
diff --git a/src/Components/TreeChart/tabContent.jsx b/src/Components/TreeChart/tabContent.jsx index 58e6e49..c5353f5 100755 --- a/src/Components/TreeChart/tabContent.jsx +++ b/src/Components/TreeChart/tabContent.jsx @@ -7,8 +7,6 @@ const getMetricName = (id) => { return `zvks_apiforsnmp_measure_${id}`; }; -//!!!!!!!!!!Пофиксить вкладуи с eth4, во всех eth 1-4 открывается именно 4 !!!!!!!!!!!!! - // Функция для рекурсивного сбора всех id потомков const getAllChildIds = (node) => { let ids = []; 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 }) => { -- 2.40.1 From e56b82bb6661bcc81c50e1ba6d27f820bba3ef6c Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Wed, 9 Apr 2025 09:15:25 -0400 Subject: [PATCH 10/13] optimized chart loading --- src/Charts/PrometheusChart.jsx | 3 +- src/Components/TreeChart/tabContent.jsx | 2 ++ src/Components/hooks/LazyChartBatchRender.jsx | 29 +++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 src/Components/hooks/LazyChartBatchRender.jsx diff --git a/src/Charts/PrometheusChart.jsx b/src/Charts/PrometheusChart.jsx index 991a606..0553f82 100755 --- a/src/Charts/PrometheusChart.jsx +++ b/src/Charts/PrometheusChart.jsx @@ -490,4 +490,5 @@ const PrometheusChart = ({ metricName }) => { ); }; -export default React.memo(PrometheusChart); \ No newline at end of file +export default React.memo(PrometheusChart); + diff --git a/src/Components/TreeChart/tabContent.jsx b/src/Components/TreeChart/tabContent.jsx index c5353f5..4072401 100755 --- a/src/Components/TreeChart/tabContent.jsx +++ b/src/Components/TreeChart/tabContent.jsx @@ -1,6 +1,7 @@ import React, { lazy, Suspense } from "react"; const PrometheusChart = lazy(() => import('../../Charts/PrometheusChart')); +import LazyChartBatchRenderer from "../hooks/LazyChartBatchRender"; // Функция для генерации названия метрики на основе id const getMetricName = (id) => { @@ -35,6 +36,7 @@ const tabContent = (data) => { const content = (

{node.title}

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

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

{childrenContent}
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; -- 2.40.1 From 5b258760560da33200b8320fec0960f62e499bdd Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Wed, 9 Apr 2025 10:00:44 -0400 Subject: [PATCH 11/13] added the api prefix --- src/Charts/PrometheusChart.jsx | 4 ++-- src/Components/UI/LoginModal.jsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Charts/PrometheusChart.jsx b/src/Charts/PrometheusChart.jsx index 0553f82..3d38a8a 100755 --- a/src/Charts/PrometheusChart.jsx +++ b/src/Charts/PrometheusChart.jsx @@ -145,7 +145,7 @@ const PrometheusChart = ({ metricName }) => { if (socketRef.current.reconnecting) return socketRef.current; } - const socket = io('http://192.168.2.39:3000/metrics-ws', { + const socket = io('http://192.168.2.39:3000/api/metrics-ws', { transports: ['websocket'], reconnection: true, reconnectionAttempts: Infinity, @@ -192,7 +192,7 @@ const PrometheusChart = ({ metricName }) => { const rangeSeconds = end - start; try { - 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, 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', -- 2.40.1 From 858f6e2c4c7abca913eff52b81a7e3fe1ea26eaf Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Wed, 9 Apr 2025 10:17:45 -0400 Subject: [PATCH 12/13] added an environment variable --- src/Charts/PrometheusChart.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Charts/PrometheusChart.jsx b/src/Charts/PrometheusChart.jsx index 3d38a8a..74641cd 100755 --- a/src/Charts/PrometheusChart.jsx +++ b/src/Charts/PrometheusChart.jsx @@ -144,8 +144,8 @@ const PrometheusChart = ({ metricName }) => { // Если соединение в процессе переподключения, тоже возвращаем if (socketRef.current.reconnecting) return socketRef.current; } - - const socket = io('http://192.168.2.39:3000/api/metrics-ws', { + //VITE_BACK_WS_URL + const socket = io(`${import.meta.env.VITE_BACK_WS_URL}/api/metrics-ws`, { transports: ['websocket'], reconnection: true, reconnectionAttempts: Infinity, -- 2.40.1 From 22c5fcf02c535e1de3f7cd3af5a3251695e830c3 Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Thu, 10 Apr 2025 05:33:15 -0400 Subject: [PATCH 13/13] Converted time to constants --- src/Charts/Components/LineChartComponent.jsx | 26 +++++++++----------- src/Charts/Components/constants.jsx | 17 ++++++++++++- src/Charts/PrometheusChart.jsx | 18 +++++++------- 3 files changed, 37 insertions(+), 24 deletions(-) diff --git a/src/Charts/Components/LineChartComponent.jsx b/src/Charts/Components/LineChartComponent.jsx index caca080..3c548ac 100755 --- a/src/Charts/Components/LineChartComponent.jsx +++ b/src/Charts/Components/LineChartComponent.jsx @@ -1,5 +1,11 @@ import React, { useState, useRef, useEffect } from 'react'; import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Line, ResponsiveContainer, ReferenceArea } from 'recharts'; +import { HOUR, DAY } from './constants'; +const TIME_FORMATS = { + LONG: 'dd.MM HH:mm', // Для диапазона > 24 часов + MEDIUM: 'HH:mm', // Для диапазона > 1 часа + SHORT: 'HH:mm:ss' // Для коротких диапазонов +}; const LineChartComponent = ({ chartData, @@ -12,6 +18,7 @@ const LineChartComponent = ({ const [isSelecting, setIsSelecting] = useState(false); const chartRef = useRef(null); + const allTimestamps = Object.values(chartData) .flat() .map(point => point.timestamp) @@ -46,22 +53,13 @@ const LineChartComponent = ({ // Функция для определения оптимального формата времени в зависимости от диапазона const getTimeFormat = () => { - if (!data.length) return 'HH:mm:ss'; + if (!data.length) return TIME_FORMATS.SHORT; - const first = data[0].timestamp; - const last = data[data.length - 1].timestamp; - const range = last - first; + const range = data[data.length - 1].timestamp - data[0].timestamp; - // Если диапазон больше 24 часов - показываем дату - if (range > 86400000) { - return 'dd.MM HH:mm'; - } - // Если больше 1 часа - показываем часы и минуты - if (range > 3600000) { - return 'HH:mm'; - } - // Для коротких диапазонов - показываем время с секундами - return 'HH:mm:ss'; + if (range > DAY) return TIME_FORMATS.LONG; + if (range > HOUR) return TIME_FORMATS.MEDIUM; + return TIME_FORMATS.SHORT; }; useEffect(() => { diff --git a/src/Charts/Components/constants.jsx b/src/Charts/Components/constants.jsx index 68f92d9..a3b621a 100644 --- a/src/Charts/Components/constants.jsx +++ b/src/Charts/Components/constants.jsx @@ -17,4 +17,19 @@ export const TIME_RANGES = [ ]; export const COLORS = ['#3e95cd', '#8e5ea2', '#3cba9f', '#e8c3b9', '#c45850']; -export const MAX_POINTS = 20; \ No newline at end of file +export const MAX_POINTS = 20; + + +// Для работы с временными интервалами (setTimeout и т.д.) +export const MS = 1; +export const SECOND_MS = 1000 * MS; +export const MINUTE_MS = 60 * SECOND_MS; +export const HOUR_MS = 60 * MINUTE_MS; +export const DAY_MS = 24 * HOUR_MS; + +// Для работы с Unix-временем и API (Prometheus и т.д.) +export const SECOND = 1; +export const MINUTE = 60 * SECOND; +export const HOUR = 60 * MINUTE; +export const DAY = 24 * HOUR; +export const WEEK = 7 * DAY; \ No newline at end of file diff --git a/src/Charts/PrometheusChart.jsx b/src/Charts/PrometheusChart.jsx index 74641cd..04c9e20 100755 --- a/src/Charts/PrometheusChart.jsx +++ b/src/Charts/PrometheusChart.jsx @@ -4,7 +4,7 @@ import LineChartComponent from './Components/LineChartComponent'; import { TimeRangeSelector } from './Components/TimeRangeSelector'; import { ConnectionStatusIndicator } from './Components/ConnectionStatusIndicator'; import { CurrentRangeDisplay } from './Components/CurrentRangeDisplay'; -import { TIME_RANGES, COLORS } from './Components/constants'; +import { TIME_RANGES, COLORS, SECOND, MINUTE, HOUR, DAY } from './Components/constants'; import axios from 'axios'; const PrometheusChart = ({ metricName }) => { @@ -59,14 +59,14 @@ const PrometheusChart = ({ metricName }) => { 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 часа + if (range <= MINUTE) return 1; // 1 мин + if (range <= MINUTE * 5) return 5; // 5 мин + if (range <= HOUR / 2) return 15; // 30 мин + if (range <= HOUR) return 30; // 1 час + if (range <= HOUR * 3) return 60; // 3 часа + if (range <= HOUR * 6) return 120; // 6 часов + if (range <= DAY / 2) return 300; // 12 часов + if (range <= DAY) return 600; // 24 часа return 1800; // > 24 часов }, []); -- 2.40.1