Compare commits
4 Commits
c208813daa
...
b1a760336d
| Author | SHA1 | Date |
|---|---|---|
|
|
b1a760336d | |
|
|
00866d9d57 | |
|
|
f55fb2df56 | |
|
|
be3bf3b21e |
|
|
@ -8,4 +8,8 @@ RUN npm install --verbose
|
|||
COPY vite.config.js eslint.config.js ./
|
||||
COPY . .
|
||||
|
||||
ENV HOST=0.0.0.0
|
||||
|
||||
EXPOSE 5173
|
||||
|
||||
ENTRYPOINT ["npm", "run", "dev"]
|
||||
|
|
@ -18,6 +18,7 @@
|
|||
"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",
|
||||
|
|
@ -40,7 +41,7 @@
|
|||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.16",
|
||||
"globals": "^15.14.0",
|
||||
"vite": "^6.0.5"
|
||||
"vite": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
|
|
@ -8880,9 +8881,10 @@
|
|||
},
|
||||
"node_modules/esbuild": {
|
||||
"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==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
|
|
@ -18642,16 +18644,17 @@
|
|||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/unbox-primitive": {
|
||||
|
|
@ -18903,22 +18906,23 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/vite/-/vite-7.1.0.tgz",
|
||||
"integrity": "sha512-3jdAy3NhBJYsa/lCFcnRfbK4kNkO/bhijFCnv5ByUQk/eekYagoV2yQSISUrhpV+5JiY5hmwOh7jNnQ68dFMuQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
"picomatch": "^4.0.2",
|
||||
"postcss": "^8.5.3",
|
||||
"rollup": "^4.34.9",
|
||||
"tinyglobby": "^0.2.13"
|
||||
"fdir": "^6.4.6",
|
||||
"picomatch": "^4.0.3",
|
||||
"postcss": "^8.5.6",
|
||||
"rollup": "^4.43.0",
|
||||
"tinyglobby": "^0.2.14"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
|
|
@ -18927,14 +18931,14 @@
|
|||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "*",
|
||||
"less": "^4.0.0",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "*",
|
||||
"sass-embedded": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"sass": "^1.70.0",
|
||||
"sass-embedded": "^1.70.0",
|
||||
"stylus": ">=0.54.8",
|
||||
"sugarss": "^5.0.0",
|
||||
"terser": "^5.16.0",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
|
|
|
|||
37
package.json
37
package.json
|
|
@ -10,27 +10,28 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"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/styled": "^11.14.0",
|
||||
"@mui/material": "^6.4.7",
|
||||
"@mui/icons-material": "^6.4.8",
|
||||
"reactflow": "^11.11.4",
|
||||
"vite-plugin-svgr": "^4.3.0",
|
||||
"react-scripts": "^5.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"@mui/material": "^6.4.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-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": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
|
|
@ -42,6 +43,6 @@
|
|||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.16",
|
||||
"globals": "^15.14.0",
|
||||
"vite": "^6.0.5"
|
||||
"vite": "^7.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
43
src/App.jsx
43
src/App.jsx
|
|
@ -110,22 +110,41 @@ function App() {
|
|||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await axios.post(`/api/auth/logout`, null, {
|
||||
withCredentials: true,
|
||||
});
|
||||
const token = localStorage.getItem('access_token');
|
||||
|
||||
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) {
|
||||
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) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class MetricsService {
|
|||
}
|
||||
|
||||
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'],
|
||||
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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -20,6 +20,7 @@ import {
|
|||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import MetricRangeEditor from './SettingsComponents/MetricRangeEditor';
|
||||
import UserManagement from './SettingsComponents/UserManagement';
|
||||
|
||||
const Transition = React.forwardRef(function Transition(props, ref) {
|
||||
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">
|
||||
<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-2" aria-controls="settings-tabpanel-2" />
|
||||
{/* Добавляйте новые вкладки здесь */}
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
|
@ -161,9 +163,14 @@ const SettingsModal = ({ open, onClose, onMenuUpdate }) => {
|
|||
<MetricRangeEditor onSave={handleMetricEditorChange} />
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tabValue} index={2}>
|
||||
<UserManagement />
|
||||
</TabPanel>
|
||||
|
||||
{/* Добавляйте новые TabPanel для новых вкладок */}
|
||||
</DialogContent>
|
||||
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose}>Закрыть</Button>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client'
|
|||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
|
|
|
|||
|
|
@ -14,8 +14,12 @@ export default defineConfig({
|
|||
rewrite: (path) => path.replace(/^\/ai-api/, ''),
|
||||
},
|
||||
'/api': {
|
||||
target: 'http://192.168.2.39:3000',
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true,
|
||||
bypass(req, res, options) {
|
||||
console.log('Proxying request:', req.url);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue