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