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",