fixed a bug with collapsing the menu, improved the MUI skeleton, fixed several visual bugs
parent
fd53b187d5
commit
e47161acd1
49
src/App.jsx
49
src/App.jsx
|
|
@ -1,11 +1,10 @@
|
||||||
import React, { useState, useMemo, useEffect } from "react";
|
import React, { useState, useMemo, useEffect } from "react";
|
||||||
import { ThemeProvider, CssBaseline, Switch, Box, CircularProgress } from "@mui/material";
|
import { ThemeProvider, CssBaseline, Switch, Box, CircularProgress, Typography } 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 { checkAuth } from "./Components/UI/auth";
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [authState, setAuthState] = useState({
|
const [authState, setAuthState] = useState({
|
||||||
|
|
@ -24,13 +23,11 @@ function App() {
|
||||||
const verifyAuth = async () => {
|
const verifyAuth = async () => {
|
||||||
try {
|
try {
|
||||||
const authStatus = await checkAuth();
|
const authStatus = await checkAuth();
|
||||||
|
|
||||||
setAuthState({
|
setAuthState({
|
||||||
isAuthenticated: authStatus.isAuthenticated,
|
isAuthenticated: authStatus.isAuthenticated,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
user: authStatus.user || null
|
user: authStatus.user || null
|
||||||
});
|
});
|
||||||
|
|
||||||
setShowLoginModal(!authStatus.isAuthenticated);
|
setShowLoginModal(!authStatus.isAuthenticated);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Auth verification error:', error);
|
console.error('Auth verification error:', error);
|
||||||
|
|
@ -42,7 +39,6 @@ function App() {
|
||||||
setShowLoginModal(true);
|
setShowLoginModal(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
verifyAuth();
|
verifyAuth();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -57,7 +53,7 @@ function App() {
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
await axios.get(`${import.meta.env.VITE_BACK_URL}/api/metrics`, {
|
await fetch('http://192.168.2.39:3000/api/auth/logout', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
|
|
@ -73,20 +69,25 @@ function App() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Полноэкранный лоадер во время проверки авторизации
|
||||||
if (authState.isLoading) {
|
if (authState.isLoading) {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0, left: 0, right: 0, bottom: 0,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
height: '100vh',
|
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: 2
|
zIndex: 9999,
|
||||||
|
bgcolor: 'background.default'
|
||||||
}}>
|
}}>
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
<p>Проверка авторизации...</p>
|
<Typography sx={{ mt: 2 }}>
|
||||||
|
Проверка авторизации...
|
||||||
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
|
@ -95,26 +96,20 @@ function App() {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
{!authState.isAuthenticated && showLoginModal ? (
|
{!authState.isAuthenticated ? (
|
||||||
<>
|
<>
|
||||||
<Box
|
<Box sx={{
|
||||||
component="div"
|
|
||||||
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, height: 'auto' }
|
||||||
width: 400,
|
}}>
|
||||||
height: 'auto'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Logo />
|
<Logo />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<LoginModal
|
<LoginModal
|
||||||
|
open={showLoginModal}
|
||||||
onLogin={handleLogin}
|
onLogin={handleLogin}
|
||||||
onClose={() => setShowLoginModal(false)}
|
onClose={() => setShowLoginModal(false)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -124,20 +119,16 @@ function App() {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
height: "100vh",
|
height: "100vh",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
bgcolor: "background.default",
|
bgcolor: "background.default"
|
||||||
color: "text.primary"
|
|
||||||
}}>
|
}}>
|
||||||
<Dashboard
|
<Dashboard user={authState.user} onLogout={handleLogout} />
|
||||||
user={authState.user}
|
|
||||||
onLogout={handleLogout}
|
|
||||||
/>
|
|
||||||
<Box sx={{ position: "absolute", top: 10, right: 10 }}>
|
|
||||||
<Switch
|
<Switch
|
||||||
checked={isDarkMode}
|
checked={isDarkMode}
|
||||||
onChange={() => setIsDarkMode((prev) => !prev)}
|
onChange={() => setIsDarkMode(!isDarkMode)}
|
||||||
|
sx={{ position: "absolute", top: 10, right: 10 }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
|
||||||
)}
|
)}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,37 @@ import { ConnectionStatusIndicator } from './Components/ConnectionStatusIndicato
|
||||||
import { CurrentRangeDisplay } from './Components/CurrentRangeDisplay';
|
import { CurrentRangeDisplay } from './Components/CurrentRangeDisplay';
|
||||||
import { TIME_RANGES, COLORS, SECOND, MINUTE, HOUR, DAY } from './Components/constants';
|
import { TIME_RANGES, COLORS, SECOND, MINUTE, HOUR, DAY } from './Components/constants';
|
||||||
import axios from 'axios';
|
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 PrometheusChart = ({ metricName }) => {
|
||||||
const [chartData, setChartData] = useState(null);
|
const [chartData, setChartData] = useState(null);
|
||||||
|
|
@ -395,11 +426,21 @@ const PrometheusChart = ({ metricName }) => {
|
||||||
}, [selectedGraphRange, chartData, interpolateData]);
|
}, [selectedGraphRange, chartData, interpolateData]);
|
||||||
|
|
||||||
if (chartData === null) {
|
if (chartData === null) {
|
||||||
return <div style={{ padding: '20px', textAlign: 'center' }}>Loading data...</div>;
|
return <ChartSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(chartData).length === 0) {
|
if (Object.keys(chartData).length === 0) {
|
||||||
return <div style={{ padding: '20px', textAlign: 'center' }}>No data available</div>;
|
return (
|
||||||
|
<Box sx={{
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '20px',
|
||||||
|
marginBottom: '20px',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}>
|
||||||
|
No data available
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -58,10 +58,11 @@ const Content = styled(Box)(({ theme }) => ({
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const { tabs, activeTab, handleOpenTab, handleCloseTab, setActiveTab } = useTabs("Главная");
|
const { tabs, activeTab, handleOpenTab, handleCloseTab, setActiveTab } = useTabs("Главная");
|
||||||
const { sidebarWidth, startResizing } = useSidebarResize(250);
|
const { sidebarWidth, startResizing } = useSidebarResize(290);
|
||||||
const [tabContent, setTabContent] = useState({});
|
const [tabContent, setTabContent] = useState({});
|
||||||
const [treeData1, setTreeData1] = useState(menuData);
|
const [treeData1, setTreeData1] = useState(menuData);
|
||||||
const [treeData2, setTreeData2] = useState(menuData);
|
const [treeData2, setTreeData2] = useState(menuData);
|
||||||
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
const [statusHistories, setStatusHistories] = useState({
|
const [statusHistories, setStatusHistories] = useState({
|
||||||
history1: [],
|
history1: [],
|
||||||
history2: [],
|
history2: [],
|
||||||
|
|
@ -103,12 +104,14 @@ const Dashboard = () => {
|
||||||
return (
|
return (
|
||||||
<DashboardContainer>
|
<DashboardContainer>
|
||||||
{/* Сайдбар */}
|
{/* Сайдбар */}
|
||||||
<SidebarWrapper sx={{ width: sidebarWidth }}>
|
<SidebarWrapper sx={{ width: collapsed ? 64 : sidebarWidth }}>
|
||||||
<SidebarMenu
|
<SidebarMenu
|
||||||
data={treeData1}
|
data={treeData1}
|
||||||
onOpenTab={handleOpenTab}
|
onOpenTab={handleOpenTab}
|
||||||
sidebarWidth={sidebarWidth}
|
sidebarWidth={sidebarWidth}
|
||||||
startResizing={startResizing}
|
startResizing={startResizing}
|
||||||
|
collapsed={collapsed}
|
||||||
|
setCollapsed={setCollapsed}
|
||||||
/>
|
/>
|
||||||
<SidebarResizer onMouseDown={startResizing} />
|
<SidebarResizer onMouseDown={startResizing} />
|
||||||
</SidebarWrapper>
|
</SidebarWrapper>
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,7 @@ const SidebarResizer = styled('div')(({ theme }) => ({
|
||||||
zIndex: 2
|
zIndex: 2
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const SidebarMenu = ({ data, onOpenTab, sidebarWidth, startResizing }) => {
|
const SidebarMenu = ({ data, onOpenTab, sidebarWidth, startResizing, collapsed, setCollapsed }) => {
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
const [menuData, setMenuData] = useState(data);
|
const [menuData, setMenuData] = useState(data);
|
||||||
|
|
||||||
|
|
@ -105,11 +104,7 @@ const SidebarMenu = ({ data, onOpenTab, sidebarWidth, startResizing }) => {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{collapsed ? (
|
{collapsed ? <ChevronRight /> : <ChevronLeft />}
|
||||||
hovered ? <ChevronRight /> : <MenuIcon />
|
|
||||||
) : (
|
|
||||||
<ChevronLeft />
|
|
||||||
)}
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,7 @@ const LoginModal = ({ onLogin, onClose }) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(
|
const response = await fetch('http://192.168.2.39:3000/api/auth/login', {
|
||||||
`${import.meta.env.VITE_BACK_URL}/api/auth/login`, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -31,7 +30,7 @@ const LoginModal = ({ onLogin, onClose }) => {
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
localStorage.setItem('access_token', data.access_token);
|
localStorage.setItem('access_token', data.access_token);
|
||||||
onLogin(data.user); // Передаем данные пользователя
|
onLogin(data.user);
|
||||||
onClose();
|
onClose();
|
||||||
} else {
|
} else {
|
||||||
setError(data.message || "Неверный логин или пароль");
|
setError(data.message || "Неверный логин или пароль");
|
||||||
|
|
|
||||||
|
|
@ -324,7 +324,7 @@ const TreeTable = ({ data }) => {
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box component="ul" sx={{
|
<Box component="ul" sx={{
|
||||||
pl: 2,
|
pl: 2,
|
||||||
maxHeight: 200,
|
maxHeight: 400,
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
listStyle: 'none'
|
listStyle: 'none'
|
||||||
}}>
|
}}>
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,20 @@ import axios from 'axios';
|
||||||
|
|
||||||
export const checkAuth = async () => {
|
export const checkAuth = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(
|
const response = await fetch('http://192.168.2.39:3000/api/auth/check', {
|
||||||
`${import.meta.env.VITE_BACK_URL}/api/auth/check`,
|
method: 'GET',
|
||||||
{
|
credentials: 'include', // Важно для отправки cookies
|
||||||
withCredentials: true, // аналог `credentials: 'include'` в fetch
|
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${localStorage.getItem('access_token') || ''}`,
|
'Authorization': `Bearer ${localStorage.getItem('access_token') || ''}`,
|
||||||
},
|
},
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// У axios нет свойства .ok, проверяем статус 200-299
|
if (!response.ok) {
|
||||||
if (response.status >= 200 && response.status < 300) {
|
|
||||||
return response.data; // Данные уже в JSON, не нужно .json()
|
|
||||||
} else {
|
|
||||||
throw new Error('Not authenticated');
|
throw new Error('Not authenticated');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (err) {
|
||||||
console.error('Auth check failed:', err);
|
console.error('Auth check failed:', err);
|
||||||
return { isAuthenticated: false };
|
return { isAuthenticated: false };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,73 @@
|
||||||
import { useEffect, useState } from "react";
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Skeleton from '@mui/material/Skeleton';
|
||||||
|
|
||||||
const LazyChartBatchRenderer = ({ charts, batchSize = 3, delay = 150 }) => {
|
const LazyChartBatchRenderer = ({ charts }) => {
|
||||||
const [visibleCharts, setVisibleCharts] = useState([]);
|
const [visibleIndices, setVisibleIndices] = useState(new Set());
|
||||||
|
const placeholderRefs = useRef([]);
|
||||||
|
|
||||||
|
const ChartSkeleton = () => (
|
||||||
|
<Box sx={{ backgroundColor: '#fff', borderRadius: '8px', padding: '20px', marginBottom: '20px', position: 'relative', height: '500px' }}>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let index = 0;
|
const observer = new IntersectionObserver(
|
||||||
const timer = setInterval(() => {
|
(entries) => {
|
||||||
setVisibleCharts((prev) => [
|
setVisibleIndices((prev) => {
|
||||||
...prev,
|
const updated = new Set(prev);
|
||||||
...charts.slice(index, index + batchSize),
|
entries.forEach((entry) => {
|
||||||
]);
|
const index = parseInt(entry.target.dataset.index, 10);
|
||||||
index += batchSize;
|
if (entry.isIntersecting) {
|
||||||
if (index >= charts.length) clearInterval(timer);
|
updated.add(index);
|
||||||
}, delay);
|
} else {
|
||||||
|
updated.delete(index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root: null,
|
||||||
|
rootMargin: '200px',
|
||||||
|
threshold: 0.1,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return () => clearInterval(timer);
|
placeholderRefs.current.forEach((ref) => {
|
||||||
|
if (ref) observer.observe(ref);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
}, [charts]);
|
}, [charts]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div>
|
||||||
{visibleCharts.map((chart, idx) => (
|
{charts.map((chart, index) => (
|
||||||
<div key={idx}>{chart}</div>
|
<div
|
||||||
|
key={index}
|
||||||
|
ref={(el) => (placeholderRefs.current[index] = el)}
|
||||||
|
data-index={index}
|
||||||
|
>
|
||||||
|
{visibleIndices.has(index) ? chart : <ChartSkeleton />}
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ export const darkTheme = createTheme({
|
||||||
|
|
||||||
// Фоновые цвета
|
// Фоновые цвета
|
||||||
background: {
|
background: {
|
||||||
default: "#1E1E1E", // Основной фон приложения
|
default: "#2d2d2d", // Основной фон приложения
|
||||||
paper: "#2d2d2d", // Фон "бумажных" поверхностей
|
paper: "#2d2d2d", // Фон "бумажных" поверхностей
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue