redisign #56

Merged
VladislavD merged 3 commits from redisign into rc 2025-08-08 16:07:09 +03:00
9 changed files with 321 additions and 54 deletions

View File

@ -8,4 +8,8 @@ RUN npm install --verbose
COPY vite.config.js eslint.config.js ./ COPY vite.config.js eslint.config.js ./
COPY . . COPY . .
ENV HOST=0.0.0.0
EXPOSE 5173
ENTRYPOINT ["npm", "run", "dev"] ENTRYPOINT ["npm", "run", "dev"]

46
package-lock.json generated
View File

@ -18,6 +18,7 @@
"chartjs-adapter-date-fns": "^3.0.0", "chartjs-adapter-date-fns": "^3.0.0",
"chartjs-chart-box-and-violin-plot": "^4.0.0", "chartjs-chart-box-and-violin-plot": "^4.0.0",
"d3": "^7.9.0", "d3": "^7.9.0",
"esbuild": "^0.25.8",
"react": "^18.3.1", "react": "^18.3.1",
"react-chartjs-2": "^5.0.0", "react-chartjs-2": "^5.0.0",
"react-datepicker": "^8.1.0", "react-datepicker": "^8.1.0",
@ -40,7 +41,7 @@
"eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16", "eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0", "globals": "^15.14.0",
"vite": "^6.0.5" "vite": "^7.1.0"
} }
}, },
"node_modules/@alloc/quick-lru": { "node_modules/@alloc/quick-lru": {
@ -8880,9 +8881,10 @@
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.25.8", "version": "0.25.8",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.8.tgz",
"integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT",
"bin": { "bin": {
"esbuild": "bin/esbuild" "esbuild": "bin/esbuild"
}, },
@ -18642,16 +18644,17 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.9.2", "version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", "resolved": "https://registry.npmmirror.com/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"license": "Apache-2.0",
"peer": true, "peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
}, },
"engines": { "engines": {
"node": ">=14.17" "node": ">=4.2.0"
} }
}, },
"node_modules/unbox-primitive": { "node_modules/unbox-primitive": {
@ -18903,22 +18906,23 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "6.3.5", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", "resolved": "https://registry.npmmirror.com/vite/-/vite-7.1.0.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "integrity": "sha512-3jdAy3NhBJYsa/lCFcnRfbK4kNkO/bhijFCnv5ByUQk/eekYagoV2yQSISUrhpV+5JiY5hmwOh7jNnQ68dFMuQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.4", "fdir": "^6.4.6",
"picomatch": "^4.0.2", "picomatch": "^4.0.3",
"postcss": "^8.5.3", "postcss": "^8.5.6",
"rollup": "^4.34.9", "rollup": "^4.43.0",
"tinyglobby": "^0.2.13" "tinyglobby": "^0.2.14"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"
}, },
"engines": { "engines": {
"node": "^18.0.0 || ^20.0.0 || >=22.0.0" "node": "^20.19.0 || >=22.12.0"
}, },
"funding": { "funding": {
"url": "https://github.com/vitejs/vite?sponsor=1" "url": "https://github.com/vitejs/vite?sponsor=1"
@ -18927,14 +18931,14 @@
"fsevents": "~2.3.3" "fsevents": "~2.3.3"
}, },
"peerDependencies": { "peerDependencies": {
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@types/node": "^20.19.0 || >=22.12.0",
"jiti": ">=1.21.0", "jiti": ">=1.21.0",
"less": "*", "less": "^4.0.0",
"lightningcss": "^1.21.0", "lightningcss": "^1.21.0",
"sass": "*", "sass": "^1.70.0",
"sass-embedded": "*", "sass-embedded": "^1.70.0",
"stylus": "*", "stylus": ">=0.54.8",
"sugarss": "*", "sugarss": "^5.0.0",
"terser": "^5.16.0", "terser": "^5.16.0",
"tsx": "^4.8.1", "tsx": "^4.8.1",
"yaml": "^2.4.2" "yaml": "^2.4.2"

View File

@ -10,27 +10,28 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"chartjs-adapter-date-fns": "^3.0.0",
"recharts": "^2.15.1",
"d3": "^7.9.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"chart.js": "^4.0.0",
"chartjs-chart-box-and-violin-plot": "^4.0.0",
"react-chartjs-2": "^5.0.0",
"axios": "^1.7.9",
"react-datepicker": "^8.1.0",
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@mui/material": "^6.4.7",
"@mui/icons-material": "^6.4.8", "@mui/icons-material": "^6.4.8",
"reactflow": "^11.11.4", "@mui/material": "^6.4.7",
"vite-plugin-svgr": "^4.3.0",
"react-scripts": "^5.0.1",
"socket.io-client": "^4.8.1",
"antd": "^5.24.7", "antd": "^5.24.7",
"axios": "^1.7.9",
"chart.js": "^4.0.0",
"chartjs-adapter-date-fns": "^3.0.0",
"chartjs-chart-box-and-violin-plot": "^4.0.0",
"d3": "^7.9.0",
"esbuild": "^0.25.8",
"react": "^18.3.1",
"react-chartjs-2": "^5.0.0",
"react-datepicker": "^8.1.0",
"react-dom": "^18.3.1",
"react-scripts": "^5.0.1",
"react-virtualized-auto-sizer": "1.0.26",
"react-window": "1.8.11", "react-window": "1.8.11",
"react-virtualized-auto-sizer": "1.0.26" "reactflow": "^11.11.4",
"recharts": "^2.15.1",
"socket.io-client": "^4.8.1",
"vite-plugin-svgr": "^4.3.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.17.0", "@eslint/js": "^9.17.0",
@ -42,6 +43,6 @@
"eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.16", "eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.14.0", "globals": "^15.14.0",
"vite": "^6.0.5" "vite": "^7.1.0"
} }
} }

View File

@ -110,22 +110,41 @@ function App() {
const handleLogout = async () => { const handleLogout = async () => {
try { try {
await axios.post(`/api/auth/logout`, null, { const token = localStorage.getItem('access_token');
withCredentials: true,
}); if (!token) {
// Если нет токена - просто очищаем данные
cleanup();
return;
}
try {
await axios.post('/api/auth/logout', {}, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
} finally {
cleanup();
}
localStorage.removeItem('access_token');
localStorage.removeItem('user');
setAuthState({
isAuthenticated: false,
isLoading: false,
user: null,
});
setShowLoginModal(true);
} catch (error) { } catch (error) {
console.error('Logout failed:', error); console.error('Logout error:', error);
cleanup();
} }
}; };
function cleanup() {
localStorage.removeItem('access_token');
localStorage.removeItem('user');
setAuthState({
isAuthenticated: false,
isLoading: false,
user: null,
});
setShowLoginModal(true);
}
// Полноэкранный лоадер во время проверки авторизации // Полноэкранный лоадер во время проверки авторизации
if (authState.isLoading) { if (authState.isLoading) {
return ( return (

View File

@ -21,7 +21,7 @@ class MetricsService {
} }
console.log('Connecting WebSocket...'); console.log('Connecting WebSocket...');
this.socket = io(`${this.baseUrl.replace('http', 'ws')}/api/metrics-ws`, { this.socket = io(`${this.baseUrl.replace('http', 'ws')}/ws`, {
transports: ['websocket'], transports: ['websocket'],
withCredentials: true, withCredentials: true,
}); });
@ -152,6 +152,6 @@ class MetricsService {
} }
// Создаем экземпляр сервиса // Создаем экземпляр сервиса
const metricsService = new MetricsService(import.meta.env.VITE_BACK_URL); const metricsService = new MetricsService();
export default metricsService; export default metricsService;

View File

@ -0,0 +1,227 @@
import React, { useState, useEffect } from 'react';
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Button,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
Select,
MenuItem,
FormControl,
InputLabel,
IconButton,
Typography,
Box,
CircularProgress,
Alert,
Snackbar,
Divider,
Tooltip
} from '@mui/material';
import { Add, Delete } from '@mui/icons-material';
import axios from 'axios';
const UserManagement = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const [openDialog, setOpenDialog] = useState(false);
const [newUser, setNewUser] = useState({
login: '',
password: '',
role: 'user'
});
useEffect(() => {
fetchUsers();
}, []);
const fetchUsers = async () => {
setLoading(true);
try {
const response = await axios.get('/api/auth/users', {
withCredentials: true
});
setUsers(response.data);
setError('');
} catch (err) {
setError('Не удалось загрузить пользователей');
console.error(err);
} finally {
setLoading(false);
}
};
const handleInputChange = (e) => {
const { name, value } = e.target;
setNewUser(prev => ({ ...prev, [name]: value }));
};
const handleRoleChange = (e) => {
setNewUser(prev => ({ ...prev, role: e.target.value }));
};
const handleSubmit = async () => {
try {
await axios.post('/api/auth/users', newUser, {
withCredentials: true
});
setOpenDialog(false);
setNewUser({
login: '',
password: '',
role: 'user'
});
setSuccess('Пользователь успешно создан');
fetchUsers();
} catch (err) {
setError(err.response?.data?.message || 'Не удалось создать пользователя');
console.error(err);
}
};
const handleDelete = async (id) => {
try {
await axios.delete(`/api/auth/users/${id}`, {
withCredentials: true
});
setSuccess('Пользователь успешно удален');
fetchUsers();
} catch (err) {
setError(err.response?.data?.message || 'Не удалось удалить пользователя');
console.error(err);
}
};
return (
<Box sx={{ position: 'relative' }}>
{loading && (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
)}
{error && (
<Alert severity="error" sx={{ mb: 2 }} onClose={() => setError('')}>
{error}
</Alert>
)}
{success && (
<Alert severity="success" sx={{ mb: 2 }} onClose={() => setSuccess('')}>
{success}
</Alert>
)}
<Typography variant="h6" gutterBottom>
Управление пользователями
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
<Button
variant="contained"
startIcon={<Add />}
onClick={() => setOpenDialog(true)}
>
Добавить пользователя
</Button>
</Box>
<Divider sx={{ mb: 2 }} />
{!loading && (
<TableContainer component={Paper} sx={{ maxHeight: '60vh', overflow: 'auto' }}>
<Table stickyHeader>
<TableHead>
<TableRow>
<TableCell sx={{ fontWeight: 'bold' }}>ID</TableCell>
<TableCell sx={{ fontWeight: 'bold' }}>Логин</TableCell>
<TableCell sx={{ fontWeight: 'bold' }}>Роль</TableCell>
<TableCell sx={{ fontWeight: 'bold' }}>Действия</TableCell>
</TableRow>
</TableHead>
<TableBody>
{users.map((user) => (
<TableRow key={user.id} hover>
<TableCell>{user.id}</TableCell>
<TableCell>{user.login}</TableCell>
<TableCell>{user.role === 'admin' ? 'Администратор' : 'Пользователь'}</TableCell>
<TableCell>
<Tooltip title={user.role === 'admin' ? 'Нельзя удалить администратора' : 'Удалить пользователя'}>
<IconButton
onClick={() => handleDelete(user.id)}
color="error"
disabled={user.role === 'admin'}
>
<Delete />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
<Dialog open={openDialog} onClose={() => setOpenDialog(false)}>
<DialogTitle>Добавить нового пользователя</DialogTitle>
<DialogContent>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: 2, minWidth: 400 }}>
<TextField
label="Логин"
name="login"
value={newUser.login}
onChange={handleInputChange}
fullWidth
variant="standard"
required
/>
<TextField
label="Пароль"
name="password"
type="password"
value={newUser.password}
onChange={handleInputChange}
fullWidth
variant="standard"
required
/>
<FormControl fullWidth variant="standard">
<InputLabel>Роль</InputLabel>
<Select
value={newUser.role}
onChange={handleRoleChange}
label="Роль"
>
<MenuItem value="user">Пользователь</MenuItem>
<MenuItem value="admin">Администратор</MenuItem>
</Select>
</FormControl>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenDialog(false)}>Отмена</Button>
<Button
onClick={handleSubmit}
variant="contained"
disabled={!newUser.login || !newUser.password}
>
Создать
</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export default UserManagement;

View File

@ -20,6 +20,7 @@ import {
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import SaveIcon from '@mui/icons-material/Save'; import SaveIcon from '@mui/icons-material/Save';
import MetricRangeEditor from './SettingsComponents/MetricRangeEditor'; import MetricRangeEditor from './SettingsComponents/MetricRangeEditor';
import UserManagement from './SettingsComponents/UserManagement';
const Transition = React.forwardRef(function Transition(props, ref) { const Transition = React.forwardRef(function Transition(props, ref) {
return <Slide direction="up" ref={ref} {...props} />; return <Slide direction="up" ref={ref} {...props} />;
@ -147,6 +148,7 @@ const SettingsModal = ({ open, onClose, onMenuUpdate }) => {
<Tabs value={tabValue} onChange={handleTabChange} aria-label="settings tabs"> <Tabs value={tabValue} onChange={handleTabChange} aria-label="settings tabs">
<Tab label="Меню" id="settings-tab-0" aria-controls="settings-tabpanel-0" /> <Tab label="Меню" id="settings-tab-0" aria-controls="settings-tabpanel-0" />
<Tab label="Границы метрик" id="settings-tab-1" aria-controls="settings-tabpanel-1" /> <Tab label="Границы метрик" id="settings-tab-1" aria-controls="settings-tabpanel-1" />
<Tab label="Управление пользователями" id="settings-tab-2" aria-controls="settings-tabpanel-2" />
{/* Добавляйте новые вкладки здесь */} {/* Добавляйте новые вкладки здесь */}
</Tabs> </Tabs>
</Box> </Box>
@ -161,9 +163,14 @@ const SettingsModal = ({ open, onClose, onMenuUpdate }) => {
<MetricRangeEditor onSave={handleMetricEditorChange} /> <MetricRangeEditor onSave={handleMetricEditorChange} />
</TabPanel> </TabPanel>
<TabPanel value={tabValue} index={2}>
<UserManagement />
</TabPanel>
{/* Добавляйте новые TabPanel для новых вкладок */} {/* Добавляйте новые TabPanel для новых вкладок */}
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={handleClose}>Закрыть</Button> <Button onClick={handleClose}>Закрыть</Button>
<Button <Button

View File

@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client'
import './index.css' import './index.css'
import App from './App.jsx' import App from './App.jsx'
createRoot(document.getElementById('root')).render( createRoot(document.getElementById('root')).render(
<StrictMode> <StrictMode>
<App /> <App />

View File

@ -14,8 +14,12 @@ export default defineConfig({
rewrite: (path) => path.replace(/^\/ai-api/, ''), rewrite: (path) => path.replace(/^\/ai-api/, ''),
}, },
'/api': { '/api': {
target: 'http://192.168.2.39:3000', target: 'http://localhost:3000',
changeOrigin: true, changeOrigin: true,
bypass(req, res, options) {
console.log('Proxying request:', req.url);
}
} }
} }
} }