Compare commits

..

No commits in common. "c208813daaeafb46de160c9c253bc69c57b5d995" and "140b058f412d362edf48d20cf087dc9eff5294cf" have entirely different histories.

34 changed files with 2574 additions and 16334 deletions

View File

@ -2,10 +2,10 @@ FROM node:22.13.0
WORKDIR /app WORKDIR /app
COPY package.json package-lock.json ./ COPY package.json package-lock.json vite.config.js eslint.config.js ./
RUN npm install --verbose
RUN npm install
COPY vite.config.js eslint.config.js ./
COPY . . COPY . .
ENTRYPOINT ["npm", "run", "dev"] ENTRYPOINT ["npm", "run", "dev"]

16692
package-lock.json generated Normal file → Executable file

File diff suppressed because it is too large Load Diff

View File

@ -110,7 +110,7 @@ function App() {
const handleLogout = async () => { const handleLogout = async () => {
try { try {
await axios.post(`/api/auth/logout`, null, { await axios.post(`${import.meta.env.VITE_BACK_URL}/api/auth/logout`, null, {
withCredentials: true, withCredentials: true,
}); });

View File

@ -0,0 +1,45 @@
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 (
<div>
<h2>{metricName} ({metricType})</h2>
<ResponsiveContainer width="100%" height={400}>
<BarChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="instance" />
<YAxis />
<Tooltip />
<Legend />
{allQuantiles.map((quantile, index) => (
<Bar
key={quantile}
dataKey={quantile}
fill={colors[index % colors.length]}
name={`Quantile ${quantile}`}
/>
))}
</BarChart>
</ResponsiveContainer>
</div>
);
};
export default BarChartComponent;

View File

@ -0,0 +1,26 @@
const ChartSkeleton = () => (
<Box sx={{
backgroundColor: '#fff',
borderRadius: '8px',
padding: '20px',
marginBottom: '20px',
position: 'relative'
}}>
<Box sx={{ position: 'absolute', right: '20px', top: '20px' }}>
<Skeleton variant="circular" width={16} height={16} />
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<Skeleton variant="text" width="40%" height={30} />
<Skeleton variant="text" width="30%" height={30} />
</Box>
<Skeleton variant="rectangular" width="100%" height={300} />
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 2, gap: 2 }}>
{[1, 2, 3, 4].map((_, i) => (
<Skeleton key={i} variant="rounded" width={80} height={36} />
))}
</Box>
</Box>
);

View File

@ -0,0 +1,20 @@
import React from 'react';
export const ConnectionStatusIndicator = ({ connectionStatus }) => {
return (
<div style={{
position: 'absolute',
top: '10px',
right: '10px',
padding: '5px 10px',
borderRadius: '4px',
backgroundColor: connectionStatus === 'connected' ? '#4CAF50' :
connectionStatus === 'error' ? '#F44336' : '#FFC107',
color: 'white',
fontSize: '12px'
}}>
{connectionStatus === 'connected' ? 'Online' :
connectionStatus === 'error' ? 'Connection Error' : 'Offline'}
</div>
);
};

View File

@ -0,0 +1,12 @@
import React from 'react';
const CounterComponent = ({ value, metricName }) => {
return (
<div style={{ textAlign: 'center', padding: '20px', border: '1px solid #ccc', borderRadius: '8px', margin: '10px' }}>
<h2>{metricName}</h2>
<p style={{ fontSize: '48px', fontWeight: 'bold', color: '#3e95cd' }}>{value}</p>
</div>
);
};
export default CounterComponent;

View File

@ -0,0 +1,17 @@
import React from 'react';
export const CurrentRangeDisplay = ({ useCustomRange, startDate, endDate, selectedRange }) => {
return (
<div style={{
margin: '10px 0',
padding: '8px 12px',
backgroundColor: '#f0f7ff',
borderRadius: '4px',
borderLeft: '3px solid #4a6baf'
}}>
Текущий диапазон: {useCustomRange
? `${startDate.toLocaleString()} - ${endDate.toLocaleString()}`
: selectedRange.label}
</div>
);
};

View File

@ -0,0 +1,239 @@
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 (
<div style={{
backgroundColor: '#fff',
padding: '10px',
border: '1px solid #ccc',
borderRadius: '4px',
boxShadow: '0 2px 5px rgba(0,0,0,0.1)'
}}>
<p style={{ fontWeight: 'bold', marginBottom: '5px' }}>
{currentPoint?.fullTime || new Date(label).toLocaleString('ru-RU')}
</p>
{payload.map((item, index) => (
<p key={index} style={{ color: item.color }}>
{`Значение: ${item.value}`}
</p>
))}
</div>
);
}
return null;
};
if (!data.length) {
return (
<Box sx={{
position: 'relative',
height: '400px',
backgroundColor: '#fff',
borderRadius: '8px',
padding: '20px'
}}>
<Skeleton variant="text" width="60%" height={30} sx={{ mb: 2 }} />
<Skeleton variant="rectangular" width="100%" height="calc(100% - 50px)" />
</Box>
);
}
return (
<div style={{ position: 'relative', height: '400px' }}>
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={displayData}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
ref={chartRef}
>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
<XAxis
dataKey="timestamp"
height={75}
tick={{ fontSize: 12, angle: -45, textAnchor: 'end' }}
interval={Math.max(1, Math.floor(data.length / 10))}
tickFormatter={(timestamp) => {
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'
});
}
}}
/>
<YAxis tick={{ fontSize: 12 }} />
<Tooltip content={<CustomTooltip />} />
{instanceKeys.map((instance, index) => (
<Line
key={instance}
type=""
dataKey={instance}
name={instance}
stroke={colors[index % colors.length]}
strokeWidth={2}
dot={false}
activeDot={{ r: 6 }}
/>
))}
{selectionArea?.start && selectionArea?.end && (
<ReferenceArea
x1={selectionArea.start}
x2={selectionArea.end}
strokeOpacity={0.3}
fill="#4a6baf"
/>
)}
</LineChart>
</ResponsiveContainer>
</div>
);
};
export default React.memo(LineChartComponent);

View File

@ -0,0 +1,29 @@
import React from 'react';
import { ScatterChart, XAxis, YAxis, CartesianGrid, Tooltip, Legend, Scatter, ResponsiveContainer } from 'recharts';
const ScatterChartComponent = ({ chartData, metricName, metricType, colors }) => {
return (
<div>
<h2>{metricName} ({metricType})</h2>
<ResponsiveContainer width="100%" height={400}>
<ScatterChart>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="time" />
<YAxis dataKey="value" />
<Tooltip />
<Legend />
{Object.keys(chartData).map((instance, index) => (
<Scatter
key={instance}
data={chartData[instance]}
name={instance}
fill={colors[index % colors.length]}
/>
))}
</ScatterChart>
</ResponsiveContainer>
</div>
);
};
export default ScatterChartComponent;

View File

@ -0,0 +1,151 @@
import React from 'react';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import { TIME_RANGES } from './constants';
export const TimeRangeSelector = ({
selectedRange,
handleRangeChange,
startDate,
setStartDate,
endDate,
setEndDate,
useCustomRange,
handleCustomRangeChange,
handleResetZoom
}) => {
return (
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: '15px',
alignItems: 'center',
marginBottom: '15px'
}}>
{/* Стандартные диапазоны */}
<div style={{ flex: '1 1 200px', display: 'flex', gap: '10px', alignItems: 'flex-end' }}>
<div style={{ flex: '1' }}>
<label htmlFor="time-range" style={{
display: 'block',
marginBottom: '5px',
fontWeight: '500',
color: '#555'
}}>Стандартные диапазоны:</label>
<select
id="time-range"
value={selectedRange.value}
onChange={handleRangeChange}
style={{
width: '100%',
padding: '8px 12px',
borderRadius: '4px',
border: '1px solid #ddd',
color: "#333",
backgroundColor: '#f9f9f9'
}}
>
{TIME_RANGES.map(range => (
<option key={range.value} value={range.value}>{range.label}</option>
))}
</select>
</div>
<button
onClick={handleResetZoom}
style={{
padding: '8px 16px',
backgroundColor: '#f0f0f0',
color: '#333',
border: '1px solid #ddd',
borderRadius: '4px',
cursor: 'pointer',
transition: 'all 0.2s',
height: '36px',
whiteSpace: 'nowrap'
}}
onMouseOver={(e) => e.target.style.backgroundColor = '#e0e0e0'}
onMouseOut={(e) => e.target.style.backgroundColor = '#f0f0f0'}
>
Сбросить
</button>
</div>
{/* Кастомный диапазон */}
<div style={{ flex: '1 1 300px' }}>
<div style={{
marginBottom: '10px',
fontWeight: '500',
color: '#555'
}}>
Или укажите свой диапазон:
</div>
<div style={{
display: 'flex',
gap: '10px',
flexWrap: 'wrap'
}}>
<div style={{ flex: '1 1 200px' }}>
<DatePicker
selected={startDate}
onChange={(date) => setStartDate(date)}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="yyyy-MM-dd HH:mm"
placeholderText="Начальная дата"
customInput={
<input style={{
backgroundColor: '#f9f9f9',
color: "#555",
width: '100%',
padding: '8px 12px',
borderRadius: '4px',
border: '1px solid #ddd'
}} />
}
/>
</div>
<div style={{ flex: '1 1 200px' }}>
<DatePicker
selected={endDate}
onChange={(date) => setEndDate(date)}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="yyyy-MM-dd HH:mm"
placeholderText="Конечная дата"
customInput={
<input style={{
backgroundColor: '#f9f9f9',
color: "#555",
width: '100%',
padding: '8px 12px',
borderRadius: '4px',
border: '1px solid #ddd'
}} />
}
/>
</div>
<button
onClick={handleCustomRangeChange}
style={{
padding: '8px 16px',
backgroundColor: '#4a6baf',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
transition: 'background-color 0.2s',
flex: '0 0 auto',
alignSelf: 'flex-end'
}}
onMouseOver={(e) => e.target.style.backgroundColor = '#3a5a9f'}
onMouseOut={(e) => e.target.style.backgroundColor = '#4a6baf'}
>
Применить
</button>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,35 @@
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;

View File

@ -1,115 +0,0 @@
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 <div>Нет данных для отображения</div>;
// Создаем массив уникальных линий
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 (
<div style={{ width: '100%', height: `${height}px` }}>
<h3>{title}</h3>
{description && <p>{description}</p>}
<ResponsiveContainer width="100%" height="90%">
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="timestamp"
tickFormatter={formatXAxis}
/>
<YAxis domain={[0, 25]} />
<Tooltip
formatter={formatTooltip}
labelFormatter={(label) => format(new Date(label), 'yyyy-MM-dd HH:mm:ss')}
/>
<Legend />
{multipleLines ? (
lineKeys.map(key => (
<Line
key={`line-${key}`}
type="monotone"
dataKey={key}
name={` ${key}`}
stroke={lineColors[key] || lineColors.default}
strokeWidth={2}
dot={false}
activeDot={{ r: 6 }}
isAnimationActive={false}
/>
))
) : (
<Line
type="monotone"
dataKey={lineKeys[0] || 'value'}
name={title}
stroke={lineColors.default}
strokeWidth={2}
dot={false}
activeDot={{ r: 6 }}
isAnimationActive={false}
/>
)}
{/* Добавляем диапазоны если они есть */}
{ranges.map((range, idx) => (
<ReferenceLine
key={`range-${idx}`}
y={range.value}
stroke={range.color}
label={range.label}
/>
))}
</LineChart>
</ResponsiveContainer>
</div>
);
};
export default LineChartComponent;

499
src/Charts/PrometheusChart.jsx Executable file
View File

@ -0,0 +1,499 @@
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 = () => (
<Box sx={{
backgroundColor: '#fff',
borderRadius: '8px',
padding: '20px',
marginBottom: '20px',
position: 'relative'
}}>
<Box sx={{ position: 'absolute', right: '20px', top: '20px' }}>
<Skeleton variant="circular" width={16} height={16} />
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<Skeleton variant="text" width="40%" height={30} />
<Skeleton variant="text" width="30%" height={30} />
</Box>
<Skeleton variant="rectangular" width="100%" height={300} />
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 2, gap: 2 }}>
{[1, 2, 3, 4].map((_, i) => (
<Skeleton key={i} variant="rounded" width={80} height={36} />
))}
</Box>
</Box>
);
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 <ChartSkeleton />;
}
if (Object.keys(chartData).length === 0) {
return (
<Box sx={{
backgroundColor: '#fff',
borderRadius: '8px',
padding: '20px',
marginBottom: '20px',
textAlign: 'center'
}}>
No data available
</Box>
);
}
return (
<div style={{
backgroundColor: '#fff',
borderRadius: '8px',
padding: '20px',
marginBottom: '20px',
position: 'relative'
}}>
<ConnectionStatusIndicator connectionStatus={connectionStatus} />
<TimeRangeSelector
selectedRange={selectedRange}
handleRangeChange={handleRangeChange}
startDate={startDate}
setStartDate={setStartDate}
endDate={endDate}
setEndDate={setEndDate}
useCustomRange={useCustomRange}
handleCustomRangeChange={handleCustomRangeChange}
handleResetZoom={handleResetZoom}
/>
<CurrentRangeDisplay
useCustomRange={useCustomRange}
startDate={startDate}
endDate={endDate}
selectedRange={selectedRange}
/>
<LineChartComponent
chartData={chartData}
metricName={metricName}
colors={COLORS}
onRangeSelect={handleRangeSelect}
filteredData={filteredData}
/>
</div>
);
};
export default React.memo(PrometheusChart);

View File

@ -1,283 +0,0 @@
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 (
<div style={{ position: 'relative' }}>
<div style={{ marginBottom: 16 }}>
<Radio.Group
value={mode}
onChange={(e) => setMode(e.target.value)}
buttonStyle="solid"
style={{ marginBottom: 10 }}
>
<Radio.Button value="realtime">Режим реального времени</Radio.Button>
<Radio.Button value="historical">Исторические данные</Radio.Button>
</Radio.Group>
{mode === 'historical' && (
<DateRangeSelector
startDate={startDate}
endDate={endDate}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
onApply={handleCustomRangeApply}
/>
)}
{mode === 'realtime' && (
<Tag color={isLiveUpdating ? 'green' : 'red'}>
{isLiveUpdating ? 'Обновление в реальном времени' : 'Режим реального времени остановлен'}
</Tag>
)}
</div>
{device && <Tag color="geekblue">Устройство: {device}</Tag>}
{source_id && <Tag color="purple">Модуль: {source_id.split('$')[1]}</Tag>}
<Box position="relative">
<MuiTooltip title={showLogs ? "Скрыть логи" : "Показать логи"}>
<IconButton
onClick={() => setShowLogs(!showLogs)}
sx={{
position: 'absolute',
right: 16,
top: 16,
zIndex: 1000,
bgcolor: 'background.paper',
boxShadow: 1
}}
>
<ListAlt />
</IconButton>
</MuiTooltip>
{isLoading ? (
<div style={{ height: chartHeight, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Spin size="large" tip="Загрузка данных..." />
</div>
) : error ? (
<div style={{ color: 'red', padding: 20 }}>Ошибка: {error}</div>
) : rawData.length === 0 ? (
<div style={{ padding: 20 }}>Нет данных для метрики: {metricName}</div>
) : (
<>
<LineChartComponent
data={rawData}
title={title}
description={description}
multipleLines={multipleLines}
lineKey={lineKey}
metaInfo={metaInfo}
height={chartHeight}
ranges={ranges}
/>
{showLogs && <StatusLogTable logs={statusLogs} />}
</>
)}
</Box>
</div>
);
};
export default SystemChart;

View File

@ -0,0 +1,42 @@
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 (
<div className="custom-tooltip" style={{
backgroundColor: '#fff',
padding: '10px',
border: '1px solid #ccc'
}}>
<p>{`Время: ${label}`}</p>
<p>{`Значение: ${payload[0].value}`}</p>
</div>
);
}
return null;
};
return (
<ResponsiveContainer width="100%" height={300}>
<LineChart
data={trimmedData} // Используем обрезанный массив
margin={{
top: 5, right: 30, left: 20, bottom: 5,
}}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="time" />
<YAxis domain={[0, 100]} />
<Tooltip content={<CustomTooltip />} />
<Line type="monotone" dataKey="status" stroke="#8884d8" activeDot={{ r: 8 }} />
</LineChart>
</ResponsiveContainer>
);
};
export default SystemStatusChart;

View File

@ -0,0 +1,121 @@
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();

View File

@ -1,4 +1,4 @@
import React, { useMemo } from 'react'; import React from 'react';
import { import {
LineChart, LineChart,
Line, Line,
@ -66,39 +66,12 @@ const StatusIndicator = ({ cx, cy, payload }) => {
); );
}; };
const StatusBadge = ({ status }) => { const CustomTooltip = ({ active, payload, label }) => {
const statusColor = getStatusColor(status);
return (
<div style={{
display: 'flex',
alignItems: 'center',
padding: '4px 8px',
background: `${statusColor}20`,
borderLeft: `4px solid ${statusColor}`,
borderRadius: '4px',
marginTop: '4px'
}}>
<span style={{
width: 12,
height: 12,
backgroundColor: statusColor,
borderRadius: '50%',
marginRight: 8
}} />
<div>
<strong>{getStatusText(status)}</strong>
<div style={{ fontSize: '0.8em', color: '#666' }}>
{getStatusDescription(status)}
</div>
</div>
</div>
);
};
const CustomTooltip = ({ active, payload, label, multipleLines }) => {
if (!active || !payload || !payload.length) return null; if (!active || !payload || !payload.length) return null;
const status = payload[0].payload.status;
const statusColor = getStatusColor(status);
return ( return (
<div style={{ <div style={{
background: '#fff', background: '#fff',
@ -108,24 +81,31 @@ const CustomTooltip = ({ active, payload, label, multipleLines }) => {
boxShadow: '0 2px 4px rgba(0,0,0,0.1)' boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}> }}>
<p><strong>{new Date(label).toLocaleString()}</strong></p> <p><strong>{new Date(label).toLocaleString()}</strong></p>
<p style={{ color: payload[0].color }}>
{multipleLines ? ( Значение: <strong>{payload[0].value.toFixed(2)}</strong>
payload.map((item, index) => ( </p>
<div key={index} style={{ marginBottom: '8px' }}> <div style={{
<p style={{ color: item.color }}> display: 'flex',
{item.name}: <strong>{item.value.toFixed(2)}</strong> alignItems: 'center',
</p> padding: '4px 8px',
<StatusBadge status={item.payload.status} /> background: `${statusColor}20`,
borderLeft: `4px solid ${statusColor}`,
borderRadius: '4px'
}}>
<span style={{
width: 12,
height: 12,
backgroundColor: statusColor,
borderRadius: '50%',
marginRight: 8
}} />
<div>
<strong>{getStatusText(status)}</strong>
<div style={{ fontSize: '0.8em', color: '#666' }}>
{getStatusDescription(status)}
</div> </div>
)) </div>
) : ( </div>
<>
<p style={{ color: payload[0].color }}>
Значение: <strong>{payload[0].value.toFixed(2)}</strong>
</p>
<StatusBadge status={payload[0].payload.status} />
</>
)}
</div> </div>
); );
}; };
@ -137,43 +117,9 @@ const LineChartComponent = ({
metaInfo, metaInfo,
dataKey = 'value', dataKey = 'value',
height = 400, height = 400,
ranges = [], ranges = [],
statusBoundaries = [], 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 = () => { const getStatusAreas = () => {
if (!data || data.length === 0) return null; if (!data || data.length === 0) return null;
@ -195,21 +141,22 @@ const LineChartComponent = ({
return areas.map((area, i) => ( return areas.map((area, i) => (
<ReferenceArea <ReferenceArea
key={`area-${i}`} key={`area-${i}`}
x1={area.start} x1={area.start}
x2={area.end} x2={area.end}
fill={getStatusColor(area.status)} fill={getStatusColor(area.status)}
fillOpacity={0.12} fillOpacity={0.12}
stroke={getStatusColor(area.status)} stroke={getStatusColor(area.status)}
strokeWidth={1} strokeWidth={1}
strokeOpacity={0.5} strokeOpacity={0.5}
/> />
)); ));
}; };
const renderRangeLines = () => { const renderRangeLines = () => {
if (!ranges || ranges.length === 0) return null; if (!ranges || ranges.length === 0) return null;
// Собираем только уникальные граничные значения, исключая дубликаты на стыках диапазонов // Собираем только уникальные граничные значения, исключая дубликаты на стыках диапазонов
const boundaryValues = []; const boundaryValues = [];
ranges.forEach((range, index) => { ranges.forEach((range, index) => {
@ -217,28 +164,28 @@ const LineChartComponent = ({
if (index === 0) { if (index === 0) {
boundaryValues.push(range.min); boundaryValues.push(range.min);
boundaryValues.push(range.max); boundaryValues.push(range.max);
} }
// Для остальных добавляем только max (min будет совпадать с max предыдущего) // Для остальных добавляем только max (min будет совпадать с max предыдущего)
else { else {
boundaryValues.push(range.max); boundaryValues.push(range.max);
} }
}); });
return boundaryValues.map((value, index) => { return boundaryValues.map((value, index) => {
// Находим диапазон, к которому принадлежит эта граница // Находим диапазон, к которому принадлежит эта граница
const range = ranges.find(r => r.min === value || r.max === value); const range = ranges.find(r => r.min === value || r.max === value);
const status = range ? range.status : 1; const status = range ? range.status : 1;
const lineStyle = { const lineStyle = {
1: { strokeWidth: 1, strokeDasharray: "none", opacity: 0.7 }, 1: { strokeWidth: 1, strokeDasharray: "none", opacity: 0.7 },
2: { strokeWidth: 2, strokeDasharray: "none", opacity: 0.9 }, 2: { strokeWidth: 2, strokeDasharray: "none", opacity: 0.9 },
3: { strokeWidth: 2, strokeDasharray: "none", opacity: 1 }, 3: { strokeWidth: 2, strokeDasharray: "none", opacity: 1 },
4: { strokeWidth: 2, strokeDasharray: "none", opacity: 1 } 4: { strokeWidth: 2, strokeDasharray: "none", opacity: 1 }
}[status] || { strokeWidth: 1, strokeDasharray: "3 3", opacity: 0.7 }; }[status] || { strokeWidth: 1, strokeDasharray: "3 3", opacity: 0.7 };
return ( return (
<ReferenceLine <ReferenceLine
key={`line-${value}`} key={`line-${value}`} // Используем значение как ключ для стабильности
y={value} y={value}
stroke={rangeColors[status] || '#888'} stroke={rangeColors[status] || '#888'}
strokeWidth={lineStyle.strokeWidth} strokeWidth={lineStyle.strokeWidth}
@ -285,9 +232,10 @@ const LineChartComponent = ({
return ( return (
<div style={{ width: '100%', height: `${height}px` }}> <div style={{ width: '100%', height: `${height}px` }}>
{/* Заголовок и описание */}
{title && <h3>{title}</h3>} {title && <h3>{title}</h3>}
{description && <p style={{ marginTop: -10, color: '#666' }}>{description}</p>} {description && (
<p style={{ marginTop: -10, color: '#666' }}>{description}</p>
)}
{metaInfo && ( {metaInfo && (
<div style={{ fontSize: 12, color: '#888', marginBottom: 10 }}> <div style={{ fontSize: 12, color: '#888', marginBottom: 10 }}>
{metaInfo} {metaInfo}
@ -339,56 +287,35 @@ const LineChartComponent = ({
</div> </div>
)} )}
{/* График */}
<ResponsiveContainer width="100%" height="75%"> <ResponsiveContainer width="100%" height="75%">
<LineChart <LineChart
data={multipleLines ? null : data} data={data}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
> >
<CartesianGrid strokeDasharray="3 3" /> <CartesianGrid strokeDasharray="3 3" />
<XAxis <XAxis
dataKey="timestamp" dataKey="timestamp"
tickFormatter={(ts) => new Date(ts).toLocaleTimeString()} tickFormatter={(ts) => new Date(ts).toLocaleTimeString()}
/> />
<YAxis /> <YAxis />
{renderRangeLines()}
{renderStatusBoundaries()}
{getStatusAreas()}
<Tooltip content={<CustomTooltip />} />
<Legend />
<Line
type="monotone"
dataKey={dataKey}
stroke="#8884d8"
strokeWidth={2}
dot={<StatusIndicator />}
activeDot={{ r: 8 }}
isAnimationActive={false}
name={title}
/>
</LineChart>
{renderRangeLines()}
{renderStatusBoundaries()}
{getStatusAreas()}
<Tooltip content={<CustomTooltip multipleLines={multipleLines} />} />
<Legend />
{multipleLines && groupedData ? (
Object.entries(groupedData).map(([key, group]) => (
<Line
key={key}
data={group.data}
type="monotone"
dataKey={dataKey}
stroke={group.color}
strokeWidth={2}
dot={<StatusIndicator />}
activeDot={{ r: 8 }}
isAnimationActive={false}
animationDuration={300}
name={group.name}
/>
))
) : (
<Line
type="monotone"
dataKey={dataKey}
stroke="#8884d8"
strokeWidth={2}
dot={<StatusIndicator />}
activeDot={{ r: 8 }}
isAnimationActive={false}
animationDuration={300}
name={title}
/>
)}
</LineChart>
</ResponsiveContainer> </ResponsiveContainer>
{/* Легенда статусов */} {/* Легенда статусов */}

View File

@ -7,7 +7,7 @@ class MetricsService {
this.subscriptions = new Map(); this.subscriptions = new Map();
this.pendingRequests = new Map(); this.pendingRequests = new Map();
window.addEventListener('beforeunload', this.cleanupAll.bind(this)); window.addEventListener('beforeunload', this.cleanupAll.bind(this));
window.addEventListener('pagehide', this.cleanupAll.bind(this)); window.addEventListener('pagehide', this.cleanupAll.bind(this));
window.addEventListener('beforeunload', () => { window.addEventListener('beforeunload', () => {
this.cleanupAll(); this.cleanupAll();
@ -42,6 +42,7 @@ class MetricsService {
}); });
this.socket.on('metrics-data', ({ metric, data, requestId }) => { this.socket.on('metrics-data', ({ metric, data, requestId }) => {
console.log('Incoming metric update:', metric);
if (requestId && this.pendingRequests.has(requestId)) { if (requestId && this.pendingRequests.has(requestId)) {
const { resolve } = this.pendingRequests.get(requestId); const { resolve } = this.pendingRequests.get(requestId);
resolve(data); resolve(data);
@ -90,7 +91,7 @@ class MetricsService {
subscribeToMetric(metricKey, callback, interval = 5000, filters = {}) { subscribeToMetric(metricKey, callback, interval = 5000, filters = {}) {
this.connectWebSocket(); this.connectWebSocket();
if (!this.subscriptions.has(metricKey)) { if (!this.subscriptions.has(metricKey)) {
this.subscriptions.set(metricKey, []); this.subscriptions.set(metricKey, []);
const [metric] = metricKey.split('?'); const [metric] = metricKey.split('?');
@ -100,10 +101,10 @@ class MetricsService {
filters filters
}); });
} }
const callbacks = this.subscriptions.get(metricKey); const callbacks = this.subscriptions.get(metricKey);
callbacks.push(callback); callbacks.push(callback);
return () => { return () => {
this.unsubscribeFromMetric(metricKey, callback); this.unsubscribeFromMetric(metricKey, callback);
}; };

View File

@ -30,9 +30,6 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => {
const [isLiveUpdating, setIsLiveUpdating] = useState(false); const [isLiveUpdating, setIsLiveUpdating] = useState(false);
const [showLogs, setShowLogs] = useState(false); const [showLogs, setShowLogs] = useState(false);
const [statusLogs, setStatusLogs] = useState([]); const [statusLogs, setStatusLogs] = useState([]);
const MAX_POINTS = 50;
const TIME_WINDOW_MS = 3600 * 1000; // 1 час в миллисекундах
const getSubscriptionKey = () => { const getSubscriptionKey = () => {
const filterParts = []; const filterParts = [];
@ -56,32 +53,16 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => {
.sort((a, b) => a.timestamp - b.timestamp); .sort((a, b) => a.timestamp - b.timestamp);
}; };
const calculateStep = (startTime, endTime, maxPoints = 10000) => { const downsampleData = (data, maxPoints = 500) => {
const durationSeconds = (endTime.getTime() - startTime.getTime()) / 1000; if (data.length <= maxPoints) return data;
return Math.max(Math.ceil(durationSeconds / maxPoints), 1);
const ratio = Math.ceil(data.length / maxPoints);
return data.filter((_, index) => index % ratio === 0);
}; };
const downsampleData = (data, maxPoints = MAX_POINTS) => { const calculateStep = (startTime, endTime, maxPoints = 10000) => {
if (data.length <= maxPoints) return [...data]; const seconds = (endTime.getTime() - startTime.getTime()) / 1000;
return Math.max(Math.ceil(seconds / maxPoints), 1); // в секундах
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;
}; };
@ -119,15 +100,9 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => {
extendedFilters extendedFilters
); );
const formattedData = formatMetricData(data)
.sort((a, b) => a.timestamp - b.timestamp);
// Применяем ограничение по количеству точек только для исторических данных const formattedData = downsampleData(formatMetricData(data), 100); //КОЛИЧЕСТВО ТОЧЕК НА ГРАФИКЕ
const limitedData = formattedData.length > MAX_POINTS if (formattedData.length > 0) {
? formattedData.slice(-MAX_POINTS)
: formattedData;
if (limitedData.length > 0) {
setMetricMeta({ setMetricMeta({
type: data[0]?.type, type: data[0]?.type,
description: data[0]?.description || description, description: data[0]?.description || description,
@ -136,7 +111,7 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => {
}); });
} }
setChartData(limitedData); setChartData(formattedData);
} catch (err) { } catch (err) {
console.error(`Error loading historical data for ${metricName}:`, err); console.error(`Error loading historical data for ${metricName}:`, err);
setError(err.message); setError(err.message);
@ -151,38 +126,21 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => {
setIsLoading(true); setIsLoading(true);
const end = new Date(); const end = new Date();
const start = new Date(end.getTime() - TIME_WINDOW_MS); const start = new Date(end.getTime() - 3600 * 1000);
fetchHistoricalData(start, end).finally(() => setIsLoading(false)); fetchHistoricalData(start, end).finally(() => setIsLoading(false));
return metricsService.subscribeToMetric( return metricsService.subscribeToMetric(
getSubscriptionKey(), getSubscriptionKey(),
(newData) => { (newData) => {
const formattedData = formatMetricData(newData);
setChartData(prev => { setChartData(prev => {
const now = Date.now(); const newChartData = [...prev, ...formattedData]
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) .filter((v, i, a) => a.findIndex(t => t.timestamp === v.timestamp) === i)
.sort((a, b) => a.timestamp - b.timestamp); .slice(-200);
return newChartData;
// Если точек слишком много, равномерно прореживаем
if (mergedData.length > MAX_POINTS) {
const step = Math.ceil(mergedData.length / MAX_POINTS);
return mergedData.filter((_, index) => index % step === 0);
}
return mergedData;
}); });
}, },
1000, // Уменьшаем интервал обновления до 1 секунды 5000,
{ {
...filters, ...filters,
...(device && { device }), ...(device && { device }),
@ -191,7 +149,6 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => {
); );
}; };
const stopRealtimeUpdates = () => { const stopRealtimeUpdates = () => {
setIsLiveUpdating(false); setIsLiveUpdating(false);
metricsService.unsubscribeFromMetric(getSubscriptionKey()); metricsService.unsubscribeFromMetric(getSubscriptionKey());

View File

@ -10,7 +10,6 @@ import menuData from "../TreeChart/menuData.json";
import SidebarMenuWrapper from "./SidebarMenuWrapper"; import SidebarMenuWrapper from "./SidebarMenuWrapper";
import MetricTabContent from "./MetricTabContent"; import MetricTabContent from "./MetricTabContent";
import ProfileMenu from "../UI/ProfileMenu"; import ProfileMenu from "../UI/ProfileMenu";
import AIAnalysisButton from "../UI/AIAnalysisButton";
const DashboardContainer = styled(Box)(({ theme }) => ({ const DashboardContainer = styled(Box)(({ theme }) => ({
display: 'flex', display: 'flex',
@ -141,13 +140,9 @@ const Dashboard = ({ isDarkMode, setIsDarkMode, user, onLogout }) => {
top: 12, top: 12,
right: 20, right: 20,
zIndex: (theme) => theme.zIndex.tooltip + 10, zIndex: (theme) => theme.zIndex.tooltip + 10,
pointerEvents: 'auto', pointerEvents: 'auto'
display: 'flex', }}
gap: 1,
alignItems: 'center'
}}//ВРЕМЕННОЕ РАСПОЛОЖЕНИЕ КНОПКИ
> >
<AIAnalysisButton />
<ProfileMenu user={user} onLogout={onLogout} /> <ProfileMenu user={user} onLogout={onLogout} />
</Box> </Box>

View File

@ -115,7 +115,7 @@ const MetricRangeEditor = ({ onSave }) => {
const loadRanges = useCallback(async () => { const loadRanges = useCallback(async () => {
try { try {
setLoading(true); setLoading(true);
const res = await axios.get(`/api/ranges/list`); const res = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/ranges/list`);
setRanges( setRanges(
Object.entries(res.data).map(([name, r]) => ({ Object.entries(res.data).map(([name, r]) => ({
name, name,
@ -184,7 +184,7 @@ const MetricRangeEditor = ({ onSave }) => {
const saveChanges = useCallback(async () => { const saveChanges = useCallback(async () => {
try { try {
setLoading(true); setLoading(true);
await axios.post(`/api/ranges/update`, ranges); await axios.post(`${import.meta.env.VITE_BACK_URL}/api/ranges/update`, ranges);
setHasChanges(false); setHasChanges(false);
setSuccess(true); setSuccess(true);
setTimeout(() => setSuccess(false), 3000); setTimeout(() => setSuccess(false), 3000);

View File

@ -46,11 +46,11 @@ const SidebarFooter = ({
const handleSettingsClose = () => { const handleSettingsClose = () => {
setSettingsOpen(false); setSettingsOpen(false);
}; };
/*console.log('SidebarFooter user with role:', { console.log('SidebarFooter user with role:', {
...user, ...user,
hasRole: 'role' in user, hasRole: 'role' in user,
roleValue: user?.role roleValue: user?.role
}); */ });
return ( return (
<> <>
<FooterList> <FooterList>

View File

@ -44,7 +44,7 @@ const SidebarMenuWrapper = ({ isDarkMode, setIsDarkMode, onMenuSelect, user }) =
setLoading(true); setLoading(true);
const headers = lastModified ? { 'If-Modified-Since': lastModified } : {}; const headers = lastModified ? { 'If-Modified-Since': lastModified } : {};
const response = await axios.get(`/api/menu/full`, { const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/menu/full`, {
headers, headers,
validateStatus: status => status === 200 || status === 304 validateStatus: status => status === 200 || status === 304
}); });
@ -78,13 +78,13 @@ const SidebarMenuWrapper = ({ isDarkMode, setIsDarkMode, onMenuSelect, user }) =
const checkForUpdates = async () => { const checkForUpdates = async () => {
try { try {
setBackgroundLoading(true); setBackgroundLoading(true);
const response = await axios.get(`/api/menu/check-updates`, { const response = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/menu/check-updates`, {
headers: { 'If-Modified-Since': lastModified } headers: { 'If-Modified-Since': lastModified }
}); });
if (response.data.hasUpdates) { if (response.data.hasUpdates) {
// Если есть обновления, загружаем их в фоне // Если есть обновления, загружаем их в фоне
const updateResponse = await axios.get(`/api/menu/full`); const updateResponse = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/menu/full`);
setMenuData(updateResponse.data); setMenuData(updateResponse.data);
setLastModified(updateResponse.headers['last-modified']); setLastModified(updateResponse.headers['last-modified']);
@ -108,7 +108,7 @@ const SidebarMenuWrapper = ({ isDarkMode, setIsDarkMode, onMenuSelect, user }) =
const handleSaveChanges = async (updatedItem) => { const handleSaveChanges = async (updatedItem) => {
try { try {
const response = await axios.put( const response = await axios.put(
`/api/menu/${updatedItem.id}`, `${import.meta.env.VITE_BACK_URL}/api/menu/${updatedItem.id}`,
updatedItem, updatedItem,
{ {
headers: { headers: {

View File

@ -1,151 +0,0 @@
import React, { useState } from 'react';
import {
Button,
CircularProgress,
Alert,
Box,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
Typography,
IconButton
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import axios from 'axios';
const AIAnalysisButton = ({ onAnalysisComplete }) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [result, setResult] = useState(null);
const [openModal, setOpenModal] = useState(false);
const handleAnalyze = async () => {
setLoading(true);
setError(null);
setResult(null);
try {
// 1. Получаем данные из ClickHouse
console.log('Запрашиваем данные из ClickHouse...');
const metricsResponse = await axios.get('/api/clickhouse');
console.log('Получены данные из ClickHouse:', metricsResponse.data);
// 2. Отправляем в AI API
console.log('Отправляем данные в AI API:', metricsResponse.data);
const aiResponse = await axios.post(
'/ai-api/api/metrics/rest',
metricsResponse.data,
{
headers: {
'Content-Type': 'application/json',
},
}
);
console.log('Ответ от AI API:', aiResponse.data);
setResult(aiResponse.data);
setOpenModal(true);
if (onAnalysisComplete) {
onAnalysisComplete(aiResponse.data);
}
} catch (err) {
console.error("Детали ошибки:", err.response?.data);
setError(err.response?.data?.message || JSON.stringify(err.response?.data)) || "Ошибка при анализе данных";
} finally {
setLoading(false);
}
};
const handleCloseModal = () => {
setOpenModal(false);
};
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Button
variant="contained"
color="primary"
onClick={handleAnalyze}
disabled={loading}
startIcon={loading ? <CircularProgress size={20} /> : null}
sx={{
minWidth: '180px',
backgroundColor: '#4caf50',
'&:hover': {
backgroundColor: '#388e3c',
}
}}
>
{loading ? 'Отправка в AI...' : 'Проанализировать AI'}
</Button>
{error && (
<Alert severity="error" sx={{ mt: 1 }}>
{error}
</Alert>
)}
{/* Модальное окно с результатом */}
<Dialog
open={openModal}
onClose={handleCloseModal}
fullWidth={true}
maxWidth="lg"
scroll="paper"
>
<DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
Результат AI-анализа
<IconButton
aria-label="close"
onClick={handleCloseModal}
sx={{
color: (theme) => theme.palette.grey[500],
}}
>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent dividers>
{result ? (
<>
<Typography variant="h6" gutterBottom>Данные анализа:</Typography>
<Box
component="pre"
sx={{
p: 2,
bgcolor: '#f5f5f5',
borderRadius: 1,
overflow: 'auto',
maxHeight: '60vh',
whiteSpace: 'pre-wrap',
wordWrap: 'break-word'
}}
>
{JSON.stringify(result, null, 2)}
</Box>
</>
) : (
<DialogContentText>Нет данных для отображения</DialogContentText>
)}
</DialogContent>
<DialogActions>
<Button onClick={handleCloseModal}>Закрыть</Button>
<Button
onClick={() => {
navigator.clipboard.writeText(JSON.stringify(result, null, 2));
alert('Результат скопирован в буфер обмена');
}}
>
Копировать
</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export default AIAnalysisButton;

View File

@ -24,8 +24,7 @@ const LoginModal = ({ onLogin, onClose }) => {
e.preventDefault(); e.preventDefault();
try { try {
const { data } = await axios.post( const { data } = await axios.post(
//`${import.meta.env.VITE_BACK_URL}/api/auth/login`, `${import.meta.env.VITE_BACK_URL}/api/auth/login`,
'/api/auth/login',
{ login: username, password }, { login: username, password },
{ {
withCredentials: true, withCredentials: true,

View File

@ -1,12 +1,12 @@
import React from 'react'; import React from 'react';
export const RoleBasedRender = ({ user, allowedRoles, children }) => { export const RoleBasedRender = ({ user, allowedRoles, children }) => {
// console.log('RoleBasedRender check:', { console.log('RoleBasedRender check:', {
// user, user,
// hasRole: user?.role, hasRole: user?.role,
// allowedRoles, allowedRoles,
// hasAccess: user && allowedRoles.includes(user.role) hasAccess: user && allowedRoles.includes(user.role)
// }); });
if (!user || !allowedRoles.includes(user.role)) { if (!user || !allowedRoles.includes(user.role)) {
return null; return null;

View File

@ -1,10 +1,7 @@
import axios from "axios"
export const checkAuth = async () => { export const checkAuth = async () => {
try { try {
const { data } = await axios.get( const { data } = await axios.get(
//`${import.meta.env.VITE_BACK_URL}/api/auth/check`, `${import.meta.env.VITE_BACK_URL}/api/auth/check`,
'/api/auth/check',
{ {
withCredentials: true, withCredentials: true,
headers: { headers: {

View File

@ -44,12 +44,12 @@ const MetricsAnalyzer = () => {
setError(null); setError(null);
// 1. Сначала загружаем метрики // 1. Сначала загружаем метрики
const metricsResponse = await axios.get(`/api/metrics/all-values`); const metricsResponse = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/metrics/all-values`);
setMetrics(metricsResponse.data); setMetrics(metricsResponse.data);
// 2. Преобразуем и отправляем на анализ // 2. Преобразуем и отправляем на анализ
const requestData = transformMetricsForAnalysis(metricsResponse.data); const requestData = transformMetricsForAnalysis(metricsResponse.data);
const analysisResponse = await axios.get(`:5134/api/metrics/rest`, { const analysisResponse = await axios.get(`${import.meta.env.VITE_BACK_URL}:5134/api/metrics/rest`, {
data: requestData, data: requestData,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@ -1,7 +1,8 @@
import SystemStatusChart from "../../Charts/SystemStatusChart";
import TreeTable from "../UI/TreeTable"; import TreeTable from "../UI/TreeTable";
import FlowChart from "../TreeChart/FlowChart"; import FlowChart from "../TreeChart/FlowChart";
import { getStatusColor } from "../TreeChart/dataUtils"; import { getStatusColor } from "../TreeChart/dataUtils";
import SystemChart from "../../Charts/SystemChart"; import MetricsAnalyzer from "./MetricsAnalyzer"; // Импортируем новый компонент
const TabContent = ({ activeTab, tabs, statusHistories, treeData1, tabContent, handleOpenTab }) => { const TabContent = ({ activeTab, tabs, statusHistories, treeData1, tabContent, handleOpenTab }) => {
const countStatuses = (data) => { const countStatuses = (data) => {
@ -16,48 +17,24 @@ const TabContent = ({ activeTab, tabs, statusHistories, treeData1, tabContent, h
} }
}; };
if (data) countRecursive(data); countRecursive(data);
return counts; return counts;
}; };
if (activeTab === "Главная") { if (activeTab === "Главная") {
const statusCounts = countStatuses(treeData1); const statusCounts = treeData1 ? countStatuses(treeData1) : { green: 0, yellow: 0, orange: 0, red: 0 };
// Конфигурация для метрики серверов (с несколькими линиями)
const serverMetric = {
name: "zvks_server_li",
title: "Надежность системы",
description: "Уровень надежности системы",
multipleLines: true,
lineKey: "device",
};
// Конфигурация для метрики приложений (одна линия)
const appMetric = {
name: "zvks_application_li",
title: "Функциональность системы",
description: "Уровень функциональности системы",
multipleLines: false
};
return ( return (
<div> <div>
<h2 style={{ textAlign: 'center' }}>Общий мониторинг состояния системы</h2> <h2 style={{ textAlign: 'center' }}>Общий мониторинг состояния системы</h2>
<div> <div>
<div style={{ display: 'inline-block', width: '48%', marginRight: '2%' }}> <div style={{ display: 'inline-block', width: '48%', marginRight: '2%' }}>
<label>Надежность серверов</label> <label>Надежность системы</label>
<SystemChart <SystemStatusChart data={statusHistories.history1} />
metricInfo={serverMetric}
chartHeight={580}
/>
</div> </div>
<div style={{ display: 'inline-block', width: '48%' }}> <div style={{ display: 'inline-block', width: '48%' }}>
<label>Функциональность приложений</label> <label>Функциональность системы</label>
<SystemChart <SystemStatusChart data={statusHistories.history2} />
metricInfo={appMetric}
chartHeight={580}
/>
</div> </div>
</div> </div>
@ -88,6 +65,9 @@ const TabContent = ({ activeTab, tabs, statusHistories, treeData1, tabContent, h
<label>Статус компонентов системы</label> <label>Статус компонентов системы</label>
<TreeTable data={treeData1} /> <TreeTable data={treeData1} />
{/* Добавляем кнопку анализа
<MetricsAnalyzer />*/}
</div> </div>
); );
} else if (activeTab === "Визуализация") { } else if (activeTab === "Визуализация") {

View File

@ -2,21 +2,14 @@ import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import svgr from 'vite-plugin-svgr' import svgr from 'vite-plugin-svgr'
// https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react(), svgr()], plugins: [
react(),
svgr()
],
server: { server: {
host: true, host: true,
allowedHosts: ['dev.msf.enode', 'demo-msf.kis-npo.ru'], allowedHosts: ['dev.msf.enode', 'demo-msf.kis-npo.ru']
proxy: {
'/ai-api': {
target: 'http://192.168.2.39:5134',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/ai-api/, ''),
},
'/api': {
target: 'http://192.168.2.39:3000',
changeOrigin: true,
}
}
} }
}); })