From d7c40ee04b7019162b86c72f806c70ceb02e7508 Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Wed, 30 Jul 2025 18:35:19 -0400 Subject: [PATCH] added real data on the main page --- Dockerfile | 6 +- src/Charts/Components/BarChartComponent.jsx | 45 -- src/Charts/Components/ChartSkeleton.jsx | 26 - .../Components/ConnectionStatusIndicator.jsx | 20 - src/Charts/Components/CounterComponent.jsx | 12 - src/Charts/Components/CurrentRangeDisplay.jsx | 17 - src/Charts/Components/LineChartComponent.jsx | 239 --------- .../Components/ScatterChartComponent.jsx | 29 - src/Charts/Components/TimeRangeSelector.jsx | 151 ------ src/Charts/Components/constants.jsx | 35 -- .../Components/hooks/useMetricsData.jsx | 0 .../Components/hooks/useTimeHandlers.jsx | 0 src/Charts/Components/hooks/useWebSocket.jsx | 0 src/Charts/LineChartComponent.jsx | 115 ++++ src/Charts/PrometheusChart.jsx | 499 ------------------ src/Charts/SystemChart.jsx | 283 ++++++++++ src/Charts/SystemStatusChart.jsx | 42 -- src/Charts/WebSocketManager.jsx | 121 ----- src/Charts2/Components/LineChartComponent.jsx | 225 +++++--- src/Charts2/PrometheusChart.jsx | 77 ++- src/Components/Layout/Dashboard.jsx | 7 +- .../SidebarMenuComponents/SidebarFooter.jsx | 4 +- src/Components/UI/AIAnalysisButton.jsx | 133 +++++ src/Components/UI/RoleBasedRender.jsx | 12 +- src/Components/UI/auth.jsx | 2 + src/Components/hooks/TabContent.jsx | 46 +- vite.config.js | 10 +- 27 files changed, 797 insertions(+), 1359 deletions(-) delete mode 100755 src/Charts/Components/BarChartComponent.jsx delete mode 100644 src/Charts/Components/ChartSkeleton.jsx delete mode 100644 src/Charts/Components/ConnectionStatusIndicator.jsx delete mode 100755 src/Charts/Components/CounterComponent.jsx delete mode 100644 src/Charts/Components/CurrentRangeDisplay.jsx delete mode 100755 src/Charts/Components/LineChartComponent.jsx delete mode 100755 src/Charts/Components/ScatterChartComponent.jsx delete mode 100644 src/Charts/Components/TimeRangeSelector.jsx delete mode 100644 src/Charts/Components/constants.jsx delete mode 100644 src/Charts/Components/hooks/useMetricsData.jsx delete mode 100644 src/Charts/Components/hooks/useTimeHandlers.jsx delete mode 100644 src/Charts/Components/hooks/useWebSocket.jsx create mode 100644 src/Charts/LineChartComponent.jsx delete mode 100755 src/Charts/PrometheusChart.jsx create mode 100644 src/Charts/SystemChart.jsx delete mode 100755 src/Charts/SystemStatusChart.jsx delete mode 100644 src/Charts/WebSocketManager.jsx create mode 100644 src/Components/UI/AIAnalysisButton.jsx diff --git a/Dockerfile b/Dockerfile index 8d37d6c..d5d7d39 100755 --- a/Dockerfile +++ b/Dockerfile @@ -2,10 +2,10 @@ FROM node:22.13.0 WORKDIR /app -COPY package.json package-lock.json vite.config.js eslint.config.js ./ - -RUN npm install +COPY package.json package-lock.json ./ +RUN npm install --verbose +COPY vite.config.js eslint.config.js ./ COPY . . ENTRYPOINT ["npm", "run", "dev"] \ No newline at end of file diff --git a/src/Charts/Components/BarChartComponent.jsx b/src/Charts/Components/BarChartComponent.jsx deleted file mode 100755 index b0d425c..0000000 --- a/src/Charts/Components/BarChartComponent.jsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import { BarChart, XAxis, YAxis, CartesianGrid, Tooltip, Legend, Bar, ResponsiveContainer } from 'recharts'; - -const BarChartComponent = ({ chartData, metricName, metricType, colors }) => { - // Преобразуем данные для отображения - const data = Object.keys(chartData).map(instance => { - const instanceData = chartData[instance].reduce((acc, point) => { - if (point.value !== null) { - acc[point.quantile] = point.value; - } - return acc; - }, {}); - return { instance, ...instanceData }; - }); - - // Получаем все уникальные квантили - const allQuantiles = [...new Set( - Object.values(chartData).flat().map(point => point.quantile) - )]; - - return ( -
-

{metricName} ({metricType})

- - - - - - - - {allQuantiles.map((quantile, index) => ( - - ))} - - -
- ); -}; - -export default BarChartComponent; \ No newline at end of file diff --git a/src/Charts/Components/ChartSkeleton.jsx b/src/Charts/Components/ChartSkeleton.jsx deleted file mode 100644 index 090f1e7..0000000 --- a/src/Charts/Components/ChartSkeleton.jsx +++ /dev/null @@ -1,26 +0,0 @@ -const ChartSkeleton = () => ( - - - - - - - - - - - - - - {[1, 2, 3, 4].map((_, i) => ( - - ))} - - -); \ No newline at end of file diff --git a/src/Charts/Components/ConnectionStatusIndicator.jsx b/src/Charts/Components/ConnectionStatusIndicator.jsx deleted file mode 100644 index 941fed2..0000000 --- a/src/Charts/Components/ConnectionStatusIndicator.jsx +++ /dev/null @@ -1,20 +0,0 @@ -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/CounterComponent.jsx b/src/Charts/Components/CounterComponent.jsx deleted file mode 100755 index 209076a..0000000 --- a/src/Charts/Components/CounterComponent.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; - -const CounterComponent = ({ value, metricName }) => { - return ( -
-

{metricName}

-

{value}

-
- ); -}; - -export default CounterComponent; \ No newline at end of file diff --git a/src/Charts/Components/CurrentRangeDisplay.jsx b/src/Charts/Components/CurrentRangeDisplay.jsx deleted file mode 100644 index 41cfb4c..0000000 --- a/src/Charts/Components/CurrentRangeDisplay.jsx +++ /dev/null @@ -1,17 +0,0 @@ -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 deleted file mode 100755 index 149f1fb..0000000 --- a/src/Charts/Components/LineChartComponent.jsx +++ /dev/null @@ -1,239 +0,0 @@ -import React, { useState, useRef, useEffect } from 'react'; -import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Line, ResponsiveContainer, ReferenceArea } from 'recharts'; -import { Skeleton } from '@mui/material'; -import { HOUR, DAY } from './constants'; - -const TIME_FORMATS = { - LONG: 'dd.MM HH:mm', // Для диапазона > 24 часов - MEDIUM: 'HH:mm', // Для диапазона > 1 часа - SHORT: 'HH:mm:ss' // Для коротких диапазонов -}; - -const LineChartComponent = ({ - chartData, - metricName, - colors, - onRangeSelect, - filteredData -}) => { - const [selectionArea, setSelectionArea] = useState(null); - const [isSelecting, setIsSelecting] = useState(false); - const chartRef = useRef(null); - - - const allTimestamps = Object.values(chartData) - .flat() - .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; - } - - Object.keys(chartData).forEach(key => { - const instanceData = chartData[key].find(p => p.timestamp === timestamp); - point[key] = instanceData ? instanceData.value : null; - }); - - 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 TIME_FORMATS.SHORT; - - const range = data[data.length - 1].timestamp - data[0].timestamp; - - if (range > DAY) return TIME_FORMATS.LONG; - if (range > HOUR) return TIME_FORMATS.MEDIUM; - return TIME_FORMATS.SHORT; - }; - - useEffect(() => { - const handleSelectStart = (e) => { - if (isSelecting) { - e.preventDefault(); - } - }; - - - document.addEventListener('selectstart', handleSelectStart); - return () => document.removeEventListener('selectstart', handleSelectStart); - }, [isSelecting]); - - const handleMouseDown = (e) => { - if (!e) return; - - const activeIndex = e.activeTooltipIndex; - if (activeIndex === undefined || activeIndex < 0 || activeIndex >= data.length) return; - - setIsSelecting(true); - setSelectionArea({ - start: data[activeIndex].timestamp, - end: null, - startIndex: activeIndex, - endIndex: null - }); - }; - - const handleMouseMove = (e) => { - if (!isSelecting || !selectionArea?.start || !e) return; - - const activeIndex = e.activeTooltipIndex; - if (activeIndex === undefined || activeIndex < 0 || activeIndex >= data.length) return; - - setSelectionArea(prev => ({ - ...prev, - end: data[activeIndex].timestamp, - endIndex: activeIndex - })); - }; - - const handleMouseUp = () => { - if (!isSelecting || !selectionArea?.start || !selectionArea?.end) { - setIsSelecting(false); - setSelectionArea(null); - return; - } - - const startIndex = Math.min(selectionArea.startIndex, selectionArea.endIndex); - const endIndex = Math.max(selectionArea.startIndex, selectionArea.endIndex); - - 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); - return ( -
-

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

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

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

- ))} -
- ); - } - return null; - }; - - if (!data.length) { - return ( - - - - - ); - } - - return ( -
- - - - { - const date = new Date(timestamp); - const format = getTimeFormat(); - - if (format === 'dd.MM HH:mm') { - return date.toLocaleString('ru-RU', { - day: '2-digit', - month: '2-digit', - hour: '2-digit', - minute: '2-digit' - }); - } else if (format === 'HH:mm') { - return date.toLocaleString('ru-RU', { - hour: '2-digit', - minute: '2-digit' - }); - } else { - return date.toLocaleString('ru-RU', { - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }); - } - }} - /> - - - } /> - {instanceKeys.map((instance, index) => ( - - ))} - {selectionArea?.start && selectionArea?.end && ( - - )} - - -
- ); -}; - -export default React.memo(LineChartComponent); \ No newline at end of file diff --git a/src/Charts/Components/ScatterChartComponent.jsx b/src/Charts/Components/ScatterChartComponent.jsx deleted file mode 100755 index 15d5e90..0000000 --- a/src/Charts/Components/ScatterChartComponent.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import { ScatterChart, XAxis, YAxis, CartesianGrid, Tooltip, Legend, Scatter, ResponsiveContainer } from 'recharts'; - -const ScatterChartComponent = ({ chartData, metricName, metricType, colors }) => { - return ( -
-

{metricName} ({metricType})

- - - - - - - - {Object.keys(chartData).map((instance, index) => ( - - ))} - - -
- ); -}; - -export default ScatterChartComponent; \ No newline at end of file diff --git a/src/Charts/Components/TimeRangeSelector.jsx b/src/Charts/Components/TimeRangeSelector.jsx deleted file mode 100644 index d5111dc..0000000 --- a/src/Charts/Components/TimeRangeSelector.jsx +++ /dev/null @@ -1,151 +0,0 @@ -import React from 'react'; -import DatePicker from 'react-datepicker'; -import 'react-datepicker/dist/react-datepicker.css'; -import { TIME_RANGES } from './constants'; - -export const TimeRangeSelector = ({ - selectedRange, - handleRangeChange, - startDate, - setStartDate, - endDate, - setEndDate, - useCustomRange, - handleCustomRangeChange, - handleResetZoom -}) => { - return ( -
- {/* Стандартные диапазоны */} -
-
- - -
- - -
- - {/* Кастомный диапазон */} -
-
- Или укажите свой диапазон: -
-
-
- setStartDate(date)} - showTimeSelect - timeFormat="HH:mm" - timeIntervals={15} - dateFormat="yyyy-MM-dd HH:mm" - placeholderText="Начальная дата" - customInput={ - - } - /> -
-
- setEndDate(date)} - showTimeSelect - timeFormat="HH:mm" - timeIntervals={15} - dateFormat="yyyy-MM-dd HH:mm" - placeholderText="Конечная дата" - customInput={ - - } - /> -
- -
-
-
- ); -}; \ No newline at end of file diff --git a/src/Charts/Components/constants.jsx b/src/Charts/Components/constants.jsx deleted file mode 100644 index a3b621a..0000000 --- a/src/Charts/Components/constants.jsx +++ /dev/null @@ -1,35 +0,0 @@ -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; - - -// Для работы с временными интервалами (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/Components/hooks/useMetricsData.jsx b/src/Charts/Components/hooks/useMetricsData.jsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/Charts/Components/hooks/useTimeHandlers.jsx b/src/Charts/Components/hooks/useTimeHandlers.jsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/Charts/Components/hooks/useWebSocket.jsx b/src/Charts/Components/hooks/useWebSocket.jsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/Charts/LineChartComponent.jsx b/src/Charts/LineChartComponent.jsx new file mode 100644 index 0000000..ee9a4e3 --- /dev/null +++ b/src/Charts/LineChartComponent.jsx @@ -0,0 +1,115 @@ +import React from 'react'; +import { + LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, + ResponsiveContainer, ReferenceLine +} from 'recharts'; +import { format } from 'date-fns'; + +const lineColors = { + '18': '#8884d8', + '19': '#82ca9d', + 'default': '#ff8042' +}; + +const formatXAxis = (tickItem) => { + return format(new Date(tickItem), 'HH:mm:ss'); +}; + +const formatTooltip = (value, name, props) => { + return [`${value.toFixed(2)}`, ` ${name}`]; +}; + +const LineChartComponent = ({ + data = [], + multipleLines = false, + lineKey = 'device', + title, + description, + height = 400, + ranges = [] +}) => { + if (!data || data.length === 0) return
Нет данных для отображения
; + + // Создаем массив уникальных линий + const lineKeys = [...new Set(data.map(item => item[lineKey] || 'default'))]; + + // Преобразуем данные в формат, удобный для Recharts + const chartData = data.reduce((acc, item) => { + const timestamp = item.timestamp; + const existingPoint = acc.find(p => p.timestamp === timestamp); + + if (existingPoint) { + return acc.map(p => + p.timestamp === timestamp + ? { ...p, [item[lineKey] || 'default']: item.value } + : p + ); + } + + return [...acc, { + timestamp, + [item[lineKey] || 'default']: item.value + }]; + }, []).sort((a, b) => a.timestamp - b.timestamp); + + return ( +
+

{title}

+ {description &&

{description}

} + + + + + + format(new Date(label), 'yyyy-MM-dd HH:mm:ss')} + /> + + + {multipleLines ? ( + lineKeys.map(key => ( + + )) + ) : ( + + )} + + {/* Добавляем диапазоны если они есть */} + {ranges.map((range, idx) => ( + + ))} + + +
+ ); +}; + +export default LineChartComponent; \ No newline at end of file diff --git a/src/Charts/PrometheusChart.jsx b/src/Charts/PrometheusChart.jsx deleted file mode 100755 index 1a8b20c..0000000 --- a/src/Charts/PrometheusChart.jsx +++ /dev/null @@ -1,499 +0,0 @@ -import React, { useEffect, useState, useRef, useCallback } from 'react'; -import { webSocketManager } from './WebSocketManager'; -import LineChartComponent from './Components/LineChartComponent'; -import { TimeRangeSelector } from './Components/TimeRangeSelector'; -import { ConnectionStatusIndicator } from './Components/ConnectionStatusIndicator'; -import { CurrentRangeDisplay } from './Components/CurrentRangeDisplay'; -import { TIME_RANGES, COLORS, SECOND, MINUTE, HOUR, DAY } from './Components/constants'; -import axios from 'axios'; -import Skeleton from '@mui/material/Skeleton'; -import Box from '@mui/material/Box'; - - -// Компонент Skeleton для графика -const ChartSkeleton = () => ( - - - - - - - - - - - - - - {[1, 2, 3, 4].map((_, i) => ( - - ))} - - -); - -const PrometheusChart = ({ metricName }) => { - const [chartData, setChartData] = useState(null); - 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 [isSelectingRange, setIsSelectingRange] = useState(false); - const [lastCustomRange, setLastCustomRange] = useState(null); - const intervalRef = useRef(null); - const debounceRef = useRef(null); - - const formatTime = useCallback((timestamp, rangeSeconds) => { - 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('ru-RU', dateOptions), - fullDisplay: date.toLocaleString('ru-RU', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: false - }), - timestamp: ts - }; - }, []); - - const calculateStep = useCallback((start, end) => { - const range = end - start; - 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 часов - }, []); - - - const processMetricsData = useCallback((response, replace = false) => { - console.log('Processing metrics data:', response); - if (response.metric !== metricName) return; - - const dataArray = Array.isArray(response.data) ? response.data : [response.data]; - if (!dataArray.length) return; - - const newData = {}; - const rangeSeconds = useCustomRange - ? (endDate.getTime() - startDate.getTime()) / 1000 - : selectedRange.value; - - dataArray.forEach(item => { - const instance = item.instance || 'default'; - if (!newData[instance]) newData[instance] = []; - - let timestamp; - if (typeof item.timestamp === 'number') { - timestamp = item.timestamp > 1e12 ? item.timestamp : item.timestamp * 1000; - } else { - timestamp = Date.now(); - } - - const value = parseFloat(item.value); - const formattedTime = formatTime(timestamp, rangeSeconds); - - newData[instance].push({ - time: formattedTime.display, - fullTime: formattedTime.fullDisplay, - value, - timestamp - }); - }); - - Object.keys(newData).forEach(instance => { - newData[instance] = newData[instance] - .sort((a, b) => a.timestamp - b.timestamp) - .slice(-1000); - }); - - if (replace) { - setChartData(newData); // Заменяем полностью - } else { - setChartData(prev => { - const merged = { ...(prev || {}) }; - Object.keys(newData).forEach(instance => { - if (!merged[instance]) merged[instance] = []; - merged[instance] = [...merged[instance], ...newData[instance]] - .sort((a, b) => a.timestamp - b.timestamp) - .slice(-1000); - }); - return merged; - }); - } - }, [metricName, selectedRange.value, formatTime, useCustomRange, startDate, endDate]); - - - const fetchData = useCallback(() => { - if (isSelectingRange) return; - - const now = Math.floor(Date.now() / 1000); - const start = now - selectedRange.value; - const end = now; - const step = calculateStep(start, end); - - webSocketManager.getMetricsRange(metricName, start, end, step) - .then(data => { - processMetricsData({ metric: metricName, data }); - }) - .catch(error => { - console.error('Error fetching metrics:', error); - }); - }, [metricName, selectedRange.value, isSelectingRange, calculateStep, processMetricsData]); - - const fetchCustomRangeData = useCallback(async () => { - // Добавляем проверку на валидность дат - if (!startDate || !endDate || startDate >= endDate) { - console.error('Invalid date range'); - return; - } - - const start = Math.floor(startDate.getTime() / 1000); - const end = Math.ceil(endDate.getTime() / 1000); // Используем Math.ceil для конечной даты - const rangeSeconds = end - start; - - try { - const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/metrics`, { - params: { - metric: metricName, - start, - end, - step: calculateStep(start, end) - } - }); - - if (response.data?.length) { - // Добавляем нормализацию timestamp - const processedData = response.data.map(item => ({ - ...item, - timestamp: item.timestamp > 1e12 ? item.timestamp : item.timestamp * 1000, - value: parseFloat(item.value) - })); - - processMetricsData({ - metric: metricName, - data: processedData - }, true); - } - } catch (error) { - console.error('Ошибка при получении кастомных данных:', error); - } - }, [metricName, startDate, endDate, calculateStep, processMetricsData]); - - - const handleRangeChange = useCallback(async (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)); - - // Полный сброс состояния перед загрузкой новых данных - setChartData(null); - setSelectedRange(range); - setUseCustomRange(false); - setSelectedGraphRange(null); - setFilteredData(null); - - const now = new Date(); - setEndDate(now); - setStartDate(new Date(now.getTime() - range.value * 1000)); - - // Ждем завершения обновления состояния перед загрузкой - await new Promise(resolve => setTimeout(resolve, 0)); - fetchData(); - }, [fetchData]); - - const handleCustomRangeChange = useCallback(() => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } - - setUseCustomRange(true); - setChartData(null); - setSelectedGraphRange(null); - setFilteredData(null); - fetchCustomRangeData(); - }, [fetchCustomRangeData]); - - - - 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) => { - setLastCustomRange(range); - if (!range || !chartData) return; - - setIsSelectingRange(true); - setSelectedGraphRange(range); - - 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]); - - const handleResetZoom = useCallback(() => { - setSelectedGraphRange(null); - setFilteredData(null); - setIsSelectingRange(false); - - if (useCustomRange) { - fetchCustomRangeData(); - } else { - fetchData(); - } - - if (lastCustomRange) { - handleRangeSelect(lastCustomRange); - } - }, [fetchData, fetchCustomRangeData, useCustomRange, lastCustomRange, handleRangeSelect]); - - useEffect(() => { - // Обработчик данных с сервера - const handleMetricsData = (data) => { - if (!useCustomRange) { - processMetricsData({ metric: metricName, data }); - } - }; - - // Подписываемся на обновления метрики - const unsubscribe = webSocketManager.subscribe(metricName, handleMetricsData); - - // Подписываемся на изменения статуса соединения - const unsubscribeStatus = webSocketManager.onConnectionStatusChange(setConnectionStatus); - - return () => { - // Отписываемся при размонтировании компонента - unsubscribe(); - unsubscribeStatus(); - - // Очищаем интервал обновления - if (intervalRef.current) { - clearInterval(intervalRef.current); - } - }; - }, [metricName, useCustomRange, processMetricsData]); - - - useEffect(() => { - if (useCustomRange && !isSelectingRange) { - if (debounceRef.current) { - clearTimeout(debounceRef.current); - } - - debounceRef.current = setTimeout(() => { - fetchCustomRangeData(); - }, 500); - } - - return () => { - if (debounceRef.current) { - clearTimeout(debounceRef.current); - } - }; - }, [useCustomRange, isSelectingRange, startDate, endDate, fetchCustomRangeData]); - - useEffect(() => { - if (useCustomRange || isSelectingRange) return; - - const fetchDataWrapper = () => { - try { - fetchData(); - } catch (error) { - console.error('Error in interval fetch:', error); - } - }; - - if (intervalRef.current) { - clearInterval(intervalRef.current); - } - - - fetchDataWrapper(); - intervalRef.current = setInterval(fetchDataWrapper, selectedRange.interval); - - return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - } - }; - }, [fetchData, selectedRange.interval, useCustomRange, isSelectingRange]); - - useEffect(() => { - if (!selectedGraphRange || !chartData) { - setFilteredData(null); - return; - } - - const allPoints = Object.values(chartData).flat(); - const sortedPoints = allPoints.sort((a, b) => a.timestamp - b.timestamp); - - const startIndex = Math.floor(selectedGraphRange.startIndex * (sortedPoints.length - 1)); - const endIndex = Math.floor(selectedGraphRange.endIndex * (sortedPoints.length - 1)); - - 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 ; - } - - if (Object.keys(chartData).length === 0) { - return ( - - No data available - - ); - } - - return ( -
- - - - - - - -
- ); -}; - -export default React.memo(PrometheusChart); \ No newline at end of file diff --git a/src/Charts/SystemChart.jsx b/src/Charts/SystemChart.jsx new file mode 100644 index 0000000..8ce0eca --- /dev/null +++ b/src/Charts/SystemChart.jsx @@ -0,0 +1,283 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import LineChartComponent from './LineChartComponent'; +import DateRangeSelector from '../Charts2/Components/DateRangeSelector'; +import metricsService from '../Charts2/Components/metricsService'; +import { Button, Radio, message, Tag, Spin } from 'antd'; +import moment from 'moment'; +import StatusLogTable from '../Charts2/Components/StatusLogTable'; +import { Box, IconButton, Tooltip as MuiTooltip } from '@mui/material'; +import { ListAlt } from '@mui/icons-material'; + +const SystemChart = ({ metricInfo, chartHeight = 580 }) => { + const { + name: metricName, + filters = {}, + title = metricName, + description, + context = {}, + ranges = [], + multipleLines = false, + lineKey = 'device' + } = metricInfo || {}; + + const { device, source_id } = context; + + const [rawData, setRawData] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [metricMeta, setMetricMeta] = useState({}); + const [mode, setMode] = useState('realtime'); + const [startDate, setStartDate] = useState(moment().subtract(1, 'hour').toDate()); + const [endDate, setEndDate] = useState(moment().toDate()); + const [isLiveUpdating, setIsLiveUpdating] = useState(false); + const [showLogs, setShowLogs] = useState(false); + const [statusLogs, setStatusLogs] = useState([]); + + const MAX_POINTS = 1000; + const TIME_WINDOW_MS = 3600 * 1000; + + const subscriptionKey = useMemo(() => { + const filterParts = []; + if (device) filterParts.push(`device=${encodeURIComponent(device)}`); + if (source_id) filterParts.push(`source_id=${encodeURIComponent(source_id)}`); + return `${metricName}${filterParts.length ? `?${filterParts.join('&')}` : ''}`; + }, [metricName, device, source_id]); + + const formatMetricData = (dataArray) => { + return dataArray.map(item => ({ + ...item, + timestamp: item.timestamp, + value: parseFloat(item.value), + name: item.__name__ || metricName, + status: parseInt(item.status) || 0, + device: item.device?.trim() || null, + source_id: item.source_id || null, + description: item.description || description, + lineId: item[lineKey] || 'default' + })); + }; + + const calculateStep = (start, end) => { + const duration = end.getTime() - start.getTime(); + return Math.max(Math.floor(duration / (MAX_POINTS * 1000)), 1); + }; + + const fetchHistoricalData = async (start, end) => { + setIsLoading(true); + setError(null); + + try { + const extendedFilters = { + ...filters, + ...(device && { device: device.toString() }), + ...(source_id && { source_id: source_id.toString() }) + }; + + const step = calculateStep(start, end); + const data = await metricsService.fetchMetricsRange( + metricName, + Math.floor(start.getTime() / 1000), + Math.floor(end.getTime() / 1000), + step, + extendedFilters + ); + + const formattedData = formatMetricData(data); + setRawData(formattedData); + + if (formattedData.length > 0) { + setMetricMeta({ + type: data[0]?.type, + description: data[0]?.description || description, + instance: data[0]?.instance, + job: data[0]?.job + }); + } + } catch (err) { + console.error(`Error loading historical data for ${metricName}:`, err); + setError(err.message); + message.error(`Failed to load historical data: ${err.message}`); + } finally { + setIsLoading(false); + } + }; + + const startRealtimeUpdates = () => { + setIsLiveUpdating(true); + setIsLoading(true); + + const end = new Date(); + const start = new Date(end.getTime() - TIME_WINDOW_MS); + fetchHistoricalData(start, end).finally(() => setIsLoading(false)); + + return metricsService.subscribeToMetric( + subscriptionKey, + (newData) => { + setRawData(prev => { + const now = Date.now(); + const cutoffTime = now - TIME_WINDOW_MS; + + const formattedNewData = formatMetricData(newData) + .filter(point => point.timestamp >= cutoffTime); + + const filteredPrev = prev.filter(point => point.timestamp >= cutoffTime); + + // Объединяем данные, удаляем дубликаты + const merged = [...filteredPrev, ...formattedNewData] + .filter((v, i, a) => + a.findIndex(t => + t.timestamp === v.timestamp && + t[lineKey] === v[lineKey] + ) === i + ); + + return merged; + }); + }, + 5000, // Интервал обновления 5 секунд + { + ...filters, + ...(device && { device }), + ...(source_id && { source_id }) + } + ); + }; + + const stopRealtimeUpdates = () => { + setIsLiveUpdating(false); + metricsService.unsubscribeFromMetric(subscriptionKey); + }; + + const handleCustomRangeApply = () => { + if (startDate && endDate) { + fetchHistoricalData(startDate, endDate); + } + }; + + // Обновляем логи статусов + useEffect(() => { + if (rawData.length > 0) { + const logs = []; + const devices = [...new Set(rawData.map(item => item[lineKey]))]; + + devices.forEach(dev => { + const deviceData = rawData + .filter(item => item[lineKey] === dev) + .sort((a, b) => a.timestamp - b.timestamp); + + if (deviceData.length > 0) { + logs.push(deviceData[0]); // Первая точка + + for (let i = 1; i < deviceData.length; i++) { + if (deviceData[i].status !== deviceData[i - 1].status) { + logs.push(deviceData[i]); + } + } + } + }); + + setStatusLogs(logs.sort((a, b) => b.timestamp - a.timestamp)); + } + }, [rawData, lineKey]); + + useEffect(() => { + let unsubscribe; + + if (mode === 'realtime') { + unsubscribe = startRealtimeUpdates(); + } else { + stopRealtimeUpdates(); + fetchHistoricalData(startDate, endDate); + } + + return () => { + if (unsubscribe) unsubscribe(); + stopRealtimeUpdates(); + }; + }, [mode, metricName, device, source_id]); + + const metaInfo = [ + metricMeta.instance && `Instance: ${metricMeta.instance}`, + metricMeta.job && `Job: ${metricMeta.job}`, + metricMeta.type && `Type: ${metricMeta.type}` + ].filter(Boolean).join(' | '); + + return ( +
+
+ setMode(e.target.value)} + buttonStyle="solid" + style={{ marginBottom: 10 }} + > + Режим реального времени + Исторические данные + + + {mode === 'historical' && ( + + )} + + {mode === 'realtime' && ( + + {isLiveUpdating ? 'Обновление в реальном времени' : 'Режим реального времени остановлен'} + + )} +
+ + {device && Устройство: {device}} + {source_id && Модуль: {source_id.split('$')[1]}} + + + + setShowLogs(!showLogs)} + sx={{ + position: 'absolute', + right: 16, + top: 16, + zIndex: 1000, + bgcolor: 'background.paper', + boxShadow: 1 + }} + > + + + + + {isLoading ? ( +
+ +
+ ) : error ? ( +
Ошибка: {error}
+ ) : rawData.length === 0 ? ( +
Нет данных для метрики: {metricName}
+ ) : ( + <> + + {showLogs && } + + )} +
+
+ ); +}; + +export default SystemChart; \ No newline at end of file diff --git a/src/Charts/SystemStatusChart.jsx b/src/Charts/SystemStatusChart.jsx deleted file mode 100755 index 5d82137..0000000 --- a/src/Charts/SystemStatusChart.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; - -const SystemStatusChart = ({ data }) => { - // Обрезаем массив, оставляя только последние 20 точек - const trimmedData = data.slice(-20); - - const CustomTooltip = ({ active, payload, label }) => { - if (active && payload && payload.length) { - return ( -
-

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

-

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

-
- ); - } - return null; - }; - - return ( - - - - - - } /> - - - - ); -}; - -export default SystemStatusChart; \ No newline at end of file diff --git a/src/Charts/WebSocketManager.jsx b/src/Charts/WebSocketManager.jsx deleted file mode 100644 index ff542a0..0000000 --- a/src/Charts/WebSocketManager.jsx +++ /dev/null @@ -1,121 +0,0 @@ -import { io } from 'socket.io-client'; - -class WebSocketManager { - constructor() { - this.socket = null; - this.subscribers = new Map(); - this.connectionStatus = 'disconnected'; - this.connectionCallbacks = new Set(); - this.connecting = false; - } - - connect() { - if (this.socket?.connected || this.connecting) { - return this.socket; - } - - this.connecting = true; - - this.socket = io(`${import.meta.env.VITE_BACK_WS_URL}/api/metrics-ws`, { - transports: ['websocket'], - reconnection: true, - reconnectionAttempts: Infinity, - reconnectionDelay: 1000, - reconnectionDelayMax: 5000, - }); - - this.socket.on('connect', () => { - this.connectionStatus = 'connected'; - this.connecting = false; - this.notifyConnectionStatus(); - }); - - this.socket.on('disconnect', (reason) => { - this.connectionStatus = 'disconnected'; - this.connecting = false; - this.notifyConnectionStatus(); - if (reason === 'io server disconnect') this.socket.connect(); - }); - - this.socket.on('connect_error', (error) => { - this.connectionStatus = 'error'; - this.notifyConnectionStatus(); - setTimeout(() => this.socket.connect(), 1000); - }); - - this.socket.on('metrics-data', (response) => { - const callbacks = this.subscribers.get(response.metric); - if (callbacks) { - callbacks.forEach(callback => callback(response.data)); - } - }); - - return this.socket; - } - - subscribe(metricName, callback) { - if (!this.socket?.connected) { - this.connect(); - } - - if (!this.subscribers.has(metricName)) { - this.subscribers.set(metricName, new Set()); - this.socket.emit('subscribe-metric', { - metric: metricName, - isSubscription: true // Флаг для подписки - }); - } - - this.subscribers.get(metricName).add(callback); - - return () => this.unsubscribe(metricName, callback); - } - - unsubscribe(metricName, callback) { - const callbacks = this.subscribers.get(metricName); - if (callbacks) { - callbacks.delete(callback); - if (callbacks.size === 0) { - this.subscribers.delete(metricName); - this.socket.emit('unsubscribe-metric', { metric: metricName }); - } - } - } - - getMetricsRange(metricName, start, end, step) { - return new Promise((resolve, reject) => { - const timer = setTimeout(() => { - reject(new Error('Timeout while waiting for metrics data')); - }, 10000); - - // Временный обработчик для разового запроса - const tempHandler = (data) => { - clearTimeout(timer); - this.socket.off(`metrics-range-${metricName}`, tempHandler); - resolve(data); - }; - - this.socket.on(`metrics-range-${metricName}`, tempHandler); - this.socket.emit('get-metrics', { - metric: metricName, - start, - end, - step, - isRangeQuery: true // Флаг для разового запроса - }); - }); - } - - onConnectionStatusChange(callback) { - this.connectionCallbacks.add(callback); - callback(this.connectionStatus); - return () => this.connectionCallbacks.delete(callback); - } - - notifyConnectionStatus() { - this.connectionCallbacks.forEach(callback => callback(this.connectionStatus)); - } -} - - -export const webSocketManager = new WebSocketManager(); \ No newline at end of file diff --git a/src/Charts2/Components/LineChartComponent.jsx b/src/Charts2/Components/LineChartComponent.jsx index e09476f..b383edd 100644 --- a/src/Charts2/Components/LineChartComponent.jsx +++ b/src/Charts2/Components/LineChartComponent.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { LineChart, Line, @@ -66,12 +66,39 @@ const StatusIndicator = ({ cx, cy, payload }) => { ); }; -const CustomTooltip = ({ active, payload, label }) => { - if (!active || !payload || !payload.length) return null; - - const status = payload[0].payload.status; +const StatusBadge = ({ status }) => { const statusColor = getStatusColor(status); + return ( +
+ +
+ {getStatusText(status)} +
+ {getStatusDescription(status)} +
+
+
+ ); +}; + +const CustomTooltip = ({ active, payload, label, multipleLines }) => { + if (!active || !payload || !payload.length) return null; + return (
{ boxShadow: '0 2px 4px rgba(0,0,0,0.1)' }}>

{new Date(label).toLocaleString()}

-

- Значение: {payload[0].value.toFixed(2)} -

-
- -
- {getStatusText(status)} -
- {getStatusDescription(status)} + + {multipleLines ? ( + payload.map((item, index) => ( +
+

+ {item.name}: {item.value.toFixed(2)} +

+
-
-
+ )) + ) : ( + <> +

+ Значение: {payload[0].value.toFixed(2)} +

+ + + )}
); }; @@ -117,9 +137,43 @@ const LineChartComponent = ({ metaInfo, dataKey = 'value', height = 400, - ranges = [], - statusBoundaries = [] + ranges = [], + statusBoundaries = [], + multipleLines = false, + lineKey = 'device' }) => { + // Группировка данных для нескольких линий + const groupedData = useMemo(() => { + if (!multipleLines || !data || data.length === 0) return null; + + return data.reduce((groups, item) => { + const key = item[lineKey] || 'default'; + if (!groups[key]) { + groups[key] = { + data: [], + color: getLineColor(key), + name: `${title} (${key})` + }; + } + groups[key].data.push(item); + return groups; + }, {}); + }, [data, multipleLines, lineKey, title]); + + // Функции для цветов линий + const getLineColor = (key) => { + const colors = ['#8884d8', '#82ca9d', '#ffc658', '#ff8042', '#0088FE']; + const index = Math.abs(hashCode(key)) % colors.length; + return colors[index]; + }; + + const hashCode = (str) => { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + return hash; + }; const getStatusAreas = () => { if (!data || data.length === 0) return null; @@ -141,22 +195,21 @@ const LineChartComponent = ({ return areas.map((area, i) => ( - + key={`area-${i}`} + x1={area.start} + x2={area.end} + fill={getStatusColor(area.status)} + fillOpacity={0.12} + stroke={getStatusColor(area.status)} + strokeWidth={1} + strokeOpacity={0.5} + /> )); }; const renderRangeLines = () => { if (!ranges || ranges.length === 0) return null; - + // Собираем только уникальные граничные значения, исключая дубликаты на стыках диапазонов const boundaryValues = []; ranges.forEach((range, index) => { @@ -164,28 +217,28 @@ const LineChartComponent = ({ if (index === 0) { boundaryValues.push(range.min); boundaryValues.push(range.max); - } + } // Для остальных добавляем только max (min будет совпадать с max предыдущего) else { boundaryValues.push(range.max); } }); - + return boundaryValues.map((value, index) => { // Находим диапазон, к которому принадлежит эта граница const range = ranges.find(r => r.min === value || r.max === value); const status = range ? range.status : 1; - + const lineStyle = { 1: { strokeWidth: 1, strokeDasharray: "none", opacity: 0.7 }, 2: { strokeWidth: 2, strokeDasharray: "none", opacity: 0.9 }, 3: { strokeWidth: 2, strokeDasharray: "none", opacity: 1 }, 4: { strokeWidth: 2, strokeDasharray: "none", opacity: 1 } }[status] || { strokeWidth: 1, strokeDasharray: "3 3", opacity: 0.7 }; - + return ( + {/* Заголовок и описание */} {title &&

{title}

} - {description && ( -

{description}

- )} + {description &&

{description}

} {metaInfo && (
{metaInfo} @@ -287,35 +339,56 @@ const LineChartComponent = ({
)} + {/* График */} - - - new Date(ts).toLocaleTimeString()} - /> - - {renderRangeLines()} - {renderStatusBoundaries()} - {getStatusAreas()} - } /> - - } - activeDot={{ r: 8 }} - isAnimationActive={false} - name={title} - /> - + + + new Date(ts).toLocaleTimeString()} + /> + + {renderRangeLines()} + {renderStatusBoundaries()} + {getStatusAreas()} + } /> + + + {multipleLines && groupedData ? ( + Object.entries(groupedData).map(([key, group]) => ( + } + activeDot={{ r: 8 }} + isAnimationActive={false} + animationDuration={300} + name={group.name} + /> + )) + ) : ( + } + activeDot={{ r: 8 }} + isAnimationActive={false} + animationDuration={300} + name={title} + /> + )} + {/* Легенда статусов */} diff --git a/src/Charts2/PrometheusChart.jsx b/src/Charts2/PrometheusChart.jsx index db9afa7..8466c14 100644 --- a/src/Charts2/PrometheusChart.jsx +++ b/src/Charts2/PrometheusChart.jsx @@ -30,6 +30,9 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => { const [isLiveUpdating, setIsLiveUpdating] = useState(false); const [showLogs, setShowLogs] = useState(false); const [statusLogs, setStatusLogs] = useState([]); + const MAX_POINTS = 50; + const TIME_WINDOW_MS = 3600 * 1000; // 1 час в миллисекундах + const getSubscriptionKey = () => { const filterParts = []; @@ -53,16 +56,32 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => { .sort((a, b) => a.timestamp - b.timestamp); }; - const downsampleData = (data, maxPoints = 500) => { - if (data.length <= maxPoints) return data; - - const ratio = Math.ceil(data.length / maxPoints); - return data.filter((_, index) => index % ratio === 0); + const calculateStep = (startTime, endTime, maxPoints = 10000) => { + const durationSeconds = (endTime.getTime() - startTime.getTime()) / 1000; + return Math.max(Math.ceil(durationSeconds / maxPoints), 1); }; - const calculateStep = (startTime, endTime, maxPoints = 10000) => { - const seconds = (endTime.getTime() - startTime.getTime()) / 1000; - return Math.max(Math.ceil(seconds / maxPoints), 1); // в секундах + const downsampleData = (data, maxPoints = MAX_POINTS) => { + if (data.length <= maxPoints) return [...data]; + + const sortedData = [...data].sort((a, b) => a.timestamp - b.timestamp); + const step = Math.max(1, Math.floor(sortedData.length / maxPoints)); + + const result = []; + for (let i = 0; i < sortedData.length; i += step) { + if (result.length >= maxPoints) break; + result.push(sortedData[i]); + } + + // Всегда включаем последнюю точку + if (result.length > 0) { + const lastOriginalPoint = sortedData[sortedData.length - 1]; + if (result[result.length - 1].timestamp !== lastOriginalPoint.timestamp) { + result[result.length - 1] = lastOriginalPoint; + } + } + + return result; }; @@ -100,9 +119,15 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => { extendedFilters ); + const formattedData = formatMetricData(data) + .sort((a, b) => a.timestamp - b.timestamp); - const formattedData = downsampleData(formatMetricData(data), 100); //КОЛИЧЕСТВО ТОЧЕК НА ГРАФИКЕ - if (formattedData.length > 0) { + // Применяем ограничение по количеству точек только для исторических данных + const limitedData = formattedData.length > MAX_POINTS + ? formattedData.slice(-MAX_POINTS) + : formattedData; + + if (limitedData.length > 0) { setMetricMeta({ type: data[0]?.type, description: data[0]?.description || description, @@ -111,7 +136,7 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => { }); } - setChartData(formattedData); + setChartData(limitedData); } catch (err) { console.error(`Error loading historical data for ${metricName}:`, err); setError(err.message); @@ -126,21 +151,38 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => { setIsLoading(true); const end = new Date(); - const start = new Date(end.getTime() - 3600 * 1000); + const start = new Date(end.getTime() - TIME_WINDOW_MS); fetchHistoricalData(start, end).finally(() => setIsLoading(false)); return metricsService.subscribeToMetric( getSubscriptionKey(), (newData) => { - const formattedData = formatMetricData(newData); setChartData(prev => { - const newChartData = [...prev, ...formattedData] + const now = Date.now(); + const cutoffTime = now - TIME_WINDOW_MS; + + // Фильтруем старые точки (старше TIME_WINDOW_MS) + const filteredPrev = prev.filter(point => point.timestamp >= cutoffTime); + + // Добавляем новые точки + const newPoints = formatMetricData(newData) + .filter(point => point.timestamp >= cutoffTime); + + // Объединяем и удаляем дубликаты + const mergedData = [...filteredPrev, ...newPoints] .filter((v, i, a) => a.findIndex(t => t.timestamp === v.timestamp) === i) - .slice(-200); - return newChartData; + .sort((a, b) => a.timestamp - b.timestamp); + + // Если точек слишком много, равномерно прореживаем + if (mergedData.length > MAX_POINTS) { + const step = Math.ceil(mergedData.length / MAX_POINTS); + return mergedData.filter((_, index) => index % step === 0); + } + + return mergedData; }); }, - 5000, + 1000, // Уменьшаем интервал обновления до 1 секунды { ...filters, ...(device && { device }), @@ -149,6 +191,7 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => { ); }; + const stopRealtimeUpdates = () => { setIsLiveUpdating(false); metricsService.unsubscribeFromMetric(getSubscriptionKey()); diff --git a/src/Components/Layout/Dashboard.jsx b/src/Components/Layout/Dashboard.jsx index baf6004..a5eff8e 100755 --- a/src/Components/Layout/Dashboard.jsx +++ b/src/Components/Layout/Dashboard.jsx @@ -10,6 +10,7 @@ import menuData from "../TreeChart/menuData.json"; import SidebarMenuWrapper from "./SidebarMenuWrapper"; import MetricTabContent from "./MetricTabContent"; import ProfileMenu from "../UI/ProfileMenu"; +import AIAnalysisButton from "../UI/AIAnalysisButton"; const DashboardContainer = styled(Box)(({ theme }) => ({ display: 'flex', @@ -140,9 +141,13 @@ const Dashboard = ({ isDarkMode, setIsDarkMode, user, onLogout }) => { top: 12, right: 20, zIndex: (theme) => theme.zIndex.tooltip + 10, - pointerEvents: 'auto' + pointerEvents: 'auto', //ВРЕМЕННОЕ РАСПОЛОЖЕНИЕ КНОПКИ + display: 'flex', + gap: 1, + alignItems: 'center' }} > + diff --git a/src/Components/Layout/SidebarMenuComponents/SidebarFooter.jsx b/src/Components/Layout/SidebarMenuComponents/SidebarFooter.jsx index 9268f9e..0ed21f7 100644 --- a/src/Components/Layout/SidebarMenuComponents/SidebarFooter.jsx +++ b/src/Components/Layout/SidebarMenuComponents/SidebarFooter.jsx @@ -46,11 +46,11 @@ const SidebarFooter = ({ const handleSettingsClose = () => { setSettingsOpen(false); }; - console.log('SidebarFooter user with role:', { + /*console.log('SidebarFooter user with role:', { ...user, hasRole: 'role' in user, roleValue: user?.role - }); + }); */ return ( <> diff --git a/src/Components/UI/AIAnalysisButton.jsx b/src/Components/UI/AIAnalysisButton.jsx new file mode 100644 index 0000000..0a0e037 --- /dev/null +++ b/src/Components/UI/AIAnalysisButton.jsx @@ -0,0 +1,133 @@ +/*import React, { useState } from 'react'; +import { Button, CircularProgress, Alert, Box } from '@mui/material'; +import axios from 'axios'; + +const AIAnalysisButton = ({ onAnalysisComplete }) => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [result, setResult] = useState(null); + + const handleAnalyze = async () => { + setLoading(true); + setError(null); + + try { + const response = await axios.post('/api/clickhouse/send-to-ai'); + setResult(response.data); + if (onAnalysisComplete) { + onAnalysisComplete(response.data); + } + } catch (err) { + setError(err.response?.data?.message || err.message); + } finally { + setLoading(false); + } + }; + + return ( + + + + {error && ( + {error} + )} + + ); +}; + +export default AIAnalysisButton; */ + +import React, { useState } from 'react'; +import { Button, CircularProgress, Alert, Box } from '@mui/material'; +import axios from 'axios'; + +const AIAnalysisButton = ({ onAnalysisComplete }) => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [result, setResult] = useState(null); + + const handleAnalyze = async () => { + setLoading(true); + setError(null); + setResult(null); + + try { + // 1. Получаем данные из ClickHouse + const metricsResponse = await axios.get('/api/clickhouse'); + + // 2. Отправляем в AI API + const aiResponse = await axios.post( + '/ai-api/api/metrics/rest', + metricsResponse.data, + { + headers: { + 'Content-Type': 'application/json', + }, + } + ); + + setResult(aiResponse.data); + if (onAnalysisComplete) { + onAnalysisComplete(aiResponse.data); + } + } catch (err) { + console.error("Детали ошибки 422:", err.response?.data); + setError(err.response?.data?.message || JSON.stringify(err.response?.data) || "Ошибка валидации данных"); + + + } finally { + setLoading(false); + } + + }; + + return ( + + + + {error && ( + + {error} + + )} + + {result && !loading && ( + + Анализ завершен! Результат в консоли. + + )} + + ); +}; + +export default AIAnalysisButton; \ No newline at end of file diff --git a/src/Components/UI/RoleBasedRender.jsx b/src/Components/UI/RoleBasedRender.jsx index afdec19..8b04592 100644 --- a/src/Components/UI/RoleBasedRender.jsx +++ b/src/Components/UI/RoleBasedRender.jsx @@ -1,12 +1,12 @@ import React from 'react'; export const RoleBasedRender = ({ user, allowedRoles, children }) => { - console.log('RoleBasedRender check:', { - user, - hasRole: user?.role, - allowedRoles, - hasAccess: user && allowedRoles.includes(user.role) - }); + // console.log('RoleBasedRender check:', { + // user, + // hasRole: user?.role, + // allowedRoles, + // hasAccess: user && allowedRoles.includes(user.role) + // }); if (!user || !allowedRoles.includes(user.role)) { return null; diff --git a/src/Components/UI/auth.jsx b/src/Components/UI/auth.jsx index 76f0b4c..bffb9bb 100644 --- a/src/Components/UI/auth.jsx +++ b/src/Components/UI/auth.jsx @@ -1,3 +1,5 @@ +import axios from "axios" + export const checkAuth = async () => { try { const { data } = await axios.get( diff --git a/src/Components/hooks/TabContent.jsx b/src/Components/hooks/TabContent.jsx index 5176b0b..e89acd9 100644 --- a/src/Components/hooks/TabContent.jsx +++ b/src/Components/hooks/TabContent.jsx @@ -1,8 +1,7 @@ -import SystemStatusChart from "../../Charts/SystemStatusChart"; import TreeTable from "../UI/TreeTable"; import FlowChart from "../TreeChart/FlowChart"; import { getStatusColor } from "../TreeChart/dataUtils"; -import MetricsAnalyzer from "./MetricsAnalyzer"; // Импортируем новый компонент +import SystemChart from "../../Charts/SystemChart"; const TabContent = ({ activeTab, tabs, statusHistories, treeData1, tabContent, handleOpenTab }) => { const countStatuses = (data) => { @@ -17,24 +16,52 @@ const TabContent = ({ activeTab, tabs, statusHistories, treeData1, tabContent, h } }; - countRecursive(data); + if (data) countRecursive(data); return counts; }; if (activeTab === "Главная") { - const statusCounts = treeData1 ? countStatuses(treeData1) : { green: 0, yellow: 0, orange: 0, red: 0 }; + const statusCounts = countStatuses(treeData1); + + // Конфигурация для метрики серверов (с несколькими линиями) + const serverMetric = { + name: "zvks_server_li", + title: "Надежность системы", + description: "Уровень надежности системы", + multipleLines: true, + lineKey: "device", + ranges: [ + { value: 15, color: 'green', label: 'Оптимально' }, + { value: 10, color: 'orange', label: 'Предупреждение' }, + { value: 5, color: 'red', label: 'Критично' } + ] + }; + + // Конфигурация для метрики приложений (одна линия) + const appMetric = { + name: "zvks_application_li", + title: "Функциональность системы", + description: "Уровень функциональности системы", + multipleLines: false + }; return (

Общий мониторинг состояния системы

- - + +
- - + +
@@ -65,9 +92,6 @@ const TabContent = ({ activeTab, tabs, statusHistories, treeData1, tabContent, h - - {/* Добавляем кнопку анализа - */}
); } else if (activeTab === "Визуализация") { diff --git a/vite.config.js b/vite.config.js index 76eceaa..04ad099 100755 --- a/vite.config.js +++ b/vite.config.js @@ -2,14 +2,10 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import svgr from 'vite-plugin-svgr' -// https://vite.dev/config/ export default defineConfig({ - plugins: [ - react(), - svgr() - ], + plugins: [react(), svgr()], server: { host: true, - allowedHosts: ['dev.msf.enode', 'demo-msf.kis-npo.ru'] + allowedHosts: ['dev.msf.enode', 'demo-msf.kis-npo.ru'], } -}) \ No newline at end of file +}); \ No newline at end of file