fixed a bug with collapsing the menu, improved the MUI skeleton, fixed several visual bugs

pull/40/head
DmitriyA 2025-04-22 08:59:35 -04:00
parent fd53b187d5
commit e47161acd1
9 changed files with 156 additions and 85 deletions

View File

@ -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>
); );

View File

@ -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 (

View File

@ -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>

View File

@ -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>

View File

@ -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 || "Неверный логин или пароль");

View File

@ -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'
}}> }}>

View File

@ -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 };
} }

View File

@ -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>
); );
}; };

View File

@ -139,7 +139,7 @@ export const darkTheme = createTheme({
// Фоновые цвета // Фоновые цвета
background: { background: {
default: "#1E1E1E", // Основной фон приложения default: "#2d2d2d", // Основной фон приложения
paper: "#2d2d2d", // Фон "бумажных" поверхностей paper: "#2d2d2d", // Фон "бумажных" поверхностей
}, },