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 (
+
+ : null}
+ sx={{
+ minWidth: '180px',
+ backgroundColor: '#4caf50',
+ '&:hover': {
+ backgroundColor: '#388e3c',
+ }
+ }}
+ >
+ {loading ? 'Analyzing...' : 'AI Analysis'}
+
+
+ {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 (
+
+ : null}
+ sx={{
+ minWidth: '180px',
+ backgroundColor: '#4caf50',
+ '&:hover': {
+ backgroundColor: '#388e3c',
+ }
+ }}
+ >
+ {loading ? 'Отправка в AI...' : 'Проанализировать AI'}
+
+
+ {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