Compare commits
10 Commits
c99b6add47
...
8fcace10b1
| Author | SHA1 | Date |
|---|---|---|
|
|
8fcace10b1 | |
|
|
140b058f41 | |
|
|
cb030a01d2 | |
|
|
dabdda4afe | |
|
|
6a73bd8104 | |
|
|
87a79f98d7 | |
|
|
15c20f1352 | |
|
|
ef5df6971d | |
|
|
61c623b93d | |
|
|
26276e0360 |
115
src/App.jsx
115
src/App.jsx
|
|
@ -5,6 +5,7 @@ import LoginModal from "./Components/UI/LoginModal";
|
|||
import { lightTheme, darkTheme } from "./Style/theme";
|
||||
import Logo from './assets/images/logo.svg?react';
|
||||
import { checkAuth } from "./Components/UI/auth";
|
||||
import axios from "axios";
|
||||
|
||||
function App() {
|
||||
const [authState, setAuthState] = useState({
|
||||
|
|
@ -22,23 +23,75 @@ function App() {
|
|||
useEffect(() => {
|
||||
const verifyAuth = async () => {
|
||||
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();
|
||||
setAuthState({
|
||||
isAuthenticated: authStatus.isAuthenticated,
|
||||
isLoading: false,
|
||||
user: authStatus.user || null
|
||||
});
|
||||
setShowLoginModal(!authStatus.isAuthenticated);
|
||||
handleAuthResponse(authStatus);
|
||||
} catch (error) {
|
||||
console.error('Auth verification error:', error);
|
||||
setAuthState({
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
user: null
|
||||
});
|
||||
setShowLoginModal(true);
|
||||
handleAuthFailure();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
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();
|
||||
}, []);
|
||||
|
||||
|
|
@ -46,29 +99,33 @@ function App() {
|
|||
setAuthState({
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
user: userData
|
||||
user: {
|
||||
id: userData.id,
|
||||
login: userData.login,
|
||||
role: userData.role
|
||||
}
|
||||
});
|
||||
setShowLoginModal(false);
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await axios.post(`${import.meta.env.VITE_BACK_URL}/api/auth/logout`, null, {
|
||||
withCredentials: true, // чтобы отправлялись куки
|
||||
});
|
||||
|
||||
localStorage.removeItem('access_token');
|
||||
setAuthState({
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
user: null,
|
||||
});
|
||||
setShowLoginModal(true);
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error);
|
||||
}
|
||||
};
|
||||
try {
|
||||
await axios.post(`${import.meta.env.VITE_BACK_URL}/api/auth/logout`, null, {
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
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) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import { statusConfig } from '../../Components/Layout/SettingsComponents/statusConfig';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
|
|
@ -11,15 +12,6 @@ import {
|
|||
Typography
|
||||
} from '@mui/material';
|
||||
|
||||
// Используем те же цвета, что и в LineChartComponent
|
||||
const statusColors = {
|
||||
'0': '#757575', // серый (нет связи)
|
||||
'1': '#4CAF50', // зеленый (норма)
|
||||
'2': '#FFC107', // желтый (отклонение)
|
||||
'3': '#FF9800', // оранжевый (критично)
|
||||
'4': '#F44336' // красный (авария)
|
||||
};
|
||||
|
||||
const StatusLogTable = ({ logs }) => {
|
||||
return (
|
||||
<TableContainer component={Paper} sx={{ mt: 2, maxHeight: 400 }}>
|
||||
|
|
@ -44,10 +36,10 @@ const StatusLogTable = ({ logs }) => {
|
|||
<TableCell>{log.source_id?.split('$')[1]}</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={getStatusText(log.status)}
|
||||
label={statusConfig.getStatusText(log.status)}
|
||||
style={{
|
||||
backgroundColor: statusColors[log.status],
|
||||
color: '#ffffff', // белый текст для лучшей читаемости
|
||||
backgroundColor: statusConfig.getStatusColor(log.status),
|
||||
color: '#ffffff',
|
||||
fontWeight: 'bold',
|
||||
border: 'none'
|
||||
}}
|
||||
|
|
@ -57,7 +49,7 @@ const StatusLogTable = ({ logs }) => {
|
|||
<TableCell>{parseFloat(log.value).toFixed(2)}</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2">
|
||||
{log.description || getStatusDescription(log.status)}
|
||||
{log.description || statusConfig.getStatusDescription(log.status)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</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;
|
||||
|
|
@ -92,13 +92,13 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => {
|
|||
};
|
||||
|
||||
const step = calculateStep(start, end);
|
||||
const data = await metricsService.fetchMetricsRange(
|
||||
metricName,
|
||||
Math.floor(start.getTime() / 1000),
|
||||
Math.floor(end.getTime() / 1000),
|
||||
step,
|
||||
extendedFilters
|
||||
);
|
||||
const data = await metricsService.fetchMetricsRange(
|
||||
metricName,
|
||||
Math.floor(start.getTime() / 1000),
|
||||
Math.floor(end.getTime() / 1000),
|
||||
step,
|
||||
extendedFilters
|
||||
);
|
||||
|
||||
|
||||
const formattedData = downsampleData(formatMetricData(data), 100); //КОЛИЧЕСТВО ТОЧЕК НА ГРАФИКЕ
|
||||
|
|
@ -256,12 +256,6 @@ const data = await metricsService.fetchMetricsRange(
|
|||
source_id
|
||||
}}
|
||||
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 && (
|
||||
<StatusLogTable logs={statusLogs} />
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ import useSidebarResize from "../hooks/useSidebarResize";
|
|||
import TabContent from "../hooks/TabContent";
|
||||
import menuData from "../TreeChart/menuData.json";
|
||||
import SidebarMenuWrapper from "./SidebarMenuWrapper";
|
||||
import MetricTabContent from "./MetricTabContent"
|
||||
import MetricTabContent from "./MetricTabContent";
|
||||
import ProfileMenu from "../UI/ProfileMenu";
|
||||
|
||||
// Стилизованные компоненты
|
||||
const DashboardContainer = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
height: '100vh',
|
||||
|
|
@ -37,7 +37,7 @@ const Content = styled(Box)(({ theme }) => ({
|
|||
color: theme.palette.custom.modalText,
|
||||
}));
|
||||
|
||||
const Dashboard = ({ isDarkMode, setIsDarkMode }) => {
|
||||
const Dashboard = ({ isDarkMode, setIsDarkMode, user, onLogout }) => {
|
||||
const { tabs, activeTab, handleOpenTab, handleCloseTab, setActiveTab } = useTabs("Главная");
|
||||
const [tabContent, setTabContent] = useState({});
|
||||
const [treeData1, setTreeData1] = useState(menuData);
|
||||
|
|
@ -117,7 +117,7 @@ const Dashboard = ({ isDarkMode, setIsDarkMode }) => {
|
|||
} else {
|
||||
setActiveTab(tabId);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Вспомогательная функция для получения всех дочерних элементов
|
||||
const getAllChildren = (node) => {
|
||||
|
|
@ -133,11 +133,25 @@ const Dashboard = ({ isDarkMode, setIsDarkMode }) => {
|
|||
|
||||
return (
|
||||
<DashboardContainer>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
top: 12,
|
||||
right: 20,
|
||||
zIndex: (theme) => theme.zIndex.tooltip + 10,
|
||||
pointerEvents: 'auto'
|
||||
}}
|
||||
>
|
||||
<ProfileMenu user={user} onLogout={onLogout} />
|
||||
</Box>
|
||||
|
||||
{/* Сайдбар */}
|
||||
<SidebarMenuWrapper
|
||||
isDarkMode={isDarkMode}
|
||||
setIsDarkMode={setIsDarkMode}
|
||||
onMenuSelect={handleMenuSelect}
|
||||
user={user}
|
||||
/>
|
||||
|
||||
{/* Основной контент */}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
TextField, Box, Typography, IconButton, Divider,
|
||||
CircularProgress, Alert, Collapse, Tooltip, Button
|
||||
CircularProgress, Alert, Collapse, Tooltip, Button, Select, MenuItem
|
||||
} from '@mui/material';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import axios from 'axios';
|
||||
import { statusConfig } from './statusConfig';
|
||||
import { VariableSizeList as List } from 'react-window';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
|
|
@ -30,7 +31,7 @@ const MetricItem = React.memo(({ metric, index, updateRange, addRange, deleteRan
|
|||
sx={{
|
||||
display: 'flex',
|
||||
gap: 2,
|
||||
alignItems: 'center',
|
||||
alignItems: 'flex-end', // Изменено с 'center' на 'flex-end'
|
||||
mt: 1,
|
||||
'& > *': { flex: 1 }
|
||||
}}
|
||||
|
|
@ -51,19 +52,38 @@ const MetricItem = React.memo(({ metric, index, updateRange, addRange, deleteRan
|
|||
size="small"
|
||||
variant="standard"
|
||||
/>
|
||||
<TextField
|
||||
<Select
|
||||
label="Статус"
|
||||
type="number"
|
||||
value={r.status}
|
||||
onChange={(e) => updateRange(index, j, 'status', e.target.value)}
|
||||
size="small"
|
||||
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="Удалить диапазон">
|
||||
<IconButton
|
||||
onClick={() => deleteRange(index, j)}
|
||||
size="small"
|
||||
sx={{ flex: 'none' }}
|
||||
sx={{
|
||||
flex: 'none',
|
||||
// Корректируем положение иконки
|
||||
marginBottom: '8px'
|
||||
}}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
|
|
@ -238,7 +258,6 @@ const MetricRangeEditor = ({ onSave }) => {
|
|||
{!loading && (
|
||||
<>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-end', gap: 1, mb: 2 }}>
|
||||
<SearchIcon sx={{ color: 'action.active', mr: 1 }} />
|
||||
<TextField
|
||||
label="Поиск по метрике"
|
||||
fullWidth
|
||||
|
|
@ -246,9 +265,15 @@ const MetricRangeEditor = ({ onSave }) => {
|
|||
onChange={(e) => setFilter(e.target.value)}
|
||||
variant="standard"
|
||||
/>
|
||||
<SearchIcon sx={{ color: 'action.active', mr: 1 }} />
|
||||
</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
|
||||
label="Новая метрика"
|
||||
value={newMetricName}
|
||||
|
|
@ -262,38 +287,27 @@ const MetricRangeEditor = ({ onSave }) => {
|
|||
color="primary"
|
||||
disabled={!newMetricName.trim()}
|
||||
>
|
||||
<AddIcon />
|
||||
<AddIcon sx={{ color: 'action.active' }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ mb: 3 }} />
|
||||
|
||||
<Box sx={{ height: '60vh', width: '100%' }}>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
height={height}
|
||||
width={width}
|
||||
itemSize={getItemSize}
|
||||
itemCount={filtered.length}
|
||||
>
|
||||
{({ index, style }) => (
|
||||
<Box style={style} sx={{ p: 1 }}>
|
||||
<MetricItem
|
||||
metric={filtered[index]}
|
||||
index={index}
|
||||
updateRange={updateRange}
|
||||
addRange={addRange}
|
||||
deleteRange={deleteRange}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</List>
|
||||
)}
|
||||
</AutoSizer>
|
||||
<Box sx={{ maxHeight: '60vh', overflowY: 'auto', pr: 1 }}>
|
||||
{filtered.map((metric, index) => (
|
||||
<MetricItem
|
||||
key={metric.name}
|
||||
metric={metric}
|
||||
index={index}
|
||||
updateRange={updateRange}
|
||||
addRange={addRange}
|
||||
deleteRange={deleteRange}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
|
||||
{filtered.length === 0 && (
|
||||
<Typography color="text.secondary" textAlign="center" py={3}>
|
||||
{filter ? 'Ничего не найдено' : 'Нет метрик для отображения'}
|
||||
|
|
|
|||
|
|
@ -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 }));
|
||||
}
|
||||
};
|
||||
|
|
@ -21,7 +21,8 @@ const SidebarMenu = ({
|
|||
isDarkMode,
|
||||
setIsDarkMode,
|
||||
onSelectItem,
|
||||
forceRefreshMenu
|
||||
forceRefreshMenu,
|
||||
user
|
||||
}) => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const { sidebarWidth, startResizing } = useSidebarResize(290);
|
||||
|
|
@ -148,6 +149,7 @@ const SidebarMenu = ({
|
|||
isDarkMode={isDarkMode}
|
||||
setIsDarkMode={setIsDarkMode}
|
||||
forceRefreshMenu={forceRefreshMenu}
|
||||
user={user}
|
||||
/>
|
||||
</Box>
|
||||
{!collapsed && (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// components/SidebarMenuComponents/SidebarFooter.jsx
|
||||
import React, { useState } from "react";
|
||||
import { Brightness4, Brightness7 } from "@mui/icons-material";
|
||||
import { IconButton, Tooltip } from "@mui/material";
|
||||
|
|
@ -12,6 +11,7 @@ import {
|
|||
Button
|
||||
} from "@mui/material";
|
||||
import SettingsModal from "../SettingsModal";
|
||||
import { RoleBasedRender } from "../../UI/RoleBasedRender";
|
||||
|
||||
const FooterList = styled(List)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.custom.sidebar,
|
||||
|
|
@ -30,7 +30,13 @@ const FooterListItem = styled(ListItem)(({ theme }) => ({
|
|||
alignItems: 'center'
|
||||
}));
|
||||
|
||||
const SidebarFooter = ({ collapsed, isDarkMode, setIsDarkMode, forceRefreshMenu }) => {
|
||||
const SidebarFooter = ({
|
||||
collapsed,
|
||||
isDarkMode,
|
||||
setIsDarkMode,
|
||||
forceRefreshMenu,
|
||||
user
|
||||
}) => {
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
|
||||
const handleSettingsOpen = () => {
|
||||
|
|
@ -40,7 +46,11 @@ const SidebarFooter = ({ collapsed, isDarkMode, setIsDarkMode, forceRefreshMenu
|
|||
const handleSettingsClose = () => {
|
||||
setSettingsOpen(false);
|
||||
};
|
||||
|
||||
console.log('SidebarFooter user with role:', {
|
||||
...user,
|
||||
hasRole: 'role' in user,
|
||||
roleValue: user?.role
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<FooterList>
|
||||
|
|
@ -56,26 +66,29 @@ const SidebarFooter = ({ collapsed, isDarkMode, setIsDarkMode, forceRefreshMenu
|
|||
</FooterListItem>
|
||||
)}
|
||||
<FooterListItem>
|
||||
{!collapsed && (
|
||||
<Button
|
||||
onClick={handleSettingsOpen}
|
||||
sx={{
|
||||
color: 'custom.sidebarText',
|
||||
textTransform: 'none',
|
||||
minWidth: 0,
|
||||
padding: 0,
|
||||
marginRight: 'auto'
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary="Настройки"
|
||||
primaryTypographyProps={{
|
||||
{/* кнопка настроек */}
|
||||
<RoleBasedRender user={user} allowedRoles={['admin']}>
|
||||
{!collapsed && (
|
||||
<Button
|
||||
onClick={handleSettingsOpen}
|
||||
sx={{
|
||||
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 }}>
|
||||
<Tooltip title="Переключить тему">
|
||||
|
|
@ -98,9 +111,14 @@ const SidebarFooter = ({ collapsed, isDarkMode, setIsDarkMode, forceRefreshMenu
|
|||
</FooterListItem>
|
||||
</FooterList>
|
||||
|
||||
<SettingsModal open={settingsOpen}
|
||||
onClose={handleSettingsClose}
|
||||
onMenuUpdate={forceRefreshMenu} />
|
||||
{/* Используем RoleBasedRender для модального окна */}
|
||||
<RoleBasedRender user={user} allowedRoles={['admin']}>
|
||||
<SettingsModal
|
||||
open={settingsOpen}
|
||||
onClose={handleSettingsClose}
|
||||
onMenuUpdate={forceRefreshMenu}
|
||||
/>
|
||||
</RoleBasedRender>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import SidebarMenu from './SidebarMenu';
|
|||
import { Box, CircularProgress, Typography } from '@mui/material';
|
||||
import axios from 'axios';
|
||||
|
||||
const SidebarMenuWrapper = ({ isDarkMode, setIsDarkMode, onMenuSelect }) => {
|
||||
const SidebarMenuWrapper = ({ isDarkMode, setIsDarkMode, onMenuSelect, user }) => {
|
||||
const [menuData, setMenuData] = useState(null);
|
||||
const [lastModified, setLastModified] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
@ -177,6 +177,7 @@ const SidebarMenuWrapper = ({ isDarkMode, setIsDarkMode, onMenuSelect }) => {
|
|||
onCloseEditModal={() => setEditModalOpen(false)}
|
||||
onSaveChanges={handleSaveChanges}
|
||||
forceRefreshMenu={forceRefreshMenu}
|
||||
user={user}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,21 +1,27 @@
|
|||
import React, { useState } from "react";
|
||||
import Modal from "./Modal";
|
||||
import "../../Style/LoginModal.css";
|
||||
import Logo from '../../assets/images/logo.svg?react';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import {
|
||||
TextField,
|
||||
IconButton,
|
||||
Button,
|
||||
Typography,
|
||||
InputAdornment
|
||||
} from "@mui/material";
|
||||
import {
|
||||
Visibility,
|
||||
VisibilityOff
|
||||
} from "@mui/icons-material";
|
||||
import axios from 'axios';
|
||||
|
||||
const LoginModal = ({ onLogin, onClose }) => {
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [showPassword, setShowPassword] = React.useState(false);
|
||||
|
||||
const handleClickShowPassword = () => setShowPassword((show) => !show);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
const { data } = await axios.post(
|
||||
`${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.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);
|
||||
onLogin(data.user);
|
||||
localStorage.setItem('user', JSON.stringify(userData));
|
||||
|
||||
console.log('User data saved:', userData);
|
||||
|
||||
onLogin(userData);
|
||||
onClose();
|
||||
} else {
|
||||
setError(data.message || "Неверный логин или пароль");
|
||||
setError(data.message || 'Ошибка авторизации');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Ошибка при отправке запроса:', err);
|
||||
setError(err.response?.data?.message || "Ошибка при подключении к серверу");
|
||||
console.error('Login error:', err);
|
||||
setError(err.response?.data?.message || err.message || 'Ошибка при входе');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -52,8 +75,8 @@ const LoginModal = ({ onLogin, onClose }) => {
|
|||
variant="filled"
|
||||
margin="normal"
|
||||
required
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
size="normal"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
|
|
@ -64,12 +87,45 @@ const LoginModal = ({ onLogin, onClose }) => {
|
|||
margin="normal"
|
||||
required
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
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>}
|
||||
<button type="submit">Войти</button>
|
||||
{error && (
|
||||
<Typography color="error" sx={{ mt: 1 }}>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
fullWidth
|
||||
sx={{
|
||||
mt: 2,
|
||||
py: 1.5,
|
||||
fontSize: '1rem'
|
||||
}}
|
||||
>
|
||||
Войти
|
||||
</Button>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
import axios from 'axios';
|
||||
|
||||
export const checkAuth = async () => {
|
||||
try {
|
||||
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) {
|
||||
console.error('Auth check failed:', err);
|
||||
return { isAuthenticated: false };
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -2,10 +2,9 @@ import SystemStatusChart from "../../Charts/SystemStatusChart";
|
|||
import TreeTable from "../UI/TreeTable";
|
||||
import FlowChart from "../TreeChart/FlowChart";
|
||||
import { getStatusColor } from "../TreeChart/dataUtils";
|
||||
|
||||
import MetricsAnalyzer from "./MetricsAnalyzer"; // Импортируем новый компонент
|
||||
|
||||
const TabContent = ({ activeTab, tabs, statusHistories, treeData1, tabContent, handleOpenTab }) => {
|
||||
// Функция для подсчета количества элементов каждого статуса
|
||||
const countStatuses = (data) => {
|
||||
const counts = { green: 0, yellow: 0, orange: 0, red: 0 };
|
||||
|
||||
|
|
@ -66,6 +65,9 @@ const TabContent = ({ activeTab, tabs, statusHistories, treeData1, tabContent, h
|
|||
|
||||
<label>Статус компонентов системы</label>
|
||||
<TreeTable data={treeData1} />
|
||||
|
||||
{/* Добавляем кнопку анализа
|
||||
<MetricsAnalyzer />*/}
|
||||
</div>
|
||||
);
|
||||
} else if (activeTab === "Визуализация") {
|
||||
|
|
|
|||
Loading…
Reference in New Issue