Merge pull request 'rc' (#52) from rc into main

Reviewed-on: http://git.enode/deployer3000/trust-module-frontend/pulls/52
proxy 1.0.19
deployer3000 2025-07-21 13:20:26 +03:00
commit 8fcace10b1
15 changed files with 567 additions and 165 deletions

View File

@ -5,6 +5,7 @@ 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({
@ -22,23 +23,75 @@ function App() {
useEffect(() => { useEffect(() => {
const verifyAuth = async () => { const verifyAuth = async () => {
try { try {
const savedToken = localStorage.getItem('access_token');
// Если есть токен, но нет пользователя - делаем запрос к серверу
if (savedToken && !localStorage.getItem('user')) {
const authStatus = await checkAuth();
handleAuthResponse(authStatus);
return;
}
// Если есть сохраненный пользователь
const savedUser = JSON.parse(localStorage.getItem('user'));
if (savedUser && savedToken) {
// Если у сохраненного пользователя нет роли - запрашиваем свежие данные
if (!savedUser.role) {
const authStatus = await checkAuth();
handleAuthResponse(authStatus);
} else {
setAuthState({
isAuthenticated: true,
isLoading: false,
user: savedUser
});
setShowLoginModal(false);
}
return;
}
// Стандартная проверка авторизации
const authStatus = await checkAuth(); const authStatus = await checkAuth();
setAuthState({ handleAuthResponse(authStatus);
isAuthenticated: authStatus.isAuthenticated,
isLoading: false,
user: authStatus.user || null
});
setShowLoginModal(!authStatus.isAuthenticated);
} catch (error) { } catch (error) {
console.error('Auth verification error:', error); console.error('Auth verification error:', error);
setAuthState({ handleAuthFailure();
isAuthenticated: false,
isLoading: false,
user: null
});
setShowLoginModal(true);
} }
}; };
const handleAuthResponse = (authStatus) => {
if (authStatus.isAuthenticated && authStatus.user?.role) {
const userToSave = {
id: authStatus.user.id,
login: authStatus.user.login,
role: authStatus.user.role
};
console.log('Saving user:', userToSave);
localStorage.setItem('user', JSON.stringify(userToSave));
setAuthState({
isAuthenticated: true,
isLoading: false,
user: userToSave
});
setShowLoginModal(false);
} else {
handleAuthFailure();
}
};
const handleAuthFailure = () => {
localStorage.removeItem('user');
localStorage.removeItem('access_token');
setAuthState({
isAuthenticated: false,
isLoading: false,
user: null
});
setShowLoginModal(true);
};
verifyAuth(); verifyAuth();
}, []); }, []);
@ -46,29 +99,33 @@ function App() {
setAuthState({ setAuthState({
isAuthenticated: true, isAuthenticated: true,
isLoading: false, isLoading: false,
user: userData user: {
id: userData.id,
login: userData.login,
role: userData.role
}
}); });
setShowLoginModal(false); setShowLoginModal(false);
}; };
const handleLogout = async () => { const handleLogout = async () => {
try { try {
await axios.post(`${import.meta.env.VITE_BACK_URL}/api/auth/logout`, null, { await axios.post(`${import.meta.env.VITE_BACK_URL}/api/auth/logout`, null, {
withCredentials: true, // чтобы отправлялись куки withCredentials: true,
}); });
localStorage.removeItem('access_token');
setAuthState({
isAuthenticated: false,
isLoading: false,
user: null,
});
setShowLoginModal(true);
} catch (error) {
console.error('Logout failed:', error);
}
};
localStorage.removeItem('access_token');
localStorage.removeItem('user');
setAuthState({
isAuthenticated: false,
isLoading: false,
user: null,
});
setShowLoginModal(true);
} catch (error) {
console.error('Logout failed:', error);
}
};
// Полноэкранный лоадер во время проверки авторизации // Полноэкранный лоадер во время проверки авторизации
if (authState.isLoading) { if (authState.isLoading) {
return ( return (

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { statusConfig } from '../../Components/Layout/SettingsComponents/statusConfig';
import { import {
Table, Table,
TableBody, TableBody,
@ -11,15 +12,6 @@ import {
Typography Typography
} from '@mui/material'; } from '@mui/material';
// Используем те же цвета, что и в LineChartComponent
const statusColors = {
'0': '#757575', // серый (нет связи)
'1': '#4CAF50', // зеленый (норма)
'2': '#FFC107', // желтый (отклонение)
'3': '#FF9800', // оранжевый (критично)
'4': '#F44336' // красный (авария)
};
const StatusLogTable = ({ logs }) => { const StatusLogTable = ({ logs }) => {
return ( return (
<TableContainer component={Paper} sx={{ mt: 2, maxHeight: 400 }}> <TableContainer component={Paper} sx={{ mt: 2, maxHeight: 400 }}>
@ -44,10 +36,10 @@ const StatusLogTable = ({ logs }) => {
<TableCell>{log.source_id?.split('$')[1]}</TableCell> <TableCell>{log.source_id?.split('$')[1]}</TableCell>
<TableCell> <TableCell>
<Chip <Chip
label={getStatusText(log.status)} label={statusConfig.getStatusText(log.status)}
style={{ style={{
backgroundColor: statusColors[log.status], backgroundColor: statusConfig.getStatusColor(log.status),
color: '#ffffff', // белый текст для лучшей читаемости color: '#ffffff',
fontWeight: 'bold', fontWeight: 'bold',
border: 'none' border: 'none'
}} }}
@ -57,7 +49,7 @@ const StatusLogTable = ({ logs }) => {
<TableCell>{parseFloat(log.value).toFixed(2)}</TableCell> <TableCell>{parseFloat(log.value).toFixed(2)}</TableCell>
<TableCell> <TableCell>
<Typography variant="body2"> <Typography variant="body2">
{log.description || getStatusDescription(log.status)} {log.description || statusConfig.getStatusDescription(log.status)}
</Typography> </Typography>
</TableCell> </TableCell>
</TableRow> </TableRow>
@ -68,27 +60,4 @@ const StatusLogTable = ({ logs }) => {
); );
}; };
// Вспомогательные функции (оставляем без изменений)
const getStatusText = (status) => {
const statusMap = {
'0': 'Нет соединения',
'1': 'Норма',
'2': 'Отклонение',
'3': 'Критично',
'4': 'Авария'
};
return statusMap[status] || 'Неизвестно';
};
const getStatusDescription = (status) => {
const descriptions = {
'0': 'Устройство не отвечает',
'1': 'Параметры в норме',
'2': 'Обнаружены отклонения от нормы',
'3': 'Критическое состояние системы',
'4': 'Аварийное состояние системы'
};
return descriptions[status] || 'Статус неизвестен';
};
export default StatusLogTable; export default StatusLogTable;

View File

@ -92,13 +92,13 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => {
}; };
const step = calculateStep(start, end); const step = calculateStep(start, end);
const data = await metricsService.fetchMetricsRange( const data = await metricsService.fetchMetricsRange(
metricName, metricName,
Math.floor(start.getTime() / 1000), Math.floor(start.getTime() / 1000),
Math.floor(end.getTime() / 1000), Math.floor(end.getTime() / 1000),
step, step,
extendedFilters extendedFilters
); );
const formattedData = downsampleData(formatMetricData(data), 100); //КОЛИЧЕСТВО ТОЧЕК НА ГРАФИКЕ const formattedData = downsampleData(formatMetricData(data), 100); //КОЛИЧЕСТВО ТОЧЕК НА ГРАФИКЕ
@ -256,12 +256,6 @@ const data = await metricsService.fetchMetricsRange(
source_id source_id
}} }}
ranges={ranges} ranges={ranges}
/*ranges={ranges.length > 0 ? ranges : [
{ min: 0, max: 60, status: 1 },
{ min: 60, max: 80, status: 2 },
{ min: 80, max: 90, status: 3 },
{ min: 90, max: 100, status: 4 }
]}*/
/> />
{showLogs && ( {showLogs && (
<StatusLogTable logs={statusLogs} /> <StatusLogTable logs={statusLogs} />

View File

@ -8,9 +8,9 @@ import useSidebarResize from "../hooks/useSidebarResize";
import TabContent from "../hooks/TabContent"; import TabContent from "../hooks/TabContent";
import menuData from "../TreeChart/menuData.json"; import menuData from "../TreeChart/menuData.json";
import SidebarMenuWrapper from "./SidebarMenuWrapper"; import SidebarMenuWrapper from "./SidebarMenuWrapper";
import MetricTabContent from "./MetricTabContent" import MetricTabContent from "./MetricTabContent";
import ProfileMenu from "../UI/ProfileMenu";
// Стилизованные компоненты
const DashboardContainer = styled(Box)(({ theme }) => ({ const DashboardContainer = styled(Box)(({ theme }) => ({
display: 'flex', display: 'flex',
height: '100vh', height: '100vh',
@ -37,7 +37,7 @@ const Content = styled(Box)(({ theme }) => ({
color: theme.palette.custom.modalText, color: theme.palette.custom.modalText,
})); }));
const Dashboard = ({ isDarkMode, setIsDarkMode }) => { const Dashboard = ({ isDarkMode, setIsDarkMode, user, onLogout }) => {
const { tabs, activeTab, handleOpenTab, handleCloseTab, setActiveTab } = useTabs("Главная"); const { tabs, activeTab, handleOpenTab, handleCloseTab, setActiveTab } = useTabs("Главная");
const [tabContent, setTabContent] = useState({}); const [tabContent, setTabContent] = useState({});
const [treeData1, setTreeData1] = useState(menuData); const [treeData1, setTreeData1] = useState(menuData);
@ -117,7 +117,7 @@ const Dashboard = ({ isDarkMode, setIsDarkMode }) => {
} else { } else {
setActiveTab(tabId); setActiveTab(tabId);
} }
}; };
// Вспомогательная функция для получения всех дочерних элементов // Вспомогательная функция для получения всех дочерних элементов
const getAllChildren = (node) => { const getAllChildren = (node) => {
@ -133,11 +133,25 @@ const Dashboard = ({ isDarkMode, setIsDarkMode }) => {
return ( return (
<DashboardContainer> <DashboardContainer>
<Box
sx={{
position: 'fixed',
top: 12,
right: 20,
zIndex: (theme) => theme.zIndex.tooltip + 10,
pointerEvents: 'auto'
}}
>
<ProfileMenu user={user} onLogout={onLogout} />
</Box>
{/* Сайдбар */} {/* Сайдбар */}
<SidebarMenuWrapper <SidebarMenuWrapper
isDarkMode={isDarkMode} isDarkMode={isDarkMode}
setIsDarkMode={setIsDarkMode} setIsDarkMode={setIsDarkMode}
onMenuSelect={handleMenuSelect} onMenuSelect={handleMenuSelect}
user={user}
/> />
{/* Основной контент */} {/* Основной контент */}

View File

@ -1,12 +1,13 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react'; import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { import {
TextField, Box, Typography, IconButton, Divider, TextField, Box, Typography, IconButton, Divider,
CircularProgress, Alert, Collapse, Tooltip, Button CircularProgress, Alert, Collapse, Tooltip, Button, Select, MenuItem
} from '@mui/material'; } from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
import SearchIcon from '@mui/icons-material/Search'; import SearchIcon from '@mui/icons-material/Search';
import axios from 'axios'; import axios from 'axios';
import { statusConfig } from './statusConfig';
import { VariableSizeList as List } from 'react-window'; import { VariableSizeList as List } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
@ -30,7 +31,7 @@ const MetricItem = React.memo(({ metric, index, updateRange, addRange, deleteRan
sx={{ sx={{
display: 'flex', display: 'flex',
gap: 2, gap: 2,
alignItems: 'center', alignItems: 'flex-end', // Изменено с 'center' на 'flex-end'
mt: 1, mt: 1,
'& > *': { flex: 1 } '& > *': { flex: 1 }
}} }}
@ -51,19 +52,38 @@ const MetricItem = React.memo(({ metric, index, updateRange, addRange, deleteRan
size="small" size="small"
variant="standard" variant="standard"
/> />
<TextField <Select
label="Статус" label="Статус"
type="number"
value={r.status} value={r.status}
onChange={(e) => updateRange(index, j, 'status', e.target.value)} onChange={(e) => updateRange(index, j, 'status', e.target.value)}
size="small" size="small"
variant="standard" variant="standard"
/> sx={{
// Добавляем вертикальное выравнивание для label
'& .MuiInputLabel-root': {
transform: 'translate(0, -20px) scale(0.75)'
},
// Корректируем положение выбранного значения
'& .MuiSelect-select': {
paddingBottom: '8px'
}
}}
>
{statusConfig.getAvailableStatuses().map(({ value, text }) => (
<MenuItem key={value} value={value}>
{text}
</MenuItem>
))}
</Select>
<Tooltip title="Удалить диапазон"> <Tooltip title="Удалить диапазон">
<IconButton <IconButton
onClick={() => deleteRange(index, j)} onClick={() => deleteRange(index, j)}
size="small" size="small"
sx={{ flex: 'none' }} sx={{
flex: 'none',
// Корректируем положение иконки
marginBottom: '8px'
}}
> >
<DeleteIcon fontSize="small" /> <DeleteIcon fontSize="small" />
</IconButton> </IconButton>
@ -238,7 +258,6 @@ const MetricRangeEditor = ({ onSave }) => {
{!loading && ( {!loading && (
<> <>
<Box sx={{ display: 'flex', alignItems: 'flex-end', gap: 1, mb: 2 }}> <Box sx={{ display: 'flex', alignItems: 'flex-end', gap: 1, mb: 2 }}>
<SearchIcon sx={{ color: 'action.active', mr: 1 }} />
<TextField <TextField
label="Поиск по метрике" label="Поиск по метрике"
fullWidth fullWidth
@ -246,9 +265,15 @@ const MetricRangeEditor = ({ onSave }) => {
onChange={(e) => setFilter(e.target.value)} onChange={(e) => setFilter(e.target.value)}
variant="standard" variant="standard"
/> />
<SearchIcon sx={{ color: 'action.active', mr: 1 }} />
</Box> </Box>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', mb: 3 }}> <Box sx={{
display: 'flex',
gap: 2,
alignItems: 'flex-end', // меняем с 'center' на 'flex-end'
mb: 3
}}>
<TextField <TextField
label="Новая метрика" label="Новая метрика"
value={newMetricName} value={newMetricName}
@ -262,38 +287,27 @@ const MetricRangeEditor = ({ onSave }) => {
color="primary" color="primary"
disabled={!newMetricName.trim()} disabled={!newMetricName.trim()}
> >
<AddIcon /> <AddIcon sx={{ color: 'action.active' }} />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</Box> </Box>
<Divider sx={{ mb: 3 }} /> <Divider sx={{ mb: 3 }} />
<Box sx={{ height: '60vh', width: '100%' }}> <Box sx={{ maxHeight: '60vh', overflowY: 'auto', pr: 1 }}>
<AutoSizer> {filtered.map((metric, index) => (
{({ height, width }) => ( <MetricItem
<List key={metric.name}
height={height} metric={metric}
width={width} index={index}
itemSize={getItemSize} updateRange={updateRange}
itemCount={filtered.length} addRange={addRange}
> deleteRange={deleteRange}
{({ index, style }) => ( />
<Box style={style} sx={{ p: 1 }}> ))}
<MetricItem
metric={filtered[index]}
index={index}
updateRange={updateRange}
addRange={addRange}
deleteRange={deleteRange}
/>
</Box>
)}
</List>
)}
</AutoSizer>
</Box> </Box>
{filtered.length === 0 && ( {filtered.length === 0 && (
<Typography color="text.secondary" textAlign="center" py={3}> <Typography color="text.secondary" textAlign="center" py={3}>
{filter ? 'Ничего не найдено' : 'Нет метрик для отображения'} {filter ? 'Ничего не найдено' : 'Нет метрик для отображения'}

View File

@ -0,0 +1,27 @@
export const statusConfig = {
statusMap: {
'0': { text: 'Нет соединения', color: '#757575', description: 'Устройство не отвечает' },
'1': { text: 'Норма', color: '#4CAF50', description: 'Параметры в норме' },
'2': { text: 'Отклонение', color: '#FFC107', description: 'Обнаружены отклонения от нормы' },
'3': { text: 'Критично', color: '#FF9800', description: 'Критическое состояние системы' },
'4': { text: 'Авария', color: '#F44336', description: 'Аварийное состояние системы' }
},
getStatusText(status) {
return this.statusMap[status]?.text || 'Неизвестно';
},
getStatusColor(status) {
return this.statusMap[status]?.color || '#757575';
},
getStatusDescription(status) {
return this.statusMap[status]?.description || 'Статус неизвестен';
},
getAvailableStatuses() {
return Object.entries(this.statusMap)
.filter(([key]) => key !== '0') // исключаем статус "Нет соединения"
.map(([value, config]) => ({ value, text: config.text }));
}
};

View File

@ -21,7 +21,8 @@ const SidebarMenu = ({
isDarkMode, isDarkMode,
setIsDarkMode, setIsDarkMode,
onSelectItem, onSelectItem,
forceRefreshMenu forceRefreshMenu,
user
}) => { }) => {
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const { sidebarWidth, startResizing } = useSidebarResize(290); const { sidebarWidth, startResizing } = useSidebarResize(290);
@ -148,6 +149,7 @@ const SidebarMenu = ({
isDarkMode={isDarkMode} isDarkMode={isDarkMode}
setIsDarkMode={setIsDarkMode} setIsDarkMode={setIsDarkMode}
forceRefreshMenu={forceRefreshMenu} forceRefreshMenu={forceRefreshMenu}
user={user}
/> />
</Box> </Box>
{!collapsed && ( {!collapsed && (

View File

@ -1,4 +1,3 @@
// components/SidebarMenuComponents/SidebarFooter.jsx
import React, { useState } from "react"; import React, { useState } from "react";
import { Brightness4, Brightness7 } from "@mui/icons-material"; import { Brightness4, Brightness7 } from "@mui/icons-material";
import { IconButton, Tooltip } from "@mui/material"; import { IconButton, Tooltip } from "@mui/material";
@ -12,6 +11,7 @@ import {
Button Button
} from "@mui/material"; } from "@mui/material";
import SettingsModal from "../SettingsModal"; import SettingsModal from "../SettingsModal";
import { RoleBasedRender } from "../../UI/RoleBasedRender";
const FooterList = styled(List)(({ theme }) => ({ const FooterList = styled(List)(({ theme }) => ({
backgroundColor: theme.palette.custom.sidebar, backgroundColor: theme.palette.custom.sidebar,
@ -30,7 +30,13 @@ const FooterListItem = styled(ListItem)(({ theme }) => ({
alignItems: 'center' alignItems: 'center'
})); }));
const SidebarFooter = ({ collapsed, isDarkMode, setIsDarkMode, forceRefreshMenu }) => { const SidebarFooter = ({
collapsed,
isDarkMode,
setIsDarkMode,
forceRefreshMenu,
user
}) => {
const [settingsOpen, setSettingsOpen] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false);
const handleSettingsOpen = () => { const handleSettingsOpen = () => {
@ -40,7 +46,11 @@ const SidebarFooter = ({ collapsed, isDarkMode, setIsDarkMode, forceRefreshMenu
const handleSettingsClose = () => { const handleSettingsClose = () => {
setSettingsOpen(false); setSettingsOpen(false);
}; };
console.log('SidebarFooter user with role:', {
...user,
hasRole: 'role' in user,
roleValue: user?.role
});
return ( return (
<> <>
<FooterList> <FooterList>
@ -56,26 +66,29 @@ const SidebarFooter = ({ collapsed, isDarkMode, setIsDarkMode, forceRefreshMenu
</FooterListItem> </FooterListItem>
)} )}
<FooterListItem> <FooterListItem>
{!collapsed && ( {/* кнопка настроек */}
<Button <RoleBasedRender user={user} allowedRoles={['admin']}>
onClick={handleSettingsOpen} {!collapsed && (
sx={{ <Button
color: 'custom.sidebarText', onClick={handleSettingsOpen}
textTransform: 'none', sx={{
minWidth: 0,
padding: 0,
marginRight: 'auto'
}}
>
<ListItemText
primary="Настройки"
primaryTypographyProps={{
color: 'custom.sidebarText', color: 'custom.sidebarText',
variant: 'body2' textTransform: 'none',
minWidth: 0,
padding: 0,
marginRight: 'auto'
}} }}
/> >
</Button> <ListItemText
)} primary="Настройки"
primaryTypographyProps={{
color: 'custom.sidebarText',
variant: 'body2'
}}
/>
</Button>
)}
</RoleBasedRender>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Tooltip title="Переключить тему"> <Tooltip title="Переключить тему">
@ -98,9 +111,14 @@ const SidebarFooter = ({ collapsed, isDarkMode, setIsDarkMode, forceRefreshMenu
</FooterListItem> </FooterListItem>
</FooterList> </FooterList>
<SettingsModal open={settingsOpen} {/* Используем RoleBasedRender для модального окна */}
onClose={handleSettingsClose} <RoleBasedRender user={user} allowedRoles={['admin']}>
onMenuUpdate={forceRefreshMenu} /> <SettingsModal
open={settingsOpen}
onClose={handleSettingsClose}
onMenuUpdate={forceRefreshMenu}
/>
</RoleBasedRender>
</> </>
); );
}; };

View File

@ -3,7 +3,7 @@ import SidebarMenu from './SidebarMenu';
import { Box, CircularProgress, Typography } from '@mui/material'; import { Box, CircularProgress, Typography } from '@mui/material';
import axios from 'axios'; import axios from 'axios';
const SidebarMenuWrapper = ({ isDarkMode, setIsDarkMode, onMenuSelect }) => { const SidebarMenuWrapper = ({ isDarkMode, setIsDarkMode, onMenuSelect, user }) => {
const [menuData, setMenuData] = useState(null); const [menuData, setMenuData] = useState(null);
const [lastModified, setLastModified] = useState(null); const [lastModified, setLastModified] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -177,6 +177,7 @@ const SidebarMenuWrapper = ({ isDarkMode, setIsDarkMode, onMenuSelect }) => {
onCloseEditModal={() => setEditModalOpen(false)} onCloseEditModal={() => setEditModalOpen(false)}
onSaveChanges={handleSaveChanges} onSaveChanges={handleSaveChanges}
forceRefreshMenu={forceRefreshMenu} forceRefreshMenu={forceRefreshMenu}
user={user}
/> />
); );
}; };

View File

@ -1,21 +1,27 @@
import React, { useState } from "react"; import React, { useState } from "react";
import Modal from "./Modal"; import Modal from "./Modal";
import "../../Style/LoginModal.css"; import "../../Style/LoginModal.css";
import Logo from '../../assets/images/logo.svg?react'; import {
import TextField from '@mui/material/TextField'; TextField,
IconButton,
Button,
Typography,
InputAdornment
} from "@mui/material";
import {
Visibility,
VisibilityOff
} from "@mui/icons-material";
import axios from 'axios'; import axios from 'axios';
const LoginModal = ({ onLogin, onClose }) => { const LoginModal = ({ onLogin, onClose }) => {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [error, setError] = useState(""); const [error, setError] = useState("");
const [showPassword, setShowPassword] = React.useState(false); const [showPassword, setShowPassword] = useState(false);
const handleClickShowPassword = () => setShowPassword((show) => !show);
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
try { try {
const { data } = await axios.post( const { data } = await axios.post(
`${import.meta.env.VITE_BACK_URL}/api/auth/login`, `${import.meta.env.VITE_BACK_URL}/api/auth/login`,
@ -28,16 +34,33 @@ const LoginModal = ({ onLogin, onClose }) => {
} }
); );
console.log('Login response:', data);
if (data.success) { if (data.success) {
if (!data.user?.role) {
console.error('Role missing in response:', data);
throw new Error('Роль пользователя не получена');
}
const userData = {
id: data.user.id,
login: data.user.login,
role: data.user.role
};
localStorage.setItem('access_token', data.access_token); localStorage.setItem('access_token', data.access_token);
onLogin(data.user); localStorage.setItem('user', JSON.stringify(userData));
console.log('User data saved:', userData);
onLogin(userData);
onClose(); onClose();
} else { } else {
setError(data.message || "Неверный логин или пароль"); setError(data.message || 'Ошибка авторизации');
} }
} catch (err) { } catch (err) {
console.error('Ошибка при отправке запроса:', err); console.error('Login error:', err);
setError(err.response?.data?.message || "Ошибка при подключении к серверу"); setError(err.response?.data?.message || err.message || 'Ошибка при входе');
} }
}; };
@ -52,8 +75,8 @@ const LoginModal = ({ onLogin, onClose }) => {
variant="filled" variant="filled"
margin="normal" margin="normal"
required required
value={username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
size="normal"
/> />
<TextField <TextField
@ -64,12 +87,45 @@ const LoginModal = ({ onLogin, onClose }) => {
margin="normal" margin="normal"
required required
type={showPassword ? 'text' : 'password'} type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
size="normal" InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={() => setShowPassword(!showPassword)}
edge="end"
sx={{
marginRight: '-12px',
alignSelf: 'flex-end'
}}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
)
}}
/> />
{error && <p className="error">{error}</p>} {error && (
<button type="submit">Войти</button> <Typography color="error" sx={{ mt: 1 }}>
{error}
</Typography>
)}
<Button
type="submit"
variant="contained"
fullWidth
sx={{
mt: 2,
py: 1.5,
fontSize: '1rem'
}}
>
Войти
</Button>
</form> </form>
</Modal> </Modal>
); );

View File

@ -0,0 +1,66 @@
import React, { useState } from 'react';
import {
IconButton, Menu, MenuItem, Avatar, Tooltip, Typography
} from '@mui/material';
const ProfileMenu = ({ user, onLogout }) => {
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const handleOpen = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const handleLogoutClick = () => {
handleClose();
onLogout();
};
return (
<>
<Tooltip title="Профиль">
<IconButton onClick={handleOpen} size="small" sx={{ ml: 2 }}>
<Avatar sx={{ bgcolor: 'primary.main' }}>
{user?.login?.[0]?.toUpperCase() || '?'}
</Avatar>
</IconButton>
</Tooltip>
<Menu
anchorEl={anchorEl}
open={open}
onClose={handleClose}
onClick={handleClose}
PaperProps={{
elevation: 3,
sx: {
mt: 1.5,
minWidth: 180,
},
}}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
>
<MenuItem disabled>
<Typography variant="body2" color="text.secondary">
{user?.login || 'Неизвестный'}
</Typography>
</MenuItem>
<MenuItem onClick={handleLogoutClick}>
Выйти
</MenuItem>
</Menu>
</>
);
};
export default ProfileMenu;

View File

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

View File

@ -1,5 +1,3 @@
import axios from 'axios';
export const checkAuth = async () => { export const checkAuth = async () => {
try { try {
const { data } = await axios.get( const { data } = await axios.get(
@ -12,7 +10,20 @@ export const checkAuth = async () => {
} }
); );
return data; console.log('Auth check response:', data);
if (!data.user) {
return { isAuthenticated: false };
}
return {
isAuthenticated: data.isAuthenticated,
user: {
id: data.user.id,
login: data.user.login,
role: data.user.role
}
};
} catch (err) { } catch (err) {
console.error('Auth check failed:', err); console.error('Auth check failed:', err);
return { isAuthenticated: false }; return { isAuthenticated: false };

View File

@ -0,0 +1,156 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import {
Button,
Typography,
Paper,
Box,
CircularProgress,
Alert,
Snackbar,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow
} from '@mui/material';
const MetricsAnalyzer = () => {
const [loading, setLoading] = useState(false);
const [metrics, setMetrics] = useState([]);
const [analysisResult, setAnalysisResult] = useState(null);
const [error, setError] = useState(null);
const [openSnackbar, setOpenSnackbar] = useState(false);
const transformMetricsForAnalysis = (metrics) => {
return metrics.flatMap(metricResponse =>
metricResponse.data.map(metricData => ({
description: metricData.description,
device: parseInt(metricData.device, 10),
id: metricData.source_id,
name: metricData.__name__,
source: metricData.instance,
status: parseInt(metricData.status, 10),
timestamp: metricData.timestamp,
value: metricData.value.toString()
}))
);
};
const analyzeMetrics = async () => {
try {
setLoading(true);
setError(null);
// 1. Сначала загружаем метрики
const metricsResponse = await axios.get(`${import.meta.env.VITE_BACK_URL}/api/metrics/all-values`);
setMetrics(metricsResponse.data);
// 2. Преобразуем и отправляем на анализ
const requestData = transformMetricsForAnalysis(metricsResponse.data);
const analysisResponse = await axios.get(`${import.meta.env.VITE_BACK_URL}:5134/api/metrics/rest`, {
data: requestData,
headers: {
'Content-Type': 'application/json',
}
});
setAnalysisResult(analysisResponse.data);
setOpenSnackbar(true);
} catch (err) {
const errorMessage = err.response?.data?.message ||
err.message ||
'Ошибка при анализе метрик';
setError(errorMessage);
setOpenSnackbar(true);
} finally {
setLoading(false);
}
};
const handleCloseSnackbar = () => {
setOpenSnackbar(false);
};
return (
<Box sx={{ maxWidth: 800, margin: '0 auto', mt: 4 }}>
<Typography variant="h5" gutterBottom sx={{ mb: 3 }}>
Анализ метрик системы
</Typography>
<Box sx={{ display: 'flex', justifyContent: 'center', mb: 3 }}>
<Button
variant="contained"
color="primary"
onClick={analyzeMetrics}
disabled={loading}
startIcon={loading ? <CircularProgress size={24} /> : null}
size="large"
>
{loading ? 'Выполняется анализ...' : 'Проанализировать метрики'}
</Button>
</Box>
{analysisResult && (
<Paper elevation={3} sx={{ p: 3, mt: 2 }}>
<Typography variant="h6" gutterBottom>
Результаты анализа
</Typography>
{Array.isArray(analysisResult) ? (
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Параметр</TableCell>
<TableCell>Результат</TableCell>
<TableCell>Описание</TableCell>
</TableRow>
</TableHead>
<TableBody>
{analysisResult.map((item, index) => (
<TableRow key={index}>
<TableCell>{item.name || item.parameter}</TableCell>
<TableCell>{item.value || item.result}</TableCell>
<TableCell>{item.description || '-'}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
) : (
<Box sx={{
p: 2,
backgroundColor: 'background.paper',
borderRadius: 1,
maxHeight: 400,
overflow: 'auto'
}}>
<Typography variant="body2" component="pre">
{JSON.stringify(analysisResult, null, 2)}
</Typography>
</Box>
)}
</Paper>
)}
<Snackbar
open={openSnackbar}
autoHideDuration={6000}
onClose={handleCloseSnackbar}
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
>
<Alert
onClose={handleCloseSnackbar}
severity={error ? 'error' : 'success'}
sx={{ width: '100%' }}
>
{error || 'Анализ метрик успешно завершен'}
</Alert>
</Snackbar>
</Box>
);
};
export default MetricsAnalyzer;

View File

@ -2,10 +2,9 @@ import SystemStatusChart from "../../Charts/SystemStatusChart";
import TreeTable from "../UI/TreeTable"; import TreeTable from "../UI/TreeTable";
import FlowChart from "../TreeChart/FlowChart"; import FlowChart from "../TreeChart/FlowChart";
import { getStatusColor } from "../TreeChart/dataUtils"; import { getStatusColor } from "../TreeChart/dataUtils";
import MetricsAnalyzer from "./MetricsAnalyzer"; // Импортируем новый компонент
const TabContent = ({ activeTab, tabs, statusHistories, treeData1, tabContent, handleOpenTab }) => { const TabContent = ({ activeTab, tabs, statusHistories, treeData1, tabContent, handleOpenTab }) => {
// Функция для подсчета количества элементов каждого статуса
const countStatuses = (data) => { const countStatuses = (data) => {
const counts = { green: 0, yellow: 0, orange: 0, red: 0 }; const counts = { green: 0, yellow: 0, orange: 0, red: 0 };
@ -66,6 +65,9 @@ const TabContent = ({ activeTab, tabs, statusHistories, treeData1, tabContent, h
<label>Статус компонентов системы</label> <label>Статус компонентов системы</label>
<TreeTable data={treeData1} /> <TreeTable data={treeData1} />
{/* Добавляем кнопку анализа
<MetricsAnalyzer />*/}
</div> </div>
); );
} else if (activeTab === "Визуализация") { } else if (activeTab === "Визуализация") {