From 26276e036046a58e95b1a6e266609e0953f39ad0 Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Mon, 7 Jul 2025 03:44:10 -0400 Subject: [PATCH 1/5] Metric editor fix --- .../SettingsComponents/MetricRangeEditor.jsx | 35 +++++++------------ 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/src/Components/Layout/SettingsComponents/MetricRangeEditor.jsx b/src/Components/Layout/SettingsComponents/MetricRangeEditor.jsx index d9135b6..54f03fc 100644 --- a/src/Components/Layout/SettingsComponents/MetricRangeEditor.jsx +++ b/src/Components/Layout/SettingsComponents/MetricRangeEditor.jsx @@ -269,31 +269,20 @@ const MetricRangeEditor = ({ onSave }) => { - - - {({ height, width }) => ( - - {({ index, style }) => ( - - - - )} - - )} - + + {filtered.map((metric, index) => ( + + ))} + {filtered.length === 0 && ( {filter ? 'Ничего не найдено' : 'Нет метрик для отображения'} -- 2.40.1 From 61c623b93dc29a428c0e2d21468a2f5e4be26013 Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Tue, 8 Jul 2025 04:46:12 -0400 Subject: [PATCH 2/5] Range editor update --- src/Charts2/Components/StatusLogTable.jsx | 43 +++---------------- .../SettingsComponents/MetricRangeEditor.jsx | 43 +++++++++++++++---- .../SettingsComponents/statusConfig.jsx | 27 ++++++++++++ 3 files changed, 67 insertions(+), 46 deletions(-) create mode 100644 src/Components/Layout/SettingsComponents/statusConfig.jsx diff --git a/src/Charts2/Components/StatusLogTable.jsx b/src/Charts2/Components/StatusLogTable.jsx index ccffd23..4314ad4 100644 --- a/src/Charts2/Components/StatusLogTable.jsx +++ b/src/Charts2/Components/StatusLogTable.jsx @@ -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 ( @@ -44,10 +36,10 @@ const StatusLogTable = ({ logs }) => { {log.source_id?.split('$')[1]} { {parseFloat(log.value).toFixed(2)} - {log.description || getStatusDescription(log.status)} + {log.description || statusConfig.getStatusDescription(log.status)} @@ -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; \ No newline at end of file diff --git a/src/Components/Layout/SettingsComponents/MetricRangeEditor.jsx b/src/Components/Layout/SettingsComponents/MetricRangeEditor.jsx index 54f03fc..e21dabd 100644 --- a/src/Components/Layout/SettingsComponents/MetricRangeEditor.jsx +++ b/src/Components/Layout/SettingsComponents/MetricRangeEditor.jsx @@ -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" /> - 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 }) => ( + + {text} + + ))} + deleteRange(index, j)} size="small" - sx={{ flex: 'none' }} + sx={{ + flex: 'none', + // Корректируем положение иконки + marginBottom: '8px' + }} > @@ -238,7 +258,6 @@ const MetricRangeEditor = ({ onSave }) => { {!loading && ( <> - { onChange={(e) => setFilter(e.target.value)} variant="standard" /> + - + { color="primary" disabled={!newMetricName.trim()} > - + diff --git a/src/Components/Layout/SettingsComponents/statusConfig.jsx b/src/Components/Layout/SettingsComponents/statusConfig.jsx new file mode 100644 index 0000000..fc06ea8 --- /dev/null +++ b/src/Components/Layout/SettingsComponents/statusConfig.jsx @@ -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 })); + } +}; \ No newline at end of file -- 2.40.1 From ef5df6971d4fc70d456ea925f97d581da1021577 Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Mon, 14 Jul 2025 02:59:06 -0400 Subject: [PATCH 3/5] adding data analyzer --- src/Charts2/PrometheusChart.jsx | 24 ++-- src/Components/hooks/MetricsAnalyzer.jsx | 156 +++++++++++++++++++++++ src/Components/hooks/TabContent.jsx | 6 +- 3 files changed, 169 insertions(+), 17 deletions(-) create mode 100644 src/Components/hooks/MetricsAnalyzer.jsx diff --git a/src/Charts2/PrometheusChart.jsx b/src/Charts2/PrometheusChart.jsx index 65f0fb2..db9afa7 100644 --- a/src/Charts2/PrometheusChart.jsx +++ b/src/Charts2/PrometheusChart.jsx @@ -55,7 +55,7 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => { const downsampleData = (data, maxPoints = 500) => { if (data.length <= maxPoints) return data; - + const ratio = Math.ceil(data.length / maxPoints); return data.filter((_, index) => index % ratio === 0); }; @@ -64,7 +64,7 @@ const PrometheusChart = ({ metricInfo, chartHeight = 580 }) => { const seconds = (endTime.getTime() - startTime.getTime()) / 1000; return Math.max(Math.ceil(seconds / maxPoints), 1); // в секундах }; - + // Обновляем логи при изменении данных useEffect(() => { @@ -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 && ( diff --git a/src/Components/hooks/MetricsAnalyzer.jsx b/src/Components/hooks/MetricsAnalyzer.jsx new file mode 100644 index 0000000..989d0b0 --- /dev/null +++ b/src/Components/hooks/MetricsAnalyzer.jsx @@ -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 ( + + + Анализ метрик системы + + + + + + + {analysisResult && ( + + + Результаты анализа + + + {Array.isArray(analysisResult) ? ( + + + + + Параметр + Результат + Описание + + + + {analysisResult.map((item, index) => ( + + {item.name || item.parameter} + {item.value || item.result} + {item.description || '-'} + + ))} + +
+
+ ) : ( + + + {JSON.stringify(analysisResult, null, 2)} + + + )} +
+ )} + + + + {error || 'Анализ метрик успешно завершен'} + + +
+ ); +}; + +export default MetricsAnalyzer; \ No newline at end of file diff --git a/src/Components/hooks/TabContent.jsx b/src/Components/hooks/TabContent.jsx index cfc050b..5176b0b 100644 --- a/src/Components/hooks/TabContent.jsx +++ b/src/Components/hooks/TabContent.jsx @@ -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 + + {/* Добавляем кнопку анализа + */} ); } else if (activeTab === "Визуализация") { -- 2.40.1 From 87a79f98d777b4aca64d89854fa6607749e25a94 Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Wed, 16 Jul 2025 09:50:19 -0400 Subject: [PATCH 4/5] adding roles --- src/App.jsx | 114 +++++++++++++----- src/Components/Layout/Dashboard.jsx | 11 +- src/Components/Layout/SidebarMenu.jsx | 4 +- .../SidebarMenuComponents/SidebarFooter.jsx | 66 ++++++---- src/Components/Layout/SidebarMenuWrapper.jsx | 3 +- src/Components/UI/LoginModal.jsx | 84 ++++++++++--- src/Components/UI/RoleBasedRender.jsx | 15 +++ src/Components/UI/auth.jsx | 17 ++- 8 files changed, 237 insertions(+), 77 deletions(-) create mode 100644 src/Components/UI/RoleBasedRender.jsx diff --git a/src/App.jsx b/src/App.jsx index 798e186..2406af5 100755 --- a/src/App.jsx +++ b/src/App.jsx @@ -22,23 +22,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 +98,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 ( diff --git a/src/Components/Layout/Dashboard.jsx b/src/Components/Layout/Dashboard.jsx index e0c2c42..48ac303 100755 --- a/src/Components/Layout/Dashboard.jsx +++ b/src/Components/Layout/Dashboard.jsx @@ -37,7 +37,7 @@ const Content = styled(Box)(({ theme }) => ({ color: theme.palette.custom.modalText, })); -const Dashboard = ({ isDarkMode, setIsDarkMode }) => { +const Dashboard = ({ isDarkMode, setIsDarkMode, user }) => { const { tabs, activeTab, handleOpenTab, handleCloseTab, setActiveTab } = useTabs("Главная"); const [tabContent, setTabContent] = useState({}); const [treeData1, setTreeData1] = useState(menuData); @@ -91,11 +91,11 @@ const Dashboard = ({ isDarkMode, setIsDarkMode }) => { filters: item.filters, title: item.title, description: item.description, - ranges: item.ranges, + ranges: item.ranges, context: { device: item.filters?.device, source_id: item.filters?.source_id, - parent: item + parent: item } }} /> @@ -111,13 +111,13 @@ const Dashboard = ({ isDarkMode, setIsDarkMode }) => { type: item.metric ? 'metric' : 'menuItem', metric: item.metric, filters: item.filters, - ranges: item.ranges + ranges: item.ranges }; handleOpenTab(newTab); } else { setActiveTab(tabId); } -}; + }; // Вспомогательная функция для получения всех дочерних элементов const getAllChildren = (node) => { @@ -138,6 +138,7 @@ const Dashboard = ({ isDarkMode, setIsDarkMode }) => { isDarkMode={isDarkMode} setIsDarkMode={setIsDarkMode} onMenuSelect={handleMenuSelect} + user={user} /> {/* Основной контент */} diff --git a/src/Components/Layout/SidebarMenu.jsx b/src/Components/Layout/SidebarMenu.jsx index d592dc9..3c07130 100644 --- a/src/Components/Layout/SidebarMenu.jsx +++ b/src/Components/Layout/SidebarMenu.jsx @@ -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} /> {!collapsed && ( diff --git a/src/Components/Layout/SidebarMenuComponents/SidebarFooter.jsx b/src/Components/Layout/SidebarMenuComponents/SidebarFooter.jsx index d9546ff..9268f9e 100644 --- a/src/Components/Layout/SidebarMenuComponents/SidebarFooter.jsx +++ b/src/Components/Layout/SidebarMenuComponents/SidebarFooter.jsx @@ -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 ( <> @@ -56,26 +66,29 @@ const SidebarFooter = ({ collapsed, isDarkMode, setIsDarkMode, forceRefreshMenu )} - {!collapsed && ( - - )} + > + + + )} + @@ -98,9 +111,14 @@ const SidebarFooter = ({ collapsed, isDarkMode, setIsDarkMode, forceRefreshMenu - + {/* Используем RoleBasedRender для модального окна */} + + + ); }; diff --git a/src/Components/Layout/SidebarMenuWrapper.jsx b/src/Components/Layout/SidebarMenuWrapper.jsx index ddce8fa..462c2ce 100644 --- a/src/Components/Layout/SidebarMenuWrapper.jsx +++ b/src/Components/Layout/SidebarMenuWrapper.jsx @@ -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} /> ); }; diff --git a/src/Components/UI/LoginModal.jsx b/src/Components/UI/LoginModal.jsx index f76b7b3..0b308e3 100755 --- a/src/Components/UI/LoginModal.jsx +++ b/src/Components/UI/LoginModal.jsx @@ -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" /> { margin="normal" required type={showPassword ? 'text' : 'password'} + value={password} onChange={(e) => setPassword(e.target.value)} - size="normal" + InputProps={{ + endAdornment: ( + + setShowPassword(!showPassword)} + edge="end" + sx={{ + marginRight: '-12px', + alignSelf: 'flex-end' + }} + > + {showPassword ? : } + + + ) + }} /> - {error &&

{error}

} - + {error && ( + + {error} + + )} + + ); diff --git a/src/Components/UI/RoleBasedRender.jsx b/src/Components/UI/RoleBasedRender.jsx new file mode 100644 index 0000000..afdec19 --- /dev/null +++ b/src/Components/UI/RoleBasedRender.jsx @@ -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; +}; \ No newline at end of file diff --git a/src/Components/UI/auth.jsx b/src/Components/UI/auth.jsx index c0beca8..76f0b4c 100644 --- a/src/Components/UI/auth.jsx +++ b/src/Components/UI/auth.jsx @@ -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 }; -- 2.40.1 From cb030a01d2a3c4c077482ff22a7fd4418ccc5814 Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Wed, 16 Jul 2025 10:22:06 -0400 Subject: [PATCH 5/5] added profile page and logout --- src/App.jsx | 1 + src/Components/Layout/Dashboard.jsx | 19 +++++++-- src/Components/UI/ProfileMenu.jsx | 66 +++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 src/Components/UI/ProfileMenu.jsx diff --git a/src/App.jsx b/src/App.jsx index 2406af5..bbc0c28 100755 --- a/src/App.jsx +++ b/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({ diff --git a/src/Components/Layout/Dashboard.jsx b/src/Components/Layout/Dashboard.jsx index 48ac303..baf6004 100755 --- a/src/Components/Layout/Dashboard.jsx +++ b/src/Components/Layout/Dashboard.jsx @@ -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, user }) => { +const Dashboard = ({ isDarkMode, setIsDarkMode, user, onLogout }) => { const { tabs, activeTab, handleOpenTab, handleCloseTab, setActiveTab } = useTabs("Главная"); const [tabContent, setTabContent] = useState({}); const [treeData1, setTreeData1] = useState(menuData); @@ -133,6 +133,19 @@ const Dashboard = ({ isDarkMode, setIsDarkMode, user }) => { return ( + + theme.zIndex.tooltip + 10, + pointerEvents: 'auto' + }} + > + + + {/* Сайдбар */} { + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + const handleOpen = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleLogoutClick = () => { + handleClose(); + onLogout(); + }; + + return ( + <> + + + + {user?.login?.[0]?.toUpperCase() || '?'} + + + + + + + {user?.login || 'Неизвестный'} + + + + Выйти + + + + ); +}; + +export default ProfileMenu; -- 2.40.1