authorization-token #38
|
|
@ -27,7 +27,8 @@
|
|||
"reactflow": "^11.11.4",
|
||||
"vite-plugin-svgr": "^4.3.0",
|
||||
"react-scripts": "^5.0.1",
|
||||
"socket.io-client": "^4.8.1"
|
||||
"socket.io-client": "^4.8.1",
|
||||
"antd": "^5.24.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
|
|
|
|||
104
src/App.jsx
104
src/App.jsx
|
|
@ -1,37 +1,109 @@
|
|||
import React, { useState, useMemo } from "react";
|
||||
import { ThemeProvider, CssBaseline, Switch, Box } from "@mui/material";
|
||||
import React, { useState, useMemo, useEffect } from "react";
|
||||
import { ThemeProvider, CssBaseline, Switch, Box, CircularProgress } from "@mui/material";
|
||||
import Dashboard from "./Components/Layout/Dashboard";
|
||||
import LoginModal from "./Components/UI/LoginModal";
|
||||
import { lightTheme, darkTheme } from "./Style/theme";
|
||||
import Logo from './assets/images/logo.svg?react'; // Импорт как компонента
|
||||
import Logo from './assets/images/logo.svg?react';
|
||||
import { checkAuth } from "./Components/UI/auth";
|
||||
import axios from 'axios';
|
||||
|
||||
function App() {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [showLoginModal, setShowLoginModal] = useState(true);
|
||||
const [authState, setAuthState] = useState({
|
||||
isAuthenticated: false,
|
||||
isLoading: true,
|
||||
user: null
|
||||
});
|
||||
const [showLoginModal, setShowLoginModal] = useState(false);
|
||||
const [isDarkMode, setIsDarkMode] = useState(
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
);
|
||||
|
||||
const theme = useMemo(() => (isDarkMode ? darkTheme : lightTheme), [isDarkMode]);
|
||||
|
||||
const handleLogin = () => {
|
||||
setIsAuthenticated(true);
|
||||
useEffect(() => {
|
||||
const verifyAuth = async () => {
|
||||
try {
|
||||
const authStatus = await checkAuth();
|
||||
|
||||
setAuthState({
|
||||
isAuthenticated: authStatus.isAuthenticated,
|
||||
isLoading: false,
|
||||
user: authStatus.user || null
|
||||
});
|
||||
|
||||
setShowLoginModal(!authStatus.isAuthenticated);
|
||||
} catch (error) {
|
||||
console.error('Auth verification error:', error);
|
||||
setAuthState({
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
user: null
|
||||
});
|
||||
setShowLoginModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
verifyAuth();
|
||||
}, []);
|
||||
|
||||
const handleLogin = (userData) => {
|
||||
setAuthState({
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
user: userData
|
||||
});
|
||||
setShowLoginModal(false);
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
|
|
||||
await axios.get(`${import.meta.env.VITE_BACK_URL}/api/metrics`, {
|
||||
method: 'POST',
|
||||
credentials: 'include'
|
||||
});
|
||||
localStorage.removeItem('access_token');
|
||||
setAuthState({
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
user: null
|
||||
});
|
||||
setShowLoginModal(true);
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (authState.isLoading) {
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100vh',
|
||||
flexDirection: 'column',
|
||||
gap: 2
|
||||
}}>
|
||||
<CircularProgress />
|
||||
<p>Проверка авторизации...</p>
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
{!isAuthenticated && showLoginModal ? (
|
||||
{!authState.isAuthenticated && showLoginModal ? (
|
||||
<>
|
||||
{/* Логотип */}
|
||||
<Box
|
||||
component="div"
|
||||
sx={{
|
||||
position: "fixed",
|
||||
top: 24,
|
||||
left: "50%", // Сдвигаем начало логотипа в центр
|
||||
transform: "translateX(-50%)", // Смещаем назад на половину ширины логотипа
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
zIndex: 1200,
|
||||
'& svg': {
|
||||
width: 400,
|
||||
|
|
@ -55,9 +127,15 @@ function App() {
|
|||
bgcolor: "background.default",
|
||||
color: "text.primary"
|
||||
}}>
|
||||
<Dashboard />
|
||||
<Dashboard
|
||||
user={authState.user}
|
||||
onLogout={handleLogout}
|
||||
/>
|
||||
<Box sx={{ position: "absolute", top: 10, right: 10 }}>
|
||||
<Switch checked={isDarkMode} onChange={() => setIsDarkMode((prev) => !prev)} />
|
||||
<Switch
|
||||
checked={isDarkMode}
|
||||
onChange={() => setIsDarkMode((prev) => !prev)}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ const LineChartComponent = ({
|
|||
{instanceKeys.map((instance, index) => (
|
||||
<Line
|
||||
key={instance}
|
||||
type="monotone"
|
||||
type=""
|
||||
dataKey={instance}
|
||||
name={instance}
|
||||
stroke={colors[index % colors.length]}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useEffect, useState, useRef, useCallback } from 'react';
|
||||
import { io } from 'socket.io-client';
|
||||
import { webSocketManager } from './WebSocketManager';
|
||||
import LineChartComponent from './Components/LineChartComponent';
|
||||
import { TimeRangeSelector } from './Components/TimeRangeSelector';
|
||||
import { ConnectionStatusIndicator } from './Components/ConnectionStatusIndicator';
|
||||
|
|
@ -19,7 +19,6 @@ const PrometheusChart = ({ metricName }) => {
|
|||
const [isSelectingRange, setIsSelectingRange] = useState(false);
|
||||
const [lastCustomRange, setLastCustomRange] = useState(null);
|
||||
const intervalRef = useRef(null);
|
||||
const socketRef = useRef(null);
|
||||
const debounceRef = useRef(null);
|
||||
|
||||
const formatTime = useCallback((timestamp, rangeSeconds) => {
|
||||
|
|
@ -70,25 +69,6 @@ const PrometheusChart = ({ metricName }) => {
|
|||
return 1800; // > 24 часов
|
||||
}, []);
|
||||
|
||||
const fetchData = useCallback(() => {
|
||||
|
||||
if (isSelectingRange) return;
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const start = now - selectedRange.value;
|
||||
const end = now;
|
||||
const step = calculateStep(start, end);
|
||||
|
||||
if (socketRef.current?.connected) {
|
||||
socketRef.current.emit('get-metrics', {
|
||||
metric: metricName,
|
||||
start,
|
||||
end,
|
||||
step,
|
||||
_t: Date.now()
|
||||
});
|
||||
}
|
||||
}, [metricName, selectedRange.value, isSelectingRange]);
|
||||
|
||||
const processMetricsData = useCallback((response) => {
|
||||
console.log('Processing metrics data:', response);
|
||||
|
|
@ -137,58 +117,32 @@ const PrometheusChart = ({ metricName }) => {
|
|||
});
|
||||
}, [metricName, selectedRange.value, formatTime, useCustomRange, startDate, endDate]);
|
||||
|
||||
const setupWebSocket = useCallback(() => {
|
||||
if (socketRef.current) {
|
||||
// Если соединение уже существует, возвращаем его
|
||||
if (socketRef.current.connected) return socketRef.current;
|
||||
// Если соединение в процессе переподключения, тоже возвращаем
|
||||
if (socketRef.current.reconnecting) return socketRef.current;
|
||||
}
|
||||
//VITE_BACK_WS_URL
|
||||
const socket = io(`${import.meta.env.VITE_BACK_WS_URL}/api/metrics-ws`, {
|
||||
transports: ['websocket'],
|
||||
reconnection: true,
|
||||
reconnectionAttempts: Infinity,
|
||||
reconnectionDelay: 1000,
|
||||
reconnectionDelayMax: 5000,
|
||||
});
|
||||
const fetchData = useCallback(() => {
|
||||
if (isSelectingRange) return;
|
||||
|
||||
socketRef.current = socket;
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const start = now - selectedRange.value;
|
||||
const end = now;
|
||||
const step = calculateStep(start, end);
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('WebSocket connected');
|
||||
setConnectionStatus('connected');
|
||||
fetchData();
|
||||
});
|
||||
|
||||
socket.on('disconnect', (reason) => {
|
||||
console.log('WebSocket disconnected:', reason);
|
||||
setConnectionStatus('disconnected');
|
||||
if (reason === 'io server disconnect') socket.connect();
|
||||
});
|
||||
|
||||
socket.on('connect_error', (error) => {
|
||||
console.error('WebSocket connection error:', error);
|
||||
setConnectionStatus('error');
|
||||
setTimeout(() => socket.connect(), 1000);
|
||||
});
|
||||
|
||||
socket.on('metrics-data', (response) => {
|
||||
console.log('Received raw metrics data:', response);
|
||||
processMetricsData(response);
|
||||
});
|
||||
|
||||
socket.on('metrics-error', (error) => {
|
||||
console.error('Metrics error:', error);
|
||||
setConnectionStatus('error');
|
||||
});
|
||||
|
||||
return socket;
|
||||
}, []);
|
||||
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 () => {
|
||||
// Добавляем проверку на валидность дат
|
||||
|
Ghost
commented
Review
комментарий убрать комментарий убрать
|
||||
if (!startDate || !endDate || startDate >= endDate) {
|
||||
console.error('Invalid date range');
|
||||
return;
|
||||
}
|
||||
|
||||
const start = Math.floor(startDate.getTime() / 1000);
|
||||
const end = Math.floor(endDate.getTime() / 1000);
|
||||
const end = Math.ceil(endDate.getTime() / 1000); // Используем Math.ceil для конечной даты
|
||||
const rangeSeconds = end - start;
|
||||
|
||||
try {
|
||||
|
|
@ -202,11 +156,11 @@ const PrometheusChart = ({ metricName }) => {
|
|||
});
|
||||
|
||||
if (response.data?.length) {
|
||||
// Преобразуем данные перед передачей в processMetricsData
|
||||
// Добавляем нормализацию timestamp
|
||||
|
Ghost
commented
Review
все комментарии убрать все комментарии убрать
|
||||
const processedData = response.data.map(item => ({
|
||||
...item,
|
||||
timestamp: item.timestamp, // оставляем в секундах - processMetricsData конвертирует
|
||||
value: item.value.toString()
|
||||
timestamp: item.timestamp > 1e12 ? item.timestamp : item.timestamp * 1000,
|
||||
|
Ghost
commented
Review
что такое ie12? Может лучше в отдельную переменную вывести? Что значит item.timestamp * 1000? Может тоже в отдельную переменную? что такое _ie12_? Может лучше в отдельную переменную вывести? Что значит item.timestamp * 1000? Может тоже в отдельную переменную?
|
||||
value: parseFloat(item.value)
|
||||
}));
|
||||
|
||||
processMetricsData({
|
||||
|
|
@ -220,8 +174,7 @@ const PrometheusChart = ({ metricName }) => {
|
|||
}, [metricName, startDate, endDate, calculateStep, processMetricsData]);
|
||||
|
||||
|
||||
const handleRangeChange = useCallback((event) => {
|
||||
// Очищаем текущий интервал
|
||||
const handleRangeChange = useCallback(async (event) => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
|
|
@ -230,9 +183,10 @@ const PrometheusChart = ({ metricName }) => {
|
|||
const selectedValue = event.target.value;
|
||||
const range = TIME_RANGES.find(r => r.value === parseInt(selectedValue, 10));
|
||||
|
||||
// Полный сброс состояния перед загрузкой новых данных
|
||||
|
Ghost
commented
Review
убрать убрать
|
||||
setChartData(null);
|
||||
setSelectedRange(range);
|
||||
setUseCustomRange(false);
|
||||
setChartData(null);
|
||||
setSelectedGraphRange(null);
|
||||
setFilteredData(null);
|
||||
|
||||
|
|
@ -240,20 +194,12 @@ const PrometheusChart = ({ metricName }) => {
|
|||
setEndDate(now);
|
||||
setStartDate(new Date(now.getTime() - range.value * 1000));
|
||||
|
||||
// Переподключение сокета
|
||||
if (!socketRef.current?.connected) {
|
||||
socketRef.current?.connect();
|
||||
}
|
||||
}, []);
|
||||
// Ждем завершения обновления состояния перед загрузкой
|
||||
|
Ghost
commented
Review
убрать убрать
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
const handleCustomRangeChange = useCallback(() => {
|
||||
// Отключаем WebSocket соединение
|
||||
if (socketRef.current?.connected) {
|
||||
socketRef.current.disconnect();
|
||||
setConnectionStatus('disconnected');
|
||||
}
|
||||
|
||||
// Очищаем интервал обновления
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
|
|
@ -266,25 +212,7 @@ const PrometheusChart = ({ metricName }) => {
|
|||
fetchCustomRangeData();
|
||||
}, [fetchCustomRangeData]);
|
||||
|
||||
const handleResetZoom = useCallback(() => {
|
||||
setSelectedGraphRange(null);
|
||||
setFilteredData(null);
|
||||
setIsSelectingRange(false);
|
||||
|
||||
if (useCustomRange) {
|
||||
fetchCustomRangeData();
|
||||
} else {
|
||||
if (!socketRef.current?.connected) {
|
||||
socketRef.current?.connect();
|
||||
}
|
||||
fetchData();
|
||||
}
|
||||
|
||||
if (lastCustomRange) {
|
||||
handleRangeSelect(lastCustomRange);
|
||||
return;
|
||||
}
|
||||
}, [fetchData, fetchCustomRangeData, useCustomRange]);
|
||||
|
||||
const interpolateData = useCallback((data, targetPointCount) => {
|
||||
if (!data || data.length < 2) return data;
|
||||
|
Ghost
commented
Review
если нет data вернуть data? поясни если нет data вернуть data? поясни
DmitriyA
commented
Review
if (!data) || data.length < 2 Интерполяция требует как минимум 2 точки начало и конец,если точек меньше, то интерполировать нечего if (!data)
Проверяет, что data существует не null, не undefined и не false.
Если data нет — функция возвращает data, то есть null/undefined.
|| data.length < 2
или проверяет, что в data меньше 2 элементов (массив пуст или содержит 1 элемент).
Если да — функция возвращает исходный data без изменений.
Интерполяция требует как минимум 2 точки начало и конец,если точек меньше, то интерполировать нечего
|
||||
|
|
@ -337,10 +265,6 @@ const PrometheusChart = ({ metricName }) => {
|
|||
setIsSelectingRange(true);
|
||||
setSelectedGraphRange(range);
|
||||
|
||||
// Отключаем автоматические обновления
|
||||
if (socketRef.current?.connected) {
|
||||
socketRef.current.disconnect();
|
||||
}
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
|
|
@ -366,23 +290,52 @@ const PrometheusChart = ({ metricName }) => {
|
|||
setIsSelectingRange(false);
|
||||
}, [chartData, interpolateData, formatTime]);
|
||||
|
||||
useEffect(() => {
|
||||
const socket = setupWebSocket();
|
||||
return () => {
|
||||
clearInterval(intervalRef.current);
|
||||
socket.disconnect();
|
||||
};
|
||||
}, [setupWebSocket]);
|
||||
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) => {
|
||||
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, processMetricsData]);
|
||||
|
||||
// Обновим useEffect для кастомного диапазона
|
||||
useEffect(() => {
|
||||
if (useCustomRange && !isSelectingRange) {
|
||||
// Очищаем предыдущий таймер
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current);
|
||||
}
|
||||
|
||||
// Устанавливаем новый таймер с задержкой 500 мс
|
||||
debounceRef.current = setTimeout(() => {
|
||||
fetchCustomRangeData();
|
||||
}, 500);
|
||||
|
|
@ -406,12 +359,11 @@ const PrometheusChart = ({ metricName }) => {
|
|||
}
|
||||
};
|
||||
|
||||
// Очищаем предыдущий интервал
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
}
|
||||
|
||||
// Запускаем сразу и затем по интервалу
|
||||
|
||||
fetchDataWrapper();
|
||||
intervalRef.current = setInterval(fetchDataWrapper, selectedRange.interval);
|
||||
|
||||
|
|
@ -490,5 +442,4 @@ const PrometheusChart = ({ metricName }) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default React.memo(PrometheusChart);
|
||||
|
||||
export default React.memo(PrometheusChart);
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
// src/services/WebSocketManager.js
|
||||
import { io } from 'socket.io-client';
|
||||
|
||||
class WebSocketManager {
|
||||
constructor() {
|
||||
this.socket = null;
|
||||
this.subscribers = new Map();
|
||||
this.connectionStatus = 'disconnected';
|
||||
this.connectionCallbacks = new Set();
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this.socket && (this.socket.connected || this.socket.reconnecting)) {
|
||||
return this.socket;
|
||||
}
|
||||
|
||||
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.notifyConnectionStatus();
|
||||
});
|
||||
|
||||
this.socket.on('disconnect', (reason) => {
|
||||
this.connectionStatus = 'disconnected';
|
||||
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) {
|
||||
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();
|
||||
|
|
@ -48,9 +48,9 @@ const MainContent = styled(Box)(({ theme }) => ({
|
|||
|
||||
const Content = styled(Box)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.custom.modalBackground,
|
||||
padding: theme.spacing(2.5),
|
||||
//padding: theme.spacing(2.5),
|
||||
borderRadius: '10px',
|
||||
boxShadow: theme.shadows[2],
|
||||
//boxShadow: theme.shadows[2],
|
||||
maxWidth: '100%',
|
||||
overflow: 'auto',
|
||||
color: theme.palette.custom.modalText,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Drawer,
|
||||
List,
|
||||
|
|
@ -15,6 +15,7 @@ import {
|
|||
} from "@mui/icons-material";
|
||||
import MenuItem from "./SidebarMenuComponents/MenuItem";
|
||||
import SidebarFooter from "./SidebarMenuComponents/SidebarFooter";
|
||||
import { statusManager1 } from "../TreeChart/dataUtils";
|
||||
|
||||
const SidebarResizer = styled('div')(({ theme }) => ({
|
||||
width: "5px",
|
||||
|
|
@ -34,6 +35,17 @@ const SidebarResizer = styled('div')(({ theme }) => ({
|
|||
const SidebarMenu = ({ data, onOpenTab, sidebarWidth, startResizing }) => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [menuData, setMenuData] = useState(data);
|
||||
|
||||
// Обновляем статусы при изменении данных
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
// Создаем глубокую копию данных, чтобы не мутировать исходные
|
||||
const dataCopy = JSON.parse(JSON.stringify(data));
|
||||
statusManager1.updateStatuses(dataCopy);
|
||||
setMenuData(dataCopy);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const handleToggleCollapse = () => {
|
||||
setCollapsed(!collapsed);
|
||||
|
|
@ -126,11 +138,13 @@ const SidebarMenu = ({ data, onOpenTab, sidebarWidth, startResizing }) => {
|
|||
Меню
|
||||
</Typography>
|
||||
)}
|
||||
<MenuItem
|
||||
item={data}
|
||||
onSelectItem={handleSelectItem}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
{menuData && (
|
||||
<MenuItem
|
||||
item={menuData}
|
||||
onSelectItem={handleSelectItem}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
)}
|
||||
</List>
|
||||
|
||||
{/* Футер */}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,14 @@ import {
|
|||
styled
|
||||
} from "@mui/material";
|
||||
import { ExpandLess, ExpandMore, Folder, FolderOpen } from "@mui/icons-material";
|
||||
import { getStatusColor } from "../../TreeChart/dataUtils";
|
||||
|
||||
|
||||
|
||||
const StyledListItem = styled(ListItem)(({ theme, level }) => ({
|
||||
cursor: "pointer",
|
||||
paddingLeft: theme.spacing(2 + level * 2),
|
||||
position: 'relative', // Добавляем для позиционирования индикатора
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
},
|
||||
|
|
@ -20,6 +24,17 @@ const StyledListItem = styled(ListItem)(({ theme, level }) => ({
|
|||
},
|
||||
}));
|
||||
|
||||
const StatusIndicator = styled('div')(({ theme, status }) => ({
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: '4px',
|
||||
backgroundColor: status ? getStatusColor(status) : 'transparent',
|
||||
borderTopRightRadius: '4px',
|
||||
borderBottomRightRadius: '4px',
|
||||
}));
|
||||
|
||||
const IconWrapper = styled('div')(({ theme }) => ({
|
||||
cursor: "pointer",
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
|
|
@ -33,7 +48,6 @@ const MenuItem = ({ item, onSelectItem, level = 0, collapsed }) => {
|
|||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const hasChildren = Array.isArray(item.items) && item.items.length > 0;
|
||||
|
||||
|
||||
const handleToggle = (e) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(!isOpen);
|
||||
|
|
@ -52,17 +66,20 @@ const MenuItem = ({ item, onSelectItem, level = 0, collapsed }) => {
|
|||
onClick={hasChildren ? handleToggle : handleOpenTab}
|
||||
level={level}
|
||||
sx={{
|
||||
pl: collapsed ? 2 : 2 + level * 2, // Адаптируем отступы
|
||||
pl: collapsed ? 2 : 2 + level * 2,
|
||||
justifyContent: collapsed ? 'center' : 'flex-start',
|
||||
}}
|
||||
>
|
||||
{/* Индикатор статуса */}
|
||||
{!collapsed && <StatusIndicator status={item.status} />}
|
||||
|
||||
<ListItemIcon sx={{ minWidth: collapsed ? 'auto' : 56 }}>
|
||||
<IconWrapper onClick={handleOpenTab}>
|
||||
{hasChildren ? (isOpen ? <FolderOpen /> : <Folder />) : <Folder />}
|
||||
</IconWrapper>
|
||||
</ListItemIcon>
|
||||
|
||||
{!collapsed && ( // Показываем текст только в развернутом состоянии
|
||||
{!collapsed && (
|
||||
<>
|
||||
<ListItemText
|
||||
primary={item.title}
|
||||
|
|
@ -75,7 +92,7 @@ const MenuItem = ({ item, onSelectItem, level = 0, collapsed }) => {
|
|||
)}
|
||||
</StyledListItem>
|
||||
|
||||
{hasChildren && !collapsed && ( // Показываем детей только в развернутом состоянии
|
||||
{hasChildren && !collapsed && (
|
||||
<Collapse in={isOpen} timeout="auto" unmountOnExit>
|
||||
<List component="div" disablePadding>
|
||||
{item.items.map((child, index) => (
|
||||
|
|
@ -94,7 +111,6 @@ const MenuItem = ({ item, onSelectItem, level = 0, collapsed }) => {
|
|||
);
|
||||
};
|
||||
|
||||
// Вспомогательная функция (остается без изменений)
|
||||
const getAllChildren = (node) => {
|
||||
let children = [];
|
||||
if (node.items && node.items.length > 0) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
import React from "react";
|
||||
import { styled } from "@mui/material";
|
||||
|
||||
const StatusIndicator = styled('div')(({ theme, status }) => ({
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: '4px',
|
||||
backgroundColor: getStatusColor(status),
|
||||
borderRadius: '0 2px 2px 0',
|
||||
transition: 'background-color 0.3s ease'
|
||||
}));
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case 'red': return '#F44336';
|
||||
case 'orange': return '#FF9800';
|
||||
case 'yellow': return '#cebd21';
|
||||
case 'green': return '#4CAF50';
|
||||
default: return 'transparent';
|
||||
}
|
||||
};
|
||||
|
||||
export default StatusIndicator;
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
import React, { lazy, Suspense } from "react";
|
||||
import Skeleton from '@mui/material/Skeleton';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
const PrometheusChart = lazy(() => import('../../Charts/PrometheusChart'));
|
||||
import LazyChartBatchRenderer from "../hooks/LazyChartBatchRender";
|
||||
|
|
@ -22,9 +24,32 @@ const getAllChildIds = (node) => {
|
|||
return ids;
|
||||
};
|
||||
|
||||
// Компонент Skeleton для графика
|
||||
const ChartSkeleton = () => (
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Skeleton variant="text" width="60%" height={30} /> {/* Заголовок */}
|
||||
<Skeleton variant="rectangular" width="100%" height={300} sx={{ mt: 2 }} /> {/* График */}
|
||||
</Box>
|
||||
);
|
||||
|
||||
// Компонент Skeleton для родительского контейнера
|
||||
const ContainerSkeleton = () => (
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Skeleton variant="text" width="40%" height={40} /> {/* Заголовок */}
|
||||
<Skeleton variant="text" width="80%" height={20} sx={{ mt: 1 }} /> {/* Описание */}
|
||||
{/* Место для дочерних элементов */}
|
||||
<Box sx={{ mt: 2 }}>
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<ChartSkeleton key={i} />
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const tabContent = (data) => {
|
||||
const tabContent = {};
|
||||
|
||||
// Функция для рекурсивного обхода и сбора данных
|
||||
// Функция для рекурсивного обхода и сбора данных
|
||||
const generateContent = (nodes) => {
|
||||
nodes.forEach((node) => {
|
||||
|
|
@ -36,9 +61,11 @@ const tabContent = (data) => {
|
|||
const content = (
|
||||
<div>
|
||||
<h2>{node.title}</h2>
|
||||
<LazyChartBatchRenderer charts={node.items.map((child) => tabContent[child.id].content)} />
|
||||
<Suspense fallback={<ContainerSkeleton />}>
|
||||
<LazyChartBatchRenderer charts={node.items.map((child) => tabContent[child.id].content)} />
|
||||
</Suspense>
|
||||
<p>Контент для {node.title}.</p>
|
||||
{childrenContent}
|
||||
{/*childrenContent*/}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
@ -53,7 +80,7 @@ const tabContent = (data) => {
|
|||
const content = (
|
||||
<div key={node.id}>
|
||||
<h3>{node.title}</h3> {/* Используем title узла */}
|
||||
<Suspense fallback={<div>Загрузка графика...</div>}>
|
||||
<Suspense fallback={<ChartSkeleton />}>
|
||||
<PrometheusChart metricName={metricName} />
|
||||
</Suspense>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import Modal from "./Modal";
|
|||
import "../../Style/LoginModal.css";
|
||||
import Logo from '../../assets/images/logo.svg?react';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import axios from 'axios';
|
||||
|
||||
const LoginModal = ({ onLogin, onClose }) => {
|
||||
const [username, setUsername] = useState("");
|
||||
|
|
@ -16,9 +17,10 @@ const LoginModal = ({ onLogin, onClose }) => {
|
|||
e.preventDefault();
|
||||
|
||||
try {
|
||||
// Отправляем данные на бэкенд
|
||||
const response = await fetch(`${import.meta.env.VITE_BACK_URL}/api/auth/login`, {
|
||||
const response = await axios.post(
|
||||
`${import.meta.env.VITE_BACK_URL}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
|
@ -28,8 +30,9 @@ const LoginModal = ({ onLogin, onClose }) => {
|
|||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
onLogin(); // Успешная авторизация
|
||||
onClose(); // Закрыть модальное окно
|
||||
localStorage.setItem('access_token', data.access_token);
|
||||
onLogin(data.user); // Передаем данные пользователя
|
||||
onClose();
|
||||
} else {
|
||||
setError(data.message || "Неверный логин или пароль");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
import axios from 'axios';
|
||||
|
||||
export const checkAuth = async () => {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`${import.meta.env.VITE_BACK_URL}/api/auth/check`,
|
||||
{
|
||||
withCredentials: true, // аналог `credentials: 'include'` в fetch
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('access_token') || ''}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// У axios нет свойства .ok, проверяем статус 200-299
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return response.data; // Данные уже в JSON, не нужно .json()
|
||||
} else {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Auth check failed:', err);
|
||||
return { isAuthenticated: false };
|
||||
}
|
||||
};
|
||||
|
|
@ -76,7 +76,7 @@ export const lightTheme = createTheme({
|
|||
|
||||
// Фоновые цвета
|
||||
background: {
|
||||
default: "#6CACE4", // Основной фон приложения
|
||||
default: "#FFFFFF", // Основной фон приложения
|
||||
|
Ghost
commented
Review
BACKGROUND_COLOR = #FFFFFF BACKGROUND_COLOR = #FFFFFF
|
||||
paper: "#FFFFFF", // Фон "бумажных" поверхностей (карточек, панелей)
|
||||
},
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
почему не в .env?