This commit is contained in:
cracklesparkle
2024-10-09 16:51:37 +09:00
parent b88d83cd74
commit 974fc12b34
32 changed files with 3456 additions and 1671 deletions

1440
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -19,24 +19,44 @@
"@js-preview/docx": "^1.6.2", "@js-preview/docx": "^1.6.2",
"@js-preview/excel": "^1.7.8", "@js-preview/excel": "^1.7.8",
"@js-preview/pdf": "^2.0.2", "@js-preview/pdf": "^2.0.2",
"@mantine/carousel": "^7.13.0",
"@mantine/charts": "^7.13.0",
"@mantine/code-highlight": "^7.13.0",
"@mantine/core": "^7.13.0",
"@mantine/dates": "^7.13.0",
"@mantine/dropzone": "^7.13.0",
"@mantine/form": "^7.13.0",
"@mantine/hooks": "^7.13.0",
"@mantine/modals": "^7.13.0",
"@mantine/notifications": "^7.13.0",
"@mantine/nprogress": "^7.13.0",
"@mantine/spotlight": "^7.13.0",
"@mantine/tiptap": "^7.13.0",
"@mui/icons-material": "^5.15.20", "@mui/icons-material": "^5.15.20",
"@mui/material": "^5.15.20", "@mui/material": "^5.15.20",
"@mui/x-charts": "^7.8.0", "@mui/x-charts": "^7.8.0",
"@mui/x-data-grid": "^7.7.1", "@mui/x-data-grid": "^7.7.1",
"@tabler/icons-react": "^3.17.0",
"@tiptap/extension-link": "^2.7.3",
"@tiptap/react": "^2.7.3",
"@tiptap/starter-kit": "^2.7.3",
"@types/ol-ext": "npm:@siedlerchr/types-ol-ext@^3.5.0", "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@^3.5.0",
"@uidotdev/usehooks": "^2.4.1", "@uidotdev/usehooks": "^2.4.1",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"axios": "^1.7.2", "axios": "^1.7.2",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"dayjs": "^1.11.13",
"embla-carousel-react": "^8.3.0",
"file-type": "^19.0.0", "file-type": "^19.0.0",
"ka-table": "^11.3.0",
"ol": "^10.0.0", "ol": "^10.0.0",
"ol-ext": "^4.0.23", "ol-ext": "^4.0.23",
"postcss": "^8.4.38",
"proj4": "^2.12.0", "proj4": "^2.12.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.52.0", "react-hook-form": "^7.52.0",
"react-router-dom": "^6.23.1", "react-router-dom": "^6.23.1",
"recharts": "^2.12.7",
"swr": "^2.2.5", "swr": "^2.2.5",
"zustand": "^4.5.2" "zustand": "^4.5.2"
}, },
@ -50,6 +70,9 @@
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6", "eslint-plugin-react-refresh": "^0.4.6",
"postcss": "^8.4.47",
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",
"serve": "^14.2.3", "serve": "^14.2.3",
"tailwindcss": "^3.4.4", "tailwindcss": "^3.4.4",
"typescript": "^5.2.2", "typescript": "^5.2.2",

View File

@ -1,6 +1,14 @@
export default { export default {
plugins: { plugins: {
tailwindcss: {}, 'postcss-preset-mantine': {},
autoprefixer: {}, 'postcss-simple-vars': {
variables: {
'mantine-breakpoint-xs': '36em',
'mantine-breakpoint-sm': '48em',
'mantine-breakpoint-md': '62em',
'mantine-breakpoint-lg': '75em',
'mantine-breakpoint-xl': '88em',
},
},
}, },
} }

View File

@ -3,31 +3,32 @@ import Main from "./pages/Main"
import Users from "./pages/Users" import Users from "./pages/Users"
import Roles from "./pages/Roles" import Roles from "./pages/Roles"
import NotFound from "./pages/NotFound" import NotFound from "./pages/NotFound"
import DashboardLayout from "./layouts/DashboardLayout"
import MainLayout from "./layouts/MainLayout" import MainLayout from "./layouts/MainLayout"
import SignIn from "./pages/auth/SignIn" import SignIn from "./pages/auth/SignIn"
import ApiTest from "./pages/ApiTest" import ApiTest from "./pages/ApiTest"
import SignUp from "./pages/auth/SignUp" import SignUp from "./pages/auth/SignUp"
import { initAuth, useAuthStore } from "./store/auth" import { initAuth, useAuthStore } from "./store/auth"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { Box, CircularProgress } from "@mui/material"
import Documents from "./pages/Documents" import Documents from "./pages/Documents"
import Reports from "./pages/Reports" import Reports from "./pages/Reports"
import Boilers from "./pages/Boilers" import Boilers from "./pages/Boilers"
import Servers from "./pages/Servers" import Servers from "./pages/Servers"
import { Api, Assignment, Cloud, Factory, Home, Login, Map, MonitorHeart, Password, People, Settings as SettingsIcon, Shield, Storage, Warning } from "@mui/icons-material"
import Settings from "./pages/Settings" import Settings from "./pages/Settings"
import PasswordReset from "./pages/auth/PasswordReset" import PasswordReset from "./pages/auth/PasswordReset"
import MapTest from "./pages/MapTest" import MapTest from "./pages/MapTest"
import MonitorPage from "./pages/MonitorPage" import MonitorPage from "./pages/MonitorPage"
import ChunkedUpload from "./components/map/ChunkedUpload" import ChunkedUpload from "./components/map/ChunkedUpload"
import DashboardLayout from "./layouts/DashboardLayout"
import { IconApi, IconBuildingFactory2, IconDeviceDesktopAnalytics, IconFiles, IconFlag2, IconHome, IconLogin, IconLogin2, IconMap, IconPassword, IconReport, IconServer, IconSettings, IconShield, IconTable, IconUsers } from "@tabler/icons-react"
import { Box, Loader } from "@mantine/core"
import TableTest from "./pages/TableTest"
// Определение страниц с путями и компонентом для рендера // Определение страниц с путями и компонентом для рендера
export const pages = [ export const pages = [
{ {
label: "", label: "",
path: "/auth/signin", path: "/auth/signin",
icon: <Login />, icon: <IconLogin2 />,
component: <SignIn />, component: <SignIn />,
drawer: false, drawer: false,
dashboard: false, dashboard: false,
@ -36,7 +37,7 @@ export const pages = [
{ {
label: "", label: "",
path: "/auth/signup", path: "/auth/signup",
icon: <Login />, icon: <IconLogin />,
component: <SignUp />, component: <SignUp />,
drawer: false, drawer: false,
dashboard: false, dashboard: false,
@ -45,7 +46,7 @@ export const pages = [
{ {
label: "", label: "",
path: "/auth/password-reset", path: "/auth/password-reset",
icon: <Password />, icon: <IconPassword />,
component: <PasswordReset />, component: <PasswordReset />,
drawer: false, drawer: false,
dashboard: false, dashboard: false,
@ -54,7 +55,7 @@ export const pages = [
{ {
label: "Настройки", label: "Настройки",
path: "/settings", path: "/settings",
icon: <SettingsIcon />, icon: <IconSettings />,
component: <Settings />, component: <Settings />,
drawer: false, drawer: false,
dashboard: true, dashboard: true,
@ -63,7 +64,7 @@ export const pages = [
{ {
label: "Главная", label: "Главная",
path: "/", path: "/",
icon: <Home />, icon: <IconHome />,
component: <Main />, component: <Main />,
drawer: true, drawer: true,
dashboard: true, dashboard: true,
@ -72,7 +73,7 @@ export const pages = [
{ {
label: "Пользователи", label: "Пользователи",
path: "/user", path: "/user",
icon: <People />, icon: <IconUsers />,
component: <Users />, component: <Users />,
drawer: true, drawer: true,
dashboard: true, dashboard: true,
@ -81,7 +82,7 @@ export const pages = [
{ {
label: "Роли", label: "Роли",
path: "/role", path: "/role",
icon: <Shield />, icon: <IconShield />,
component: <Roles />, component: <Roles />,
drawer: true, drawer: true,
dashboard: true, dashboard: true,
@ -90,7 +91,7 @@ export const pages = [
{ {
label: "Документы", label: "Документы",
path: "/documents", path: "/documents",
icon: <Storage />, icon: <IconFiles />,
component: <Documents />, component: <Documents />,
drawer: true, drawer: true,
dashboard: true, dashboard: true,
@ -99,7 +100,7 @@ export const pages = [
{ {
label: "Отчеты", label: "Отчеты",
path: "/reports", path: "/reports",
icon: <Assignment />, icon: <IconReport />,
component: <Reports />, component: <Reports />,
drawer: true, drawer: true,
dashboard: true, dashboard: true,
@ -108,7 +109,7 @@ export const pages = [
{ {
label: "Серверы", label: "Серверы",
path: "/servers", path: "/servers",
icon: <Cloud />, icon: <IconServer />,
component: <Servers />, component: <Servers />,
drawer: true, drawer: true,
dashboard: true, dashboard: true,
@ -117,7 +118,7 @@ export const pages = [
{ {
label: "Котельные", label: "Котельные",
path: "/boilers", path: "/boilers",
icon: <Factory />, icon: <IconBuildingFactory2 />,
component: <Boilers />, component: <Boilers />,
drawer: true, drawer: true,
dashboard: true, dashboard: true,
@ -126,7 +127,7 @@ export const pages = [
{ {
label: "API Test", label: "API Test",
path: "/api-test", path: "/api-test",
icon: <Api />, icon: <IconApi />,
component: <ApiTest />, component: <ApiTest />,
drawer: true, drawer: true,
dashboard: true, dashboard: true,
@ -135,16 +136,16 @@ export const pages = [
{ {
label: "ИКС", label: "ИКС",
path: "/map-test", path: "/map-test",
icon: <Map />, icon: <IconMap />,
component: <MapTest />, component: <MapTest />,
drawer: true, drawer: true,
dashboard: true, dashboard: true,
enabled: false, enabled: true,
}, },
{ {
label: "Chunk test", label: "Chunk test",
path: "/chunk-test", path: "/chunk-test",
icon: <Warning />, icon: <IconFlag2 />,
component: <ChunkedUpload />, component: <ChunkedUpload />,
drawer: true, drawer: true,
dashboard: true, dashboard: true,
@ -153,12 +154,21 @@ export const pages = [
{ {
label: "Монитор", label: "Монитор",
path: "/monitor", path: "/monitor",
icon: <MonitorHeart />, icon: <IconDeviceDesktopAnalytics />,
component: <MonitorPage />, component: <MonitorPage />,
drawer: true, drawer: true,
dashboard: true, dashboard: true,
enabled: false, enabled: false,
}, },
{
label: "Table test",
path: "/table-test",
icon: <IconTable />,
component: <TableTest />,
drawer: true,
dashboard: true,
enabled: true,
},
] ]
function App() { function App() {
@ -178,14 +188,11 @@ function App() {
if (isLoading) { if (isLoading) {
return ( return (
<CircularProgress /> <Loader />
) )
} else { } else {
return ( return (
<Box sx={{ <Box w='100%' h='100vh'>
width: "100%",
height: "100vh"
}}>
<Router> <Router>
<Routes> <Routes>
<Route element={<MainLayout />}> <Route element={<MainLayout />}>
@ -194,7 +201,7 @@ function App() {
))} ))}
</Route> </Route>
<Route element={auth.isAuthenticated ? <DashboardLayout /> : <Navigate to={"/auth/signin"} />}> <Route element={auth.isAuthenticated ? <DashboardLayout></DashboardLayout> : <Navigate to={"/auth/signin"} />}>
{pages.filter((page) => page.dashboard).filter((page) => page.enabled).map((page, index) => ( {pages.filter((page) => page.dashboard).filter((page) => page.enabled).map((page, index) => (
<Route key={`dl-${index}`} path={page.path} element={page.component} /> <Route key={`dl-${index}`} path={page.path} element={page.component} />
))} ))}

View File

@ -1,156 +0,0 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import Avatar from '@mui/material/Avatar';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
import Settings from '@mui/icons-material/Settings';
import Logout from '@mui/icons-material/Logout';
import { useNavigate } from 'react-router-dom';
import { logout } from '../store/auth';
import { ListItemText, Switch, styled } from '@mui/material';
import { setDarkMode, usePrefStore } from '../store/preferences';
const Android12Switch = styled(Switch)(({ theme }) => ({
padding: 8,
'& .MuiSwitch-track': {
borderRadius: 22 / 2,
'&::before, &::after': {
content: '""',
position: 'absolute',
top: '50%',
transform: 'translateY(-50%)',
width: 16,
height: 16,
},
'&::before': {
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 24 24"><path fill="${encodeURIComponent(
theme.palette.getContrastText(theme.palette.primary.main),
)}" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"/></svg>')`,
left: 12,
},
'&::after': {
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 24 24"><path fill="${encodeURIComponent(
theme.palette.getContrastText(theme.palette.primary.main),
)}" d="M19,13H5V11H19V13Z" /></svg>')`,
right: 12,
},
},
'& .MuiSwitch-thumb': {
boxShadow: 'none',
width: 16,
height: 16,
margin: 2,
},
}));
export default function AccountMenu() {
const navigate = useNavigate()
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const prefStore = usePrefStore()
return (
<React.Fragment>
<Box sx={{ display: 'flex', alignItems: 'center', textAlign: 'center' }}>
<Tooltip title="Account settings">
<IconButton
onClick={handleClick}
size="small"
sx={{ ml: 2 }}
aria-controls={open ? 'account-menu' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
>
<Avatar sx={{ width: 32, height: 32 }}></Avatar>
</IconButton>
</Tooltip>
</Box>
<Menu
anchorEl={anchorEl}
id="account-menu"
open={open}
onClose={handleClose}
slotProps={{
paper: {
elevation: 0,
sx: {
overflow: 'visible',
filter: 'drop-shadow(0px 2px 8px rgba(0,0,0,0.32))',
mt: 1.5,
'& .MuiAvatar-root': {
width: 32,
height: 32,
ml: -0.5,
mr: 1,
},
'&::before': {
content: '""',
display: 'block',
position: 'absolute',
top: 0,
right: 14,
width: 10,
height: 10,
bgcolor: 'background.paper',
transform: 'translateY(-50%) rotate(45deg)',
zIndex: 0,
},
},
}
}}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
>
<MenuItem onClick={() => {
}}>
<ListItemIcon>
<Android12Switch
checked={prefStore.darkMode}
onChange={(e) => {
setDarkMode(e.target.checked)
}} />
</ListItemIcon>
<ListItemText>
Тема: {prefStore.darkMode ? "темная" : "светлая"}
</ListItemText>
</MenuItem>
<MenuItem onClick={() => {
navigate('/settings')
}}>
<ListItemIcon>
<Settings fontSize="small" />
</ListItemIcon>
Настройки
</MenuItem>
<MenuItem
onClick={() => {
logout()
navigate("/auth/signin")
}}
>
<ListItemIcon>
<Logout fontSize="small" />
</ListItemIcon>
Выход
</MenuItem>
</Menu>
</React.Fragment>
);
}

View File

@ -1,11 +1,13 @@
import { useDocuments, useDownload, useFolders } from '../hooks/swrHooks' import { useDocuments, useDownload, useFolders } from '../hooks/swrHooks'
import { IDocument, IDocumentFolder } from '../interfaces/documents' import { IDocument, IDocumentFolder } from '../interfaces/documents'
import { Box, Breadcrumbs, Button, CircularProgress, Divider, IconButton, Link, List, ListItemButton, SxProps } from '@mui/material' import { Box, CircularProgress, Divider, SxProps } from '@mui/material'
import { Cancel, Close, Download, Folder, InsertDriveFile, Upload, UploadFile } from '@mui/icons-material' import { Folder, InsertDriveFile } from '@mui/icons-material'
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useState } from 'react'
import DocumentService from '../services/DocumentService' import DocumentService from '../services/DocumentService'
import { mutate } from 'swr' import { mutate } from 'swr'
import FileViewer from './modals/FileViewer' import FileViewer from './modals/FileViewer'
import { ActionIcon, Anchor, Breadcrumbs, Button, FileButton, Flex, Loader, RingProgress, Table, Text } from '@mantine/core'
import { IconCancel, IconDownload, IconFile, IconFilePlus, IconFileUpload, IconX } from '@tabler/icons-react'
interface FolderProps { interface FolderProps {
folder: IDocumentFolder; folder: IDocumentFolder;
@ -31,7 +33,7 @@ const FileItemStyle: SxProps = {
function ItemFolder({ folder, handleFolderClick, ...props }: FolderProps) { function ItemFolder({ folder, handleFolderClick, ...props }: FolderProps) {
return ( return (
<ListItemButton <Flex
onClick={() => handleFolderClick(folder)} onClick={() => handleFolderClick(folder)}
> >
<Box <Box
@ -41,7 +43,7 @@ function ItemFolder({ folder, handleFolderClick, ...props }: FolderProps) {
<Folder /> <Folder />
{folder.name} {folder.name}
</Box> </Box>
</ListItemButton> </Flex>
) )
} }
@ -69,7 +71,7 @@ function ItemDocument({ doc, index, handleDocumentClick, ...props }: DocumentPro
}, [shouldFetch, file]) }, [shouldFetch, file])
return ( return (
<ListItemButton> <Flex align='center'>
<Box <Box
sx={FileItemStyle} sx={FileItemStyle}
onClick={() => handleDocumentClick(index)} onClick={() => handleDocumentClick(index)}
@ -79,22 +81,21 @@ function ItemDocument({ doc, index, handleDocumentClick, ...props }: DocumentPro
{doc.name} {doc.name}
</Box> </Box>
<Box> <Box>
<IconButton <ActionIcon
onClick={() => { onClick={() => {
if (!isLoading) { if (!isLoading) {
setShouldFetch(true) setShouldFetch(true)
} }
}} }}
sx={{ ml: 'auto' }} variant='subtle'>
>
{isLoading ? {isLoading ?
<CircularProgress size={24} variant='indeterminate' /> <Loader size='sm' />
: :
<Download /> <IconDownload />
} }
</IconButton> </ActionIcon>
</Box> </Box>
</ListItemButton> </Flex>
) )
} }
@ -105,7 +106,6 @@ export default function FolderViewer() {
const { documents, isLoading: documentsLoading } = useDocuments(currentFolder?.id) const { documents, isLoading: documentsLoading } = useDocuments(currentFolder?.id)
const [uploadProgress, setUploadProgress] = useState(0) const [uploadProgress, setUploadProgress] = useState(0)
const [isUploading, setIsUploading] = useState(false) const [isUploading, setIsUploading] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)
const [fileViewerModal, setFileViewerModal] = useState(false) const [fileViewerModal, setFileViewerModal] = useState(false)
const [currentFileNo, setCurrentFileNo] = useState<number>(-1) const [currentFileNo, setCurrentFileNo] = useState<number>(-1)
@ -128,12 +128,6 @@ export default function FolderViewer() {
setCurrentFolder(newBreadcrumbs[newBreadcrumbs.length - 1]) setCurrentFolder(newBreadcrumbs[newBreadcrumbs.length - 1])
} }
const handleUploadClick = () => {
if (fileInputRef.current) {
fileInputRef.current.click()
}
}
const handleDragOver = (e: React.DragEvent) => { const handleDragOver = (e: React.DragEvent) => {
e.preventDefault() e.preventDefault()
setDragOver(true) setDragOver(true)
@ -150,10 +144,11 @@ export default function FolderViewer() {
setFilesToUpload((prevFiles) => [...prevFiles, ...files]) setFilesToUpload((prevFiles) => [...prevFiles, ...files])
} }
const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => { const handleFileInput = (files: File[] | null) => {
const files = Array.from(e.target.files || []) if (files !== null) {
setFilesToUpload((prevFiles) => [...prevFiles, ...files]) setFilesToUpload((prevFiles) => [...prevFiles, ...files])
} }
}
const uploadFiles = async () => { const uploadFiles = async () => {
setIsUploading(true) setIsUploading(true)
@ -196,28 +191,21 @@ export default function FolderViewer() {
/> />
<Breadcrumbs> <Breadcrumbs>
<Link <Anchor
underline='hover'
color='inherit'
onClick={() => { onClick={() => {
setCurrentFolder(null) setCurrentFolder(null)
setBreadcrumbs([]) setBreadcrumbs([])
}} }}
sx={{ cursor: 'pointer' }}
> >
Главная Главная
</Link> </Anchor>
{breadcrumbs.map((breadcrumb, index) => ( {breadcrumbs.map((breadcrumb, index) => (
<Link <Anchor
key={breadcrumb.id} key={breadcrumb.id}
underline="hover"
color="inherit"
onClick={() => handleBreadcrumbClick(index)} onClick={() => handleBreadcrumbClick(index)}
sx={{ cursor: 'pointer' }}
> >
{breadcrumb.name} {breadcrumb.name}
</Link> </Anchor>
))} ))}
</Breadcrumbs> </Breadcrumbs>
@ -232,44 +220,23 @@ export default function FolderViewer() {
p: '16px' p: '16px'
}}> }}>
<Box sx={{ display: 'flex', gap: '16px' }}> <Box sx={{ display: 'flex', gap: '16px' }}>
<Button <FileButton multiple onChange={handleFileInput}>
LinkComponent="label" {(props) => <Button variant='filled' leftSection={isUploading ? <Loader /> : <IconFilePlus />} {...props}>Добавить</Button>}
role={undefined} </FileButton>
variant="outlined"
tabIndex={-1}
startIcon={
isUploading ? <CircularProgress sx={{ maxHeight: "20px", maxWidth: "20px" }} variant="determinate" value={uploadProgress} /> : <UploadFile />
}
onClick={handleUploadClick}
>
<input
type='file'
ref={fileInputRef}
style={{ display: 'none' }}
onChange={handleFileInput}
onClick={(e) => {
if (e.currentTarget) {
e.currentTarget.value = ''
}
}}
/>
Добавить
</Button>
{filesToUpload.length > 0 && {filesToUpload.length > 0 &&
<> <>
<Button <Button
variant="contained" variant='filled'
color="primary" leftSection={isUploading ? <RingProgress sections={[{ value: uploadProgress, color: 'blue' }]} /> : <IconFileUpload />}
startIcon={<Upload />}
onClick={uploadFiles} onClick={uploadFiles}
> >
Загрузить все Загрузить все
</Button> </Button>
<Button <Button
variant='outlined' variant='outline'
startIcon={<Cancel />} leftSection={<IconCancel />}
onClick={() => { onClick={() => {
setFilesToUpload([]) setFilesToUpload([])
}} }}
@ -283,62 +250,69 @@ export default function FolderViewer() {
<Divider /> <Divider />
{filesToUpload.length > 0 && {filesToUpload.length > 0 &&
<Box> <Flex direction='column'>
{filesToUpload.map((file, index) => ( {filesToUpload.map((file, index) => (
<Box key={index} sx={{ display: 'flex', alignItems: 'center', gap: '8px', marginTop: '8px' }}> <Flex key={index} p='8px'>
<Box> <Flex gap='sm' direction='row' align='center'>
<InsertDriveFile /> <IconFile />
<span>{file.name}</span> <Text>{file.name}</Text>
</Box> </Flex>
<IconButton sx={{ ml: 'auto' }} onClick={() => { <ActionIcon onClick={() => {
setFilesToUpload(prev => { setFilesToUpload(prev => {
return prev.filter((_, i) => i != index) return prev.filter((_, i) => i != index)
}) })
}}> }} ml='auto' variant='subtle'>
<Close /> <IconX />
</IconButton> </ActionIcon>
</Box> </Flex>
))} ))}
</Box> </Flex>
} }
</Box> </Box>
</Box> </Box>
} }
<List <Table
dense
onDragOver={handleDragOver} onDragOver={handleDragOver}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
onDrop={handleDrop} onDrop={handleDrop}
sx={{ bg={dragOver ? 'rgba(0, 0, 0, 0.1)' : 'inherit'}
backgroundColor: dragOver ? 'rgba(0, 0, 0, 0.1)' : 'inherit' highlightOnHover>
}} <Table.Thead>
> <Table.Tr>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{currentFolder ? ( {currentFolder ? (
documents?.map((doc: IDocument, index: number) => ( documents?.map((doc: IDocument, index: number) => (
<div key={`${doc.id}-${doc.name}`}> <Table.Tr key={doc.id}>
<Table.Td>
<ItemDocument <ItemDocument
doc={doc} doc={doc}
index={index} index={index}
handleDocumentClick={handleDocumentClick} handleDocumentClick={handleDocumentClick}
/> />
{index < documents.length - 1 && <Divider />} </Table.Td>
</div> </Table.Tr>
)) ))
) : ( ) : (
folders?.map((folder: IDocumentFolder, index: number) => ( folders?.map((folder: IDocumentFolder, index: number) => (
<div key={`${folder.id}-${folder.name}`}> <Table.Tr key={folder.id}>
<Table.Td>
<ItemFolder <ItemFolder
folder={folder} folder={folder}
index={index} index={index}
handleFolderClick={handleFolderClick} handleFolderClick={handleFolderClick}
/> />
{index < folders.length - 1 && <Divider />} </Table.Td>
</div> </Table.Tr>
)) ))
)} )}
</List> </Table.Tbody>
</Table>
</Box> </Box>
) )
} }

View File

@ -1,7 +1,7 @@
import { SubmitHandler, useForm } from 'react-hook-form' import { SubmitHandler, useForm } from 'react-hook-form'
import { CreateField } from '../interfaces/create' import { CreateField } from '../interfaces/create'
import { Box, Button, CircularProgress, Stack, SxProps, TextField, Typography } from '@mui/material';
import { AxiosResponse } from 'axios'; import { AxiosResponse } from 'axios';
import { Button, Loader, Stack, Text, TextInput } from '@mantine/core';
interface Props { interface Props {
title?: string; title?: string;
@ -11,7 +11,6 @@ interface Props {
mutateHandler?: any; mutateHandler?: any;
defaultValues?: {}; defaultValues?: {};
watchValues?: string[]; watchValues?: string[];
sx?: SxProps | null;
} }
function FormFields({ function FormFields({
@ -20,8 +19,7 @@ function FormFields({
fields, fields,
submitButtonText = 'Сохранить', submitButtonText = 'Сохранить',
mutateHandler, mutateHandler,
defaultValues, defaultValues
sx
}: Props) { }: Props) {
const getDefaultValues = (fields: CreateField[]) => { const getDefaultValues = (fields: CreateField[]) => {
let result: { [key: string]: string | boolean } = {} let result: { [key: string]: string | boolean } = {}
@ -53,20 +51,20 @@ function FormFields({
return ( return (
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<Stack sx={sx} spacing={2} width='100%'> <Stack gap='sm' w='100%'>
<Typography variant="h6" component="h6" gutterBottom> {title.length > 0 &&
<Text size="xl" fw={500}>
{title} {title}
</Typography> </Text>
}
{fields.map((field: CreateField) => { {fields.map((field: CreateField) => {
return ( return (
<TextField <TextInput
fullWidth
margin='normal'
key={field.key} key={field.key}
type={field.inputType ? field.inputType : 'text'}
label={field.headerName || field.key.charAt(0).toUpperCase() + field.key.slice(1)} label={field.headerName || field.key.charAt(0).toUpperCase() + field.key.slice(1)}
required={field.required || false} //placeholder="Your name"
type={field.inputType ? field.inputType : 'text'}
{...register(field.key, { {...register(field.key, {
required: field.required ? `${field.headerName} обязателен` : false, required: field.required ? `${field.headerName} обязателен` : false,
validate: (val: string | boolean) => { validate: (val: string | boolean) => {
@ -77,21 +75,17 @@ function FormFields({
} }
}, },
})} })}
error={!!errors[field.key]} radius="md"
helperText={errors[field.key]?.message} required={field.required || false}
error={errors[field.key]?.message}
errorProps={errors[field.key]}
/> />
) )
})} })}
<Box sx={{ <Button disabled={isSubmitting || Object.keys(dirtyFields).length === 0 || !isValid} type='submit'>
display: "flex", {isSubmitting ? <Loader size={16} /> : submitButtonText}
justifyContent: "space-between",
gap: "8px"
}}>
<Button disabled={isSubmitting || Object.keys(dirtyFields).length === 0 || !isValid} type="submit" variant="contained" color="primary">
{isSubmitting ? <CircularProgress size={16} /> : submitButtonText}
</Button> </Button>
</Box>
</Stack> </Stack>
</form> </form>
) )

View File

@ -1,8 +1,8 @@
import { Box } from '@mui/material' import { Box } from '@mui/material'
import { IServer } from '../interfaces/servers' import { IServer } from '../interfaces/servers'
import { useServerIps } from '../hooks/swrHooks' import { useServerIps } from '../hooks/swrHooks'
import FullFeaturedCrudGrid from './TableEditable'
import { GridColDef } from '@mui/x-data-grid' import { GridColDef } from '@mui/x-data-grid'
import { Table } from '@mantine/core'
function ServerData({ id }: IServer) { function ServerData({ id }: IServer) {
const { serverIps } = useServerIps(id, 0, 10) const { serverIps } = useServerIps(id, 0, 10)
@ -19,18 +19,34 @@ function ServerData({ id }: IServer) {
return ( return (
<Box sx={{ display: 'flex', flexDirection: 'column', p: '16px' }}> <Box sx={{ display: 'flex', flexDirection: 'column', p: '16px' }}>
{serverIps && {serverIps &&
<FullFeaturedCrudGrid // <FullFeaturedCrudGrid
initialRows={serverIps} // initialRows={serverIps}
columns={serverIpsColumns} // columns={serverIpsColumns}
actions // actions
onRowClick={() => { // onRowClick={() => {
//setCurrentServerData(params.row) // //setCurrentServerData(params.row)
//setServerDataOpen(true) // //setServerDataOpen(true)
}} // }}
onSave={undefined} // onSave={undefined}
onDelete={undefined} // onDelete={undefined}
loading={false} // loading={false}
/> // />
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
{serverIpsColumns.map(column => (
<Table.Th key={column.field}>{column.headerName}</Table.Th>
))}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
<Table.Tr>
{serverIpsColumns.map(column => (
<Table.Td key={column.field}>{serverIps ? serverIps[column.field] : ''}</Table.Td>
))}
</Table.Tr>
</Table.Tbody>
</Table>
} }
</Box> </Box>
) )

View File

@ -1,16 +1,17 @@
import { AppBar, Autocomplete, CircularProgress, Dialog, IconButton, TextField, Toolbar } from '@mui/material' import { AppBar, CircularProgress, Dialog, IconButton, Toolbar } from '@mui/material'
import { Fragment, useState } from 'react' import { Fragment, useState } from 'react'
import { IRegion } from '../interfaces/fuel' import { IRegion } from '../interfaces/fuel'
import { useHardwares, useServers } from '../hooks/swrHooks' import { useHardwares, useServers } from '../hooks/swrHooks'
import FullFeaturedCrudGrid from './TableEditable'
import ServerService from '../services/ServersService' import ServerService from '../services/ServersService'
import { GridColDef } from '@mui/x-data-grid' import { GridColDef } from '@mui/x-data-grid'
import { Close } from '@mui/icons-material' import { Close } from '@mui/icons-material'
import ServerData from './ServerData' import ServerData from './ServerData'
import { Autocomplete, CloseButton, Table } from '@mantine/core'
import { IServer } from '../interfaces/servers'
export default function ServerHardware() { export default function ServerHardware() {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [selectedOption, setSelectedOption] = useState<IRegion | null>(null) const [selectedOption, setSelectedOption] = useState<number | null>(null)
const { servers, isLoading } = useServers() const { servers, isLoading } = useServers()
const [serverDataOpen, setServerDataOpen] = useState(false) const [serverDataOpen, setServerDataOpen] = useState(false)
@ -71,54 +72,96 @@ export default function ServerHardware() {
} }
</Dialog> </Dialog>
<form>
<Autocomplete
placeholder="Сервер"
flex={'1'}
data={servers ? servers.map((item: IServer) => ({ label: item.name, value: item.id.toString() })) : []}
onSelect={(e) => console.log(e.currentTarget.value)}
//onChange={(value) => setSearch(value)}
onOptionSubmit={(value) => setSelectedOption(Number(value))}
rightSection={
//search !== '' &&
(
<CloseButton
size="sm"
onMouseDown={(event) => event.preventDefault()}
onClick={() => {
//setSearch('')
setSelectedOption(null)
}}
aria-label="Clear value"
/>
)
}
//value={search}
/>
</form>
{serversLoading ? {serversLoading ?
<CircularProgress /> <CircularProgress />
: :
<FullFeaturedCrudGrid // <FullFeaturedCrudGrid
autoComplete={ // autoComplete={
<Autocomplete // <Autocomplete
open={open} // open={open}
onOpen={() => { // onOpen={() => {
setOpen(true) // setOpen(true)
}} // }}
onClose={() => { // onClose={() => {
setOpen(false) // setOpen(false)
}} // }}
onInputChange={(_, value) => handleInputChange(value)} // onInputChange={(_, value) => handleInputChange(value)}
onChange={(_, value) => handleOptionChange(value)} // onChange={(_, value) => handleOptionChange(value)}
filterOptions={(x) => x} // filterOptions={(x) => x}
isOptionEqualToValue={(option: IRegion, value: IRegion) => option.name === value.name} // isOptionEqualToValue={(option: IRegion, value: IRegion) => option.name === value.name}
getOptionLabel={(option: IRegion) => option.name ? option.name : ""} // getOptionLabel={(option: IRegion) => option.name ? option.name : ""}
options={servers || []} // options={servers || []}
loading={isLoading} // loading={isLoading}
value={selectedOption} // value={selectedOption}
renderInput={(params) => ( // renderInput={(params) => (
<TextField // <TextField
{...params} // {...params}
label="Сервер" // label="Сервер"
size='small' // size='small'
InputProps={{ // InputProps={{
...params.InputProps, // ...params.InputProps,
endAdornment: ( // endAdornment: (
<Fragment> // <Fragment>
{isLoading ? <CircularProgress color="inherit" size={20} /> : null} // {isLoading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment} // {params.InputProps.endAdornment}
</Fragment> // </Fragment>
) // )
}} /> // }} />
)} />} // )} />}
onSave={() => { // onSave={() => {
}} // }}
onDelete={ServerService.removeServer} // onDelete={ServerService.removeServer}
initialRows={hardwares || []} // initialRows={hardwares || []}
columns={hardwareColumns} // columns={hardwareColumns}
actions // actions
onRowClick={(params) => { // onRowClick={(params) => {
setCurrentServerData(params.row) // setCurrentServerData(params.row)
setServerDataOpen(true) // setServerDataOpen(true)
}} // }}
loading={false} // loading={false}
/> // />
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
{hardwareColumns.map(column => (
<Table.Th key={column.field}>{column.headerName}</Table.Th>
))}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
<Table.Tr>
{hardwareColumns.map(column => (
<Table.Td key={column.field}>{hardwares ? hardwares[column.field] : ''}</Table.Td>
))}
</Table.Tr>
</Table.Tbody>
</Table>
} }
</> </>
) )

View File

@ -1,16 +1,17 @@
import { AppBar, Autocomplete, CircularProgress, Dialog, IconButton, TextField, Toolbar } from '@mui/material' import { AppBar, CircularProgress, Dialog, IconButton, TextField, Toolbar } from '@mui/material'
import { Fragment, useState } from 'react' import { Fragment, useState } from 'react'
import { IRegion } from '../interfaces/fuel' import { IRegion } from '../interfaces/fuel'
import { useServerIps, useServers } from '../hooks/swrHooks' import { useServerIps, useServers } from '../hooks/swrHooks'
import FullFeaturedCrudGrid from './TableEditable'
import ServerService from '../services/ServersService' import ServerService from '../services/ServersService'
import { GridColDef } from '@mui/x-data-grid' import { GridColDef } from '@mui/x-data-grid'
import { Close } from '@mui/icons-material' import { Close } from '@mui/icons-material'
import ServerData from './ServerData' import ServerData from './ServerData'
import { Autocomplete, CloseButton, Table } from '@mantine/core'
import { IServer } from '../interfaces/servers'
export default function ServerIpsView() { export default function ServerIpsView() {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [selectedOption, setSelectedOption] = useState<IRegion | null>(null) const [selectedOption, setSelectedOption] = useState<number | null>(null)
const { servers, isLoading } = useServers() const { servers, isLoading } = useServers()
const [serverDataOpen, setServerDataOpen] = useState(false) const [serverDataOpen, setServerDataOpen] = useState(false)
@ -69,52 +70,97 @@ export default function ServerIpsView() {
} }
</Dialog> </Dialog>
<form>
<Autocomplete
placeholder="Сервер"
flex={'1'}
data={servers ? servers.map((item: IServer) => ({ label: item.name, value: item.id.toString() })) : []}
onSelect={(e) => console.log(e.currentTarget.value)}
//onChange={(value) => setSearch(value)}
onOptionSubmit={(value) => setSelectedOption(Number(value))}
rightSection={
//search !== '' &&
(
<CloseButton
size="sm"
onMouseDown={(event) => event.preventDefault()}
onClick={() => {
//setSearch('')
setSelectedOption(null)
}}
aria-label="Clear value"
/>
)
}
//value={search}
/>
</form>
{serversLoading ? {serversLoading ?
<CircularProgress /> <CircularProgress />
: :
<FullFeaturedCrudGrid // <FullFeaturedCrudGrid
autoComplete={ // autoComplete={
<Autocomplete // <Autocomplete
open={open} // open={open}
onOpen={() => { // onOpen={() => {
setOpen(true) // setOpen(true)
}} // }}
onClose={() => { // onClose={() => {
setOpen(false) // setOpen(false)
}} // }}
onInputChange={(_, value) => handleInputChange(value)} // onInputChange={(_, value) => handleInputChange(value)}
onChange={(_, value) => handleOptionChange(value)} // onChange={(_, value) => handleOptionChange(value)}
filterOptions={(x) => x} // filterOptions={(x) => x}
isOptionEqualToValue={(option: IRegion, value: IRegion) => option.name === value.name} // isOptionEqualToValue={(option: IRegion, value: IRegion) => option.name === value.name}
getOptionLabel={(option: IRegion) => option.name ? option.name : ""} // getOptionLabel={(option: IRegion) => option.name ? option.name : ""}
options={servers || []} // options={servers || []}
loading={isLoading} // loading={isLoading}
value={selectedOption} // value={selectedOption}
renderInput={(params) => ( // renderInput={(params) => (
<TextField // <TextField
{...params} // {...params}
size='small' // size='small'
label="Сервер" // label="Сервер"
InputProps={{ // InputProps={{
...params.InputProps, // ...params.InputProps,
endAdornment: ( // endAdornment: (
<Fragment> // <Fragment>
{isLoading ? <CircularProgress color="inherit" size={20} /> : null} // {isLoading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment} // {params.InputProps.endAdornment}
</Fragment> // </Fragment>
) // )
}} /> // }} />
)} />} // )} />}
onSave={() => { // onSave={() => {
}} // }}
onDelete={ServerService.removeServer} // onDelete={ServerService.removeServer}
initialRows={serverIps || []} // initialRows={serverIps || []}
columns={serverIpsColumns} // columns={serverIpsColumns}
actions // actions
onRowClick={(params) => { // onRowClick={(params) => {
setCurrentServerData(params.row) // setCurrentServerData(params.row)
setServerDataOpen(true) // setServerDataOpen(true)
}} loading={false} /> // }} loading={false}
// />
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
{serverIpsColumns.map(column => (
<Table.Th key={column.field}>{column.headerName}</Table.Th>
))}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
<Table.Tr
//bg={selectedRows.includes(element.position) ? 'var(--mantine-color-blue-light)' : undefined}
>
{serverIpsColumns.map(column => (
<Table.Td key={column.field}>{servers ? servers[column.field] : ''}</Table.Td>
))}
</Table.Tr>
</Table.Tbody>
</Table>
} }
</> </>
) )

View File

@ -1,8 +1,7 @@
import { AppBar, Autocomplete, Box, CircularProgress, Dialog, Grid, IconButton, TextField, Toolbar } from '@mui/material' import { AppBar, Box, CircularProgress, Dialog, Grid, IconButton, TextField, Toolbar } from '@mui/material'
import { Fragment, useState } from 'react' import { Fragment, useState } from 'react'
import { IRegion } from '../interfaces/fuel' import { IRegion } from '../interfaces/fuel'
import { useRegions, useServers, useServersInfo } from '../hooks/swrHooks' import { useRegions, useServers, useServersInfo } from '../hooks/swrHooks'
import FullFeaturedCrudGrid from './TableEditable'
import ServerService from '../services/ServersService' import ServerService from '../services/ServersService'
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid' import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid'
import { Close, Cloud, CloudOff } from '@mui/icons-material' import { Close, Cloud, CloudOff } from '@mui/icons-material'
@ -12,22 +11,23 @@ import CardInfo from './CardInfo/CardInfo'
import CardInfoLabel from './CardInfo/CardInfoLabel' import CardInfoLabel from './CardInfo/CardInfoLabel'
import CardInfoChip from './CardInfo/CardInfoChip' import CardInfoChip from './CardInfo/CardInfoChip'
import { useDebounce } from '@uidotdev/usehooks' import { useDebounce } from '@uidotdev/usehooks'
import { Autocomplete, CloseButton, Table } from '@mantine/core'
export default function ServersView() { export default function ServersView() {
const [search, setSearch] = useState<string | null>("") const [search, setSearch] = useState<string | undefined>("")
const debouncedSearch = useDebounce(search, 500) const debouncedSearch = useDebounce(search, 500)
const [selectedOption, setSelectedOption] = useState<IRegion | null>(null) const [selectedOption, setSelectedOption] = useState<number | null>(null)
const { regions, isLoading } = useRegions(10, 1, debouncedSearch) const { regions, isLoading } = useRegions(10, 1, debouncedSearch)
const { serversInfo } = useServersInfo(selectedOption?.id) const { serversInfo } = useServersInfo(selectedOption)
const [serverDataOpen, setServerDataOpen] = useState(false) const [serverDataOpen, setServerDataOpen] = useState(false)
const [currentServerData, setCurrentServerData] = useState<any | null>(null) const [currentServerData, setCurrentServerData] = useState<any | null>(null)
const { servers, isLoading: serversLoading } = useServers(selectedOption?.id, 0, 10) const { servers, isLoading: serversLoading } = useServers(selectedOption, 0, 10)
const serversColumns: GridColDef[] = [ const serversColumns: GridColDef[] = [
//{ field: 'id', headerName: 'ID', type: "number" }, //{ field: 'id', headerName: 'ID', type: "number" },
@ -37,42 +37,43 @@ export default function ServersView() {
{ {
field: 'region_id', field: 'region_id',
editable: true, editable: true,
headerName: 'region_id',
renderCell: (params) => ( renderCell: (params) => (
<div> <div>
{params.value} {params.value}
</div> </div>
), ),
renderEditCell: (params: GridRenderCellParams) => ( // renderEditCell: (params: GridRenderCellParams) => (
<Autocomplete // <Autocomplete
sx={{ display: 'flex', flexGrow: '1' }} // sx={{ display: 'flex', flexGrow: '1' }}
onInputChange={(_, value) => setSearch(value)} // onInputChange={(_, value) => setSearch(value)}
onChange={(_, value) => { // onChange={(_, value) => {
params.value = value // params.value = value
}} // }}
isOptionEqualToValue={(option: IRegion, value: IRegion) => option.name === value.name} // isOptionEqualToValue={(option: IRegion, value: IRegion) => option.name === value.name}
getOptionLabel={(option: IRegion) => option.name ? option.name : ""} // getOptionLabel={(option: IRegion) => option.name ? option.name : ""}
options={regions || []} // options={regions || []}
loading={isLoading} // loading={isLoading}
value={params.value} // value={params.value}
renderInput={(params) => ( // renderInput={(params) => (
<TextField // <TextField
{...params} // {...params}
size='small' // size='small'
variant='standard' // variant='standard'
label="Район" // label="Район"
InputProps={{ // InputProps={{
...params.InputProps, // ...params.InputProps,
endAdornment: ( // endAdornment: (
<Fragment> // <Fragment>
{isLoading ? <CircularProgress color="inherit" size={20} /> : null} // {isLoading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment} // {params.InputProps.endAdornment}
</Fragment> // </Fragment>
) // )
}} // }}
/> // />
)} // )}
/> // />
), // ),
flex: 1 flex: 1
} }
] ]
@ -132,7 +133,50 @@ export default function ServersView() {
</Box> </Box>
} }
<FullFeaturedCrudGrid <form>
<Autocomplete
placeholder="Район"
flex={'1'}
data={regions ? regions.map((item: IRegion) => ({ label: item.name, value: item.id.toString() })) : []}
onSelect={(e) => console.log(e.currentTarget.value)}
onChange={(value) => setSearch(value)}
onOptionSubmit={(value) => setSelectedOption(Number(value))}
rightSection={
search !== '' && (
<CloseButton
size="sm"
onMouseDown={(event) => event.preventDefault()}
onClick={() => {
setSearch('')
setSelectedOption(null)
}}
aria-label="Clear value"
/>
)
}
value={search}
/>
</form>
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
{serversColumns.map(column => (
<Table.Th key={column.field}>{column.headerName}</Table.Th>
))}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
<Table.Tr>
{serversColumns.map(column => (
<Table.Td key={column.field}>{servers ? servers[column.field] : ''}</Table.Td>
))}
</Table.Tr>
</Table.Tbody>
</Table>
{/* <FullFeaturedCrudGrid
loading={serversLoading} loading={serversLoading}
autoComplete={ autoComplete={
<Autocomplete <Autocomplete
@ -171,7 +215,7 @@ export default function ServersView() {
setCurrentServerData(params.row) setCurrentServerData(params.row)
setServerDataOpen(true) setServerDataOpen(true)
}} }}
/> /> */}
</> </>
) )
} }

View File

@ -6,8 +6,7 @@ import View from 'ol/View'
import { Draw, Modify, Select, Snap, Translate } from 'ol/interaction' import { Draw, Modify, Select, Snap, Translate } from 'ol/interaction'
import { ImageStatic, OSM, Vector as VectorSource, XYZ } from 'ol/source' import { ImageStatic, OSM, Vector as VectorSource, XYZ } from 'ol/source'
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer' import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'
import { Divider, IconButton, Slider, Stack, Select as MUISelect, MenuItem, Box, Typography, Accordion, AccordionSummary, AccordionDetails, SxProps, Theme } from '@mui/material' import { Stack, Typography } from '@mui/material'
import { Add, Adjust, Api, CircleOutlined, ExpandMore, OpenWith, RectangleOutlined, Straighten, Timeline, Undo, Upload, Warning } from '@mui/icons-material'
import { Type } from 'ol/geom/Geometry' import { Type } from 'ol/geom/Geometry'
import { click, never, noModifierKeys, platformModifierKeyOnly, primaryAction, shiftKeyOnly } from 'ol/events/condition' import { click, never, noModifierKeys, platformModifierKeyOnly, primaryAction, shiftKeyOnly } from 'ol/events/condition'
import Feature from 'ol/Feature' import Feature from 'ol/Feature'
@ -30,6 +29,8 @@ import { useCities } from '../../hooks/swrHooks'
import useSWR from 'swr' import useSWR from 'swr'
import { fetcher } from '../../http/axiosInstance' import { fetcher } from '../../http/axiosInstance'
import { BASE_URL } from '../../constants' import { BASE_URL } from '../../constants'
import { Accordion, ActionIcon, Box, Flex, Select as MantineSelect, MantineStyleProp, rem, Slider, useMantineColorScheme } from '@mantine/core'
import { IconApi, IconArrowBackUp, IconArrowsMove, IconCircle, IconExclamationCircle, IconLine, IconPlus, IconPoint, IconPolygon, IconRuler, IconTable, IconUpload } from '@tabler/icons-react'
const MapComponent = () => { const MapComponent = () => {
const { cities } = useCities(100, 1) const { cities } = useCities(100, 1)
@ -775,14 +776,17 @@ const MapComponent = () => {
} }
} }
const mapControlsStyle: SxProps<Theme> = { const { colorScheme } = useMantineColorScheme();
const mapControlsStyle: MantineStyleProp = {
borderRadius: '4px', borderRadius: '4px',
position: 'absolute', position: 'absolute',
zIndex: '1', zIndex: '1',
backgroundColor: (theme) => // backgroundColor: (theme) =>
theme.palette.mode === 'light' // theme.palette.mode === 'light'
? '#FFFFFFAA' // ? '#FFFFFFAA'
: '#000000AA', // : '#000000AA',
backgroundColor: colorScheme === 'light' ? '#FFFFFFAA' : '#000000AA',
backdropFilter: 'blur(8px)' backdropFilter: 'blur(8px)'
} }
@ -808,134 +812,118 @@ const MapComponent = () => {
}, [nodes]) }, [nodes])
return ( return (
<Box height={'calc(100% - 64px)'} maxHeight={'100%'} flex={'1'} flexGrow={'1'} position={'relative'}> <Box w={'100%'} h={'calc(100% - 64px)'} mah={'100%'} flex={'1'} pos={'relative'}>
<Stack <ActionIcon.Group orientation='vertical' pos='absolute' top='8px' right='8px' style={{ zIndex: 1, backdropFilter: 'blur(8px)', backgroundColor: colorScheme === 'light' ? '#FFFFFFAA' : '#000000AA', borderRadius: '4px' }}>
direction={'column'} <ActionIcon size='lg' variant='transparent' onClick={() => {
sx={{
...mapControlsStyle,
top: '8px',
right: '8px',
}}
divider={<Divider orientation='horizontal' flexItem />}>
<IconButton onClick={() => {
fetch(`${import.meta.env.VITE_API_EMS_URL}/hello`, { method: 'GET' }).then(res => console.log(res)) fetch(`${import.meta.env.VITE_API_EMS_URL}/hello`, { method: 'GET' }).then(res => console.log(res))
}}> }}>
<Api /> <IconApi />
</IconButton> </ActionIcon>
<IconButton onClick={() => { <ActionIcon size='lg' variant='transparent' onClick={() => {
saveFeatures() saveFeatures()
}}> }}>
<Warning /> <IconExclamationCircle />
</IconButton> </ActionIcon>
<IconButton <ActionIcon size='lg' variant='transparent' onClick={() => {
onClick={() => {
draw.current?.removeLastPoint() draw.current?.removeLastPoint()
}}> }}>
<Undo /> <IconArrowBackUp />
</IconButton> </ActionIcon>
<IconButton <ActionIcon
sx={{ backgroundColor: currentTool === 'Point' ? 'Highlight' : 'transparent' }} size='lg'
onClick={() => handleToolSelect('Point')}> variant={currentTool === 'Point' ? 'filled' : 'transparent'}
<Adjust /> onClick={() => {
</IconButton> handleToolSelect('Point')
}}>
<IconPoint />
</ActionIcon>
<IconButton <ActionIcon
sx={{ backgroundColor: currentTool === 'LineString' ? 'Highlight' : 'transparent' }} size='lg'
onClick={() => handleToolSelect('LineString')}> variant={currentTool === 'LineString' ? 'filled' : 'transparent'}
<Timeline /> onClick={() => {
</IconButton> handleToolSelect('LineString')
}}>
<IconLine />
</ActionIcon>
<IconButton <ActionIcon
sx={{ backgroundColor: currentTool === 'Polygon' ? 'Highlight' : 'transparent' }} size='lg'
onClick={() => handleToolSelect('Polygon')}> variant={currentTool === 'Polygon' ? 'filled' : 'transparent'}
<RectangleOutlined /> onClick={() => {
</IconButton> handleToolSelect('Polygon')
}}>
<IconPolygon />
</ActionIcon>
<IconButton <ActionIcon
sx={{ backgroundColor: currentTool === 'Circle' ? 'Highlight' : 'transparent' }} size='lg'
onClick={() => handleToolSelect('Circle')}> variant={currentTool === 'Circle' ? 'filled' : 'transparent'}
<CircleOutlined /> onClick={() => {
</IconButton> handleToolSelect('Circle')
}}>
<IconCircle />
</ActionIcon>
<IconButton <ActionIcon
size='lg'
variant='transparent'
onClick={() => map?.current?.addInteraction(new Translate())} onClick={() => map?.current?.addInteraction(new Translate())}
> >
<OpenWith /> <IconArrowsMove />
</IconButton> </ActionIcon>
<IconButton> <ActionIcon
<Straighten /> size='lg'
</IconButton> variant='transparent'
</Stack> >
<IconRuler />
</ActionIcon>
</ActionIcon.Group>
<Stack <Flex direction='column' style={{
direction={'column'}
sx={{
...mapControlsStyle, ...mapControlsStyle,
maxWidth: '300px', maxWidth: '300px',
width: '100%', width: '100%',
top: '8px', top: '8px',
left: '8px', left: '8px'
}} divider={<Divider orientation='horizontal' flexItem />} }}>
<Flex direction='row'>
<ActionIcon
size='lg'
variant='transparent'
onClick={() => submitOverlay()}
> >
<Stack direction={'row'}> <IconUpload style={{ width: rem(20), height: rem(20) }} />
<IconButton onClick={() => submitOverlay()}> </ActionIcon>
<Upload />
</IconButton>
<IconButton title='Добавить подложку'> <ActionIcon
<Add /> size='lg'
</IconButton> variant='transparent'
</Stack> title='Добавить подложку'
<Stack direction={'row'} padding={'8px'} spacing={4}>
<Slider size='small' aria-label="Opacity" min={0} max={1} step={0.001} defaultValue={satelliteOpacity} value={satelliteOpacity} onChange={(_, value) => setSatelliteOpacity(Array.isArray(value) ? value[0] : value)} />
<MUISelect
variant='standard'
labelId="demo-simple-select-label"
id="demo-simple-select"
value={satMapsProvider}
label="Satellite Provider"
onChange={(e) => setSatMapsProvider(e.target.value as SatelliteMapsProvider)}
> >
<MenuItem value={'google'}>Google</MenuItem> <IconPlus style={{ width: rem(20), height: rem(20) }} />
<MenuItem value={'yandex'}>Яндекс</MenuItem> </ActionIcon>
<MenuItem value={'custom'}>Custom</MenuItem> </Flex>
</MUISelect>
</Stack>
<Accordion disableGutters sx={{ backgroundColor: 'transparent' }} defaultExpanded> <Flex align='center' direction='row' p='sm' gap='sm'>
<AccordionSummary <Slider w='100%' min={0} max={1} step={0.001} value={satelliteOpacity} defaultValue={satelliteOpacity} onChange={(value) => setSatelliteOpacity(Array.isArray(value) ? value[0] : value)} />
expandIcon={<ExpandMore />}
aria-controls="panel1-content" <MantineSelect variant='filled' value={satMapsProvider} data={[{ label: 'Google', value: 'google' }, { label: 'Yandex', value: 'yandex' }, { label: 'Custom', value: 'custom' }]} onChange={(value) => setSatMapsProvider(value as SatelliteMapsProvider)} />
id="panel1-header" </Flex>
>
<Typography>Объекты</Typography> <Accordion variant='filled' style={{ backgroundColor: 'transparent' }} defaultValue='Объекты'>
</AccordionSummary> <Accordion.Item key={'s'} value={'Объекты'}>
<AccordionDetails> <Accordion.Control icon={<IconTable />}>{'Объекты'}</Accordion.Control>
<Typography> <Accordion.Panel>{'ASd'}</Accordion.Panel>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse </Accordion.Item>
malesuada lacus ex, sit amet blandit leo lobortis eget.
</Typography>
</AccordionDetails>
</Accordion> </Accordion>
</Flex>
</Stack> <Flex direction='row' pos='absolute' bottom='8px' left='8px' style={{ ...mapControlsStyle }}>
<Stack direction={'row'}
sx={{
...mapControlsStyle,
bottom: '8px',
left: '8px',
}}
divider={<Divider orientation='vertical' flexItem />}
>
<Stack> <Stack>
<Typography> <Typography>
x: {currentCoordinate?.[0]} x: {currentCoordinate?.[0]}
@ -950,20 +938,11 @@ const MapComponent = () => {
X={currentX} X={currentX}
Y={currentY} Y={currentY}
</Typography> </Typography>
</Stack> </Flex>
<Stack direction={'row'} <Flex direction='row' style={{ ...mapControlsStyle, bottom: '8px', right: '8px' }}>
sx={{
...mapControlsStyle,
bottom: '8px',
right: '8px',
}}
divider={<Divider orientation='vertical' flexItem />}>
<Stack>
{statusText} {statusText}
</Stack> </Flex>
</Stack>
<div <div
id="map-container" id="map-container"

View File

@ -1,171 +0,0 @@
import * as React from 'react';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import CssBaseline from '@mui/material/CssBaseline';
import Divider from '@mui/material/Divider';
import Drawer from '@mui/material/Drawer';
import IconButton from '@mui/material/IconButton';
import InboxIcon from '@mui/icons-material/MoveToInbox';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import MailIcon from '@mui/icons-material/Mail';
import MenuIcon from '@mui/icons-material/Menu';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
const drawerWidth = 240;
export default function ResponsiveDrawer() {
//const { window } = props;
const [mobileOpen, setMobileOpen] = React.useState(false);
const [isClosing, setIsClosing] = React.useState(false);
const handleDrawerClose = () => {
setIsClosing(true);
setMobileOpen(false);
};
const handleDrawerTransitionEnd = () => {
setIsClosing(false);
};
const handleDrawerToggle = () => {
if (!isClosing) {
setMobileOpen(!mobileOpen);
}
};
const drawer = (
<div>
<Toolbar />
<Divider />
<List>
{['Inbox', 'Starred', 'Send email', 'Drafts'].map((text, index) => (
<ListItem key={text} disablePadding>
<ListItemButton>
<ListItemIcon>
{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
</ListItemIcon>
<ListItemText primary={text} />
</ListItemButton>
</ListItem>
))}
</List>
<Divider />
<List>
{['All mail', 'Trash', 'Spam'].map((text, index) => (
<ListItem key={text} disablePadding>
<ListItemButton>
<ListItemIcon>
{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}
</ListItemIcon>
<ListItemText primary={text} />
</ListItemButton>
</ListItem>
))}
</List>
</div>
);
return (
<Box sx={{ display: 'flex' }}>
<CssBaseline />
<AppBar
position="fixed"
sx={{
width: { sm: `calc(100% - ${drawerWidth}px)` },
ml: { sm: `${drawerWidth}px` },
}}
>
<Toolbar>
<IconButton
color="inherit"
aria-label="open drawer"
edge="start"
onClick={handleDrawerToggle}
sx={{ mr: 2, display: { sm: 'none' } }}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" noWrap component="div">
Dashboard
</Typography>
</Toolbar>
</AppBar>
<Box
component="nav"
sx={{ width: { sm: drawerWidth }, flexShrink: { sm: 0 } }}
aria-label="mailbox folders"
>
{/* The implementation can be swapped with js to avoid SEO duplication of links. */}
<Drawer
variant="temporary"
open={mobileOpen}
onTransitionEnd={handleDrawerTransitionEnd}
onClose={handleDrawerClose}
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
sx={{
display: { xs: 'block', sm: 'none' },
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
>
{drawer}
</Drawer>
<Drawer
variant="permanent"
sx={{
display: { xs: 'none', sm: 'block' },
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
open
>
{drawer}
</Drawer>
</Box>
<Box
component="main"
sx={{ flexGrow: 1, p: 3, width: { sm: `calc(100% - ${drawerWidth}px)` } }}
>
<Toolbar />
<Typography paragraph>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Rhoncus dolor purus non
enim praesent elementum facilisis leo vel. Risus at ultrices mi tempus
imperdiet. Semper risus in hendrerit gravida rutrum quisque non tellus.
Convallis convallis tellus id interdum velit laoreet id donec ultrices.
Odio morbi quis commodo odio aenean sed adipiscing. Amet nisl suscipit
adipiscing bibendum est ultricies integer quis. Cursus euismod quis viverra
nibh cras. Metus vulputate eu scelerisque felis imperdiet proin fermentum
leo. Mauris commodo quis imperdiet massa tincidunt. Cras tincidunt lobortis
feugiat vivamus at augue. At augue eget arcu dictum varius duis at
consectetur lorem. Velit sed ullamcorper morbi tincidunt. Lorem donec massa
sapien faucibus et molestie ac.
</Typography>
<Typography paragraph>
Consequat mauris nunc congue nisi vitae suscipit. Fringilla est ullamcorper
eget nulla facilisi etiam dignissim diam. Pulvinar elementum integer enim
neque volutpat ac tincidunt. Ornare suspendisse sed nisi lacus sed viverra
tellus. Purus sit amet volutpat consequat mauris. Elementum eu facilisis
sed odio morbi. Euismod lacinia at quis risus sed vulputate odio. Morbi
tincidunt ornare massa eget egestas purus viverra accumsan in. In hendrerit
gravida rutrum quisque non tellus orci ac. Pellentesque nec nam aliquam sem
et tortor. Habitant morbi tristique senectus et. Adipiscing elit duis
tristique sollicitudin nibh sit. Ornare aenean euismod elementum nisi quis
eleifend. Commodo viverra maecenas accumsan lacus vel facilisis. Nulla
posuere sollicitudin aliquam ultrices sagittis orci a.
</Typography>
</Box>
</Box>
);
}

View File

@ -1,30 +0,0 @@
import { Tab, Tabs } from "@mui/material"
import { Link, matchPath, useLocation } from "react-router-dom"
function useRouteMatch(patterns: readonly string[]) {
const { pathname } = useLocation()
for (let i = 0; i < patterns.length; i += 1) {
const pattern = patterns[i]
const possibleMatch = matchPath(pattern, pathname)
if (possibleMatch !== null) {
return possibleMatch
}
}
return null
}
export default function NavTabs() {
const routeMatch = useRouteMatch(['/', '/user', '/role']);
const currentTab = routeMatch?.pattern?.path;
return (
<Tabs value={currentTab}>
<Tab label="Главная" value="/" to="/" component={Link} />
<Tab label="Пользователи" value="/user" to="/user" component={Link} />
<Tab label="Роли" value="/role" to="/role" component={Link} />
</Tabs>
);
}

View File

@ -198,7 +198,7 @@ export function useServers(region_id?: number | null, offset?: number, limit?: n
} }
} }
export function useServersInfo(region_id?: number, offset?: number, limit?: number) { export function useServersInfo(region_id?: number | null, offset?: number, limit?: number) {
const { data, error, isLoading } = useSWR( const { data, error, isLoading } = useSWR(
region_id ? `/api/servers_info?region_id=${region_id}&offset=${offset || 0}&limit=${limit || 10}` : `/api/servers_info?offset=${offset || 0}&limit=${limit || 10}`, region_id ? `/api/servers_info?region_id=${region_id}&offset=${offset || 0}&limit=${limit || 10}` : `/api/servers_info?offset=${offset || 0}&limit=${limit || 10}`,
(url: string) => fetcher(url, BASE_URL.servers), (url: string) => fetcher(url, BASE_URL.servers),

View File

@ -1,96 +1,28 @@
import * as React from 'react'; import { AppShell, Avatar, Burger, Button, Flex, Group, Menu, NavLink, rem, Text, useMantineColorScheme } from '@mantine/core';
import { styled, createTheme, ThemeProvider } from '@mui/material/styles'; import { useDisclosure } from '@mantine/hooks';
import CssBaseline from '@mui/material/CssBaseline';
import MuiDrawer from '@mui/material/Drawer';
import Box from '@mui/material/Box';
import MuiAppBar, { AppBarProps as MuiAppBarProps } from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import List from '@mui/material/List';
import Typography from '@mui/material/Typography';
import Divider from '@mui/material/Divider';
import IconButton from '@mui/material/IconButton';
import MenuIcon from '@mui/icons-material/Menu';
import { colors, ListItem, ListItemButton, ListItemIcon, ListItemText, } from '@mui/material';
import { Outlet, useNavigate } from 'react-router-dom'; import { Outlet, useNavigate } from 'react-router-dom';
import { UserData } from '../interfaces/auth';
import { getUserData, useAuthStore } from '../store/auth';
import { useTheme } from '@emotion/react';
import AccountMenu from '../components/AccountMenu';
import { pages } from '../App'; import { pages } from '../App';
import { IconChevronDown, IconLogout, IconSettings, IconMoon, IconSun } from '@tabler/icons-react';
import { getUserData, logout, useAuthStore } from '../store/auth';
import { useEffect, useState } from 'react';
import { UserData } from '../interfaces/auth';
const drawerWidth: number = 240; function DashboardLayout() {
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure()
interface AppBarProps extends MuiAppBarProps { const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true)
open?: boolean;
}
const AppBar = styled(MuiAppBar, {
shouldForwardProp: (prop) => prop !== 'open',
})<AppBarProps>(({ theme, open }) => ({
zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
...(open && {
marginLeft: drawerWidth,
width: `calc(100% - ${drawerWidth}px)`,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
}),
}));
const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open' })(
({ theme, open }) => ({
'& .MuiDrawer-paper': {
position: 'relative',
whiteSpace: 'nowrap',
width: drawerWidth,
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
boxSizing: 'border-box',
...(!open && {
overflowX: 'hidden',
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
width: theme.spacing(7),
[theme.breakpoints.up('sm')]: {
//width: theme.spacing(9),
},
}),
},
}),
);
export default function DashboardLayout() {
const theme = useTheme()
const innerTheme = createTheme(theme)
const [open, setOpen] = React.useState(true);
const toggleDrawer = () => {
setOpen(!open);
};
const authStore = useAuthStore();
const navigate = useNavigate() const navigate = useNavigate()
const getPageTitle = () => { const getPageTitle = () => {
const currentPath = location.pathname; const currentPath = location.pathname
const allPages = [...pages]; const allPages = [...pages]
const currentPage = allPages.find(page => page.path === currentPath); const currentPage = allPages.find(page => page.path === currentPath)
return currentPage ? currentPage.label : "Dashboard"; return currentPage ? currentPage.label : "Панель управления"
}; }
const [userData, setUserData] = React.useState<UserData>(); const authStore = useAuthStore()
const [userData, setUserData] = useState<UserData>()
React.useEffect(() => { useEffect(() => {
if (authStore) { if (authStore) {
const stored = getUserData() const stored = getUserData()
if (stored) { if (stored) {
@ -99,113 +31,95 @@ export default function DashboardLayout() {
} }
}, [authStore]) }, [authStore])
const { colorScheme, setColorScheme } = useMantineColorScheme();
return ( return (
<ThemeProvider theme={innerTheme}> <AppShell
<Box sx={{ header={{ height: 60 }}
display: 'flex', navbar={{
height: "100%" width: desktopOpened ? 200 : 50,
}}> breakpoint: 'sm',
<CssBaseline /> collapsed: { mobile: !mobileOpened },
<AppBar position="absolute" open={open}>
<Toolbar
sx={{
pr: '24px', // keep right padding when drawer closed
}} }}
> >
<IconButton <AppShell.Header>
edge="start" <Flex h="100%" px="md" w='100%' align='center' gap='sm'>
color="inherit" <Group>
aria-label="open drawer" <Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
onClick={toggleDrawer} <Burger opened={desktopOpened} onClick={toggleDesktop} visibleFrom="sm" size="sm" />
sx={{ </Group>
marginRight: '36px',
//...(open && { display: 'none' }),
}}
>
<MenuIcon />
</IconButton>
<Typography <Group w='100%'>
component="h1"
variant="h6"
color="inherit"
noWrap
sx={{ flexGrow: 1 }}
>
{getPageTitle()} {getPageTitle()}
</Typography> </Group>
<Box sx={{ display: "flex", gap: "8px" }}> <Group style={{ flexShrink: 0 }}>
<Box> <Menu
<Typography>{userData?.name} {userData?.surname}</Typography> width={260}
<Divider /> position="bottom-end"
<Typography variant="caption">{userData?.login}</Typography> transitionProps={{ transition: 'pop-top-right' }}
</Box> withinPortal
<AccountMenu />
</Box>
</Toolbar>
</AppBar>
<Drawer variant="permanent" open={open}>
<Toolbar
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
px: [1],
}}
> >
<Box display="flex" justifyContent={'space-between'} width={"100%"}> <Menu.Target>
<Button variant='transparent'>
</Box> <Group gap={7}>
</Toolbar> <Avatar name={`${userData?.name} ${userData?.surname}`} radius="xl" size={30} />
<Text fw={500} size="sm" lh={1} mr={3}>
<Divider /> {`${userData?.name} ${userData?.surname}`}
</Text>
<List component="nav"> <IconChevronDown style={{ width: rem(12), height: rem(12) }} stroke={1.5} />
{pages.filter((page) => page.drawer).filter((page) => page.enabled).map((item, index) => ( </Group>
<ListItem </Button>
key={index} </Menu.Target>
disablePadding <Menu.Dropdown>
<Menu.Label>{userData?.login}</Menu.Label>
<Menu.Item
leftSection={
colorScheme === 'dark' ? <IconMoon style={{ width: rem(16), height: rem(16) }} stroke={1.5} /> : <IconSun style={{ width: rem(16), height: rem(16) }} stroke={1.5} />
}
onClick={() => colorScheme === 'dark' ? setColorScheme('light') : setColorScheme('dark')}
> >
<ListItemButton Тема: {colorScheme === 'dark' ? 'тёмная' : 'светлая'}
</Menu.Item>
<Menu.Item
leftSection={
<IconSettings style={{ width: rem(16), height: rem(16) }} stroke={1.5} />
}
onClick={() => navigate('/settings')}
>
Настройки профиля
</Menu.Item>
<Menu.Item
onClick={() => { onClick={() => {
navigate(item.path) logout()
navigate("/auth/signin")
}} }}
style={{ background: location.pathname === item.path ? innerTheme.palette.action.selected : "transparent" }} leftSection={<IconLogout style={{ width: rem(16), height: rem(16) }} stroke={1.5} />}
selected={location.pathname === item.path}
> >
<ListItemIcon> Выход
{item.icon} </Menu.Item>
</ListItemIcon> </Menu.Dropdown>
<ListItemText </Menu>
primary={item.label} </Group>
sx={{ color: location.pathname === item.path ? colors.blue[700] : innerTheme.palette.text.primary }} </Flex>
</AppShell.Header>
<AppShell.Navbar style={{ transition: "width 0.2s ease" }}>
{pages.filter((page) => page.drawer).filter((page) => page.enabled).map((item) => (
<NavLink
key={item.path}
onClick={() => navigate(item.path)}
label={item.label}
leftSection={item.icon}
active={location.pathname === item.path}
style={{textWrap: 'nowrap'}}
/> />
</ListItemButton>
</ListItem>
))} ))}
</List> </AppShell.Navbar>
</Drawer> <AppShell.Main>
<Box
component="main"
sx={{
backgroundColor: (theme) =>
theme.palette.mode === 'light'
? theme.palette.grey[100]
: theme.palette.grey[900],
flexGrow: 1,
maxHeight: "100vh",
overflow: 'auto',
}}
>
<Toolbar />
<Outlet /> <Outlet />
</Box> </AppShell.Main>
</Box> </AppShell>
</ThemeProvider> )
);
} }
export default DashboardLayout

View File

@ -1,252 +0,0 @@
// Layout for dashboard with responsive drawer
import { Outlet, useLocation, useNavigate } from "react-router-dom"
import * as React from 'react';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import CssBaseline from '@mui/material/CssBaseline';
import Divider from '@mui/material/Divider';
import Drawer from '@mui/material/Drawer';
import IconButton from '@mui/material/IconButton';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import MenuIcon from '@mui/icons-material/Menu';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import { Api, ExitToApp, Home, People, Settings, Shield } from "@mui/icons-material";
import { getUserData, useAuthStore } from "../store/auth";
import { UserData } from "../interfaces/auth";
const drawerWidth = 240;
export default function DashboardLayoutResponsive() {
const [open, setOpen] = React.useState(true);
const authStore = useAuthStore();
const [userData, setUserData] = React.useState<UserData>();
const location = useLocation()
const navigate = useNavigate()
//const { window } = props;
const [mobileOpen, setMobileOpen] = React.useState(false);
const [isClosing, setIsClosing] = React.useState(false);
const handleDrawerClose = () => {
setIsClosing(true);
setMobileOpen(false);
};
const handleDrawerTransitionEnd = () => {
setIsClosing(false);
};
const handleDrawerToggle = () => {
if (!isClosing) {
setMobileOpen(!mobileOpen);
}
};
const pages = [
{
label: "Главная",
path: "/",
icon: <Home />
},
{
label: "Пользователи",
path: "/user",
icon: <People />
},
{
label: "Роли",
path: "/role",
icon: <Shield />
},
{
label: "API Test",
path: "/api-test",
icon: <Api />
},
]
const misc = [
{
label: "Настройки",
path: "/settings",
icon: <Settings />
},
{
label: "Выход",
path: "/signOut",
icon: <ExitToApp />
}
]
const getPageTitle = () => {
const currentPath = location.pathname;
const allPages = [...pages, ...misc];
const currentPage = allPages.find(page => page.path === currentPath);
return currentPage ? currentPage.label : "Dashboard";
};
const toggleDrawer = () => {
setOpen(!open);
};
const drawer = (
<div>
<Toolbar>
<Box display="flex" justifyContent={'space-between'} width={"100%"}>
<Box>
<Typography>{userData?.name} {userData?.surname}</Typography>
<Divider />
<Typography variant="caption">{userData?.login}</Typography>
</Box>
<IconButton
edge="start"
color="inherit"
aria-label="open drawer"
onClick={toggleDrawer}
sx={{
...(open && { display: 'none' }),
}}
>
<MenuIcon />
</IconButton>
</Box>
</Toolbar>
<Divider />
<List>
{pages.map((item, index) => (
<ListItem key={index} disablePadding>
<ListItemButton
onClick={() => {
navigate(item.path)
}}
selected={location.pathname === item.path}
>
<ListItemIcon>
{item.icon}
</ListItemIcon>
<ListItemText primary={item.label} />
</ListItemButton>
</ListItem>
))}
</List>
<Divider />
<List>
{misc.map((item, index) => (
<ListItem key={index} disablePadding>
<ListItemButton
onClick={() => {
navigate(item.path)
}}
selected={location.pathname === item.path}
>
<ListItemIcon>
{item.icon}
</ListItemIcon>
<ListItemText primary={item.label} />
</ListItemButton>
</ListItem>
))}
</List>
</div >
);
React.useEffect(() => {
if (authStore) {
const stored = getUserData()
if (stored) {
setUserData(stored)
}
}
}, [authStore])
return (
<Box sx={{
display: 'flex',
flexGrow: 1,
height: "100vh"
}}>
<CssBaseline />
<AppBar
position="fixed"
sx={{
width: { sm: `calc(100% - ${drawerWidth}px)` },
ml: { sm: `${drawerWidth}px` },
}}
>
<Toolbar>
<IconButton
color="inherit"
aria-label="open drawer"
edge="start"
onClick={handleDrawerToggle}
sx={{ mr: 2, display: { sm: 'none' } }}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" noWrap component="div">
{getPageTitle()}
</Typography>
</Toolbar>
</AppBar>
<Box
component="nav"
sx={{ width: { sm: drawerWidth }, flexShrink: { sm: 0 } }}
aria-label="mailbox folders"
>
{/* The implementation can be swapped with js to avoid SEO duplication of links. */}
<Drawer
variant="temporary"
open={mobileOpen}
onTransitionEnd={handleDrawerTransitionEnd}
onClose={handleDrawerClose}
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
sx={{
display: { xs: 'block', sm: 'none' },
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
>
{drawer}
</Drawer>
<Drawer
variant="permanent"
sx={{
display: { xs: 'none', sm: 'block' },
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
open
>
{drawer}
</Drawer>
</Box>
<Box
component="main"
sx={{
flexGrow: 1,
p: 3,
width: { sm: `calc(100% - ${drawerWidth}px)` }
}}
>
<Toolbar />
<Outlet />
</Box>
</Box>
);
}

View File

@ -1,31 +1,12 @@
// Layout for fullscreen pages // Layout for fullscreen pages
import { Box, createTheme, ThemeProvider, useTheme } from "@mui/material"; import { Flex } from "@mantine/core";
import { Outlet } from "react-router-dom"; import { Outlet } from "react-router-dom";
export default function MainLayout() { export default function MainLayout() {
const theme = useTheme()
const innerTheme = createTheme(theme)
return ( return (
<ThemeProvider theme={innerTheme}> <Flex align='center' justify='center' h='100%' w='100%'>
<Box
sx={{
color: (theme) => theme.palette.mode === 'light'
? theme.palette.grey[900]
: theme.palette.grey[100],
backgroundColor: (theme) =>
theme.palette.mode === 'light'
? theme.palette.grey[100]
: theme.palette.grey[900],
flexGrow: 1,
maxHeight: "100vh",
height: '100%',
overflow: 'auto',
}}
>
<Outlet /> <Outlet />
</Box> </Flex>
</ThemeProvider>
) )
} }

View File

@ -1,98 +1,15 @@
import "@fontsource/inter"; import "@fontsource/inter";
import React, { useEffect } from 'react' import '@mantine/core/styles.css';
import React from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import App from './App.tsx' import App from './App.tsx'
import './index.css' import './index.css'
import { ThemeProvider } from '@emotion/react' import { MantineProvider } from '@mantine/core';
import { createTheme } from '@mui/material'
import { ruRU } from '@mui/material/locale'
import { getDarkMode, usePrefStore } from "./store/preferences.ts";
const mainTheme = createTheme(
{
typography: {
fontFamily: [
'Inter'
].join(',')
},
components: {
MuiAppBar: {
// styleOverrides: {
// colorPrimary: {
// backgroundColor: 'gray'
// }
// }
},
MuiListItemButton: {
defaultProps: {
//disableRipple: true
}
},
MuiButton: {
defaultProps: {
//disableRipple: true
}
},
MuiButtonBase: {
defaultProps: {
//disableRipple: true,
}
},
MuiButtonGroup: {
defaultProps: {
//disableRipple: true,
}
},
MuiIconButton: {
defaultProps: {
}
},
MuiIcon: {
defaultProps: {
}
}
},
},
ruRU
)
const darkTheme = createTheme(
{
...mainTheme,
palette: {
mode: "dark",
primary: { main: '#1976d2' },
},
},
);
const lightTheme = createTheme(
{
...mainTheme,
palette: {
mode: "light",
primary: { main: '#1976d2' },
},
},
);
function ThemedApp() {
const prefStore = usePrefStore()
useEffect(() => {
getDarkMode()
}, [])
return (
<ThemeProvider theme={prefStore.darkMode ? darkTheme : lightTheme}>
<App />
</ThemeProvider>
)
}
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode> <React.StrictMode>
<ThemedApp /> <MantineProvider defaultColorScheme="light">
<App />
</MantineProvider>
</React.StrictMode>, </React.StrictMode>,
) )

View File

@ -1,8 +1,7 @@
import { Box, Typography } from '@mui/material' import { GridColDef } from '@mui/x-data-grid'
import { DataGrid, GridColDef } from '@mui/x-data-grid'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { IBoiler } from '../interfaces/fuel'
import { useBoilers } from '../hooks/swrHooks' import { useBoilers } from '../hooks/swrHooks'
import { Badge, Flex, Table, Text } from '@mantine/core'
function Boilers() { function Boilers() {
const [boilersPage, setBoilersPage] = useState(1) const [boilersPage, setBoilersPage] = useState(1)
@ -26,7 +25,7 @@ function Boilers() {
}, []) }, [])
const boilersColumns: GridColDef[] = [ const boilersColumns: GridColDef[] = [
{ field: 'id', headerName: 'ID', type: "number" }, { field: 'id_object', headerName: 'ID', type: "number" },
{ field: 'boiler_name', headerName: 'Название', type: "string", flex: 1 }, { field: 'boiler_name', headerName: 'Название', type: "string", flex: 1 },
{ field: 'boiler_code', headerName: 'Код', type: "string", flex: 1 }, { field: 'boiler_code', headerName: 'Код', type: "string", flex: 1 },
{ field: 'id_city', headerName: 'Город', type: "string", flex: 1 }, { field: 'id_city', headerName: 'Город', type: "string", flex: 1 },
@ -34,23 +33,52 @@ function Boilers() {
] ]
return ( return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', height: '100%' }}> <Flex direction='column' gap='sm' p='sm'>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', height: '100%', p: '16px' }}> <Text size="xl" fw={600}>
<Typography variant='h6' fontWeight='600'>
Котельные Котельные
</Typography> </Text>
{boilers && {boilers &&
<DataGrid <Table highlightOnHover>
rows={boilers.map((boiler: IBoiler) => { <Table.Thead>
return { ...boiler, id: boiler.id_object } <Table.Tr>
})} {boilersColumns.map(column => (
columns={boilersColumns} <Table.Th key={column.field}>{column.headerName}</Table.Th>
/> ))}
} </Table.Tr>
</Table.Thead>
<Table.Tbody>
{boilers.map((boiler: any) => (
<Table.Tr key={boiler.id_object}>
{boilersColumns.map(column => {
if (column.field === 'activity') {
return (
boiler.activity ? (
<Table.Td key={`${boiler.id_object}-${boiler[column.field]}`}>
<Badge fullWidth variant="light">
Активен
</Badge>
</Table.Td>
</Box> ) : (
</Box> <Table.Td key={`${boiler.id_object}-${boiler[column.field]}`}>
<Badge color="gray" fullWidth variant="light">
Отключен
</Badge>
</Table.Td>
)
)
}
else return (
<Table.Td key={`${boiler.id_object}-${boiler[column.field]}`}>{boiler[column.field]}</Table.Td>
)
})}
</Table.Tr>
))}
</Table.Tbody>
</Table>
}
</Flex>
) )
} }

View File

@ -1,15 +1,54 @@
import { Box, Card, Typography } from "@mui/material"; import { Card, Flex, SimpleGrid, Text } from "@mantine/core";
import { IconBuildingFactory2, IconFiles, IconMap, IconReport, IconServer, IconShield, IconUsers } from "@tabler/icons-react";
import { ReactNode } from "react";
import { useNavigate } from "react-router-dom";
export default function Main() { export default function Main() {
const navigate = useNavigate()
interface CustomCardProps {
link: string;
icon: ReactNode;
label: string;
}
const CustomCard = ({
link,
icon,
label
}: CustomCardProps) => {
return ( return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', p: '16px' }}> <Card
<Typography variant='h6' fontWeight='700'> onClick={() => navigate(link)}
Последние файлы withBorder
</Typography> style={{ cursor: 'pointer', userSelect: 'none' }}
>
<Card> <Flex mih='50'>
{icon}
</Flex>
<Text fw={500} size="lg" mt="md">
{label}
</Text>
</Card> </Card>
</Box> )
}
return (
<Flex direction='column' gap='sm' p='sm'>
<Text size="xl" fw={700}>
Главная
</Text>
<SimpleGrid cols={{ xs: 1, md: 3 }}>
<CustomCard link="/user" icon={<IconUsers size='50' color="#6495ED" />} label="Пользователи" />
<CustomCard link="/role" icon={<IconShield size='50' color="#6495ED" />} label="Роли" />
<CustomCard link="/documents" icon={<IconFiles size='50' color="#6495ED" />} label="Документы" />
<CustomCard link="/reports" icon={<IconReport size='50' color="#6495ED" />} label="Отчеты" />
<CustomCard link="/servers" icon={<IconServer size='50' color="#6495ED" />} label="Серверы" />
<CustomCard link="/boilers" icon={<IconBuildingFactory2 size='50' color="#6495ED" />} label="Котельные" />
<CustomCard link="/map-test" icon={<IconMap size='50' color="#6495ED" />} label="ИКС" />
</SimpleGrid>
</Flex>
) )
} }

View File

@ -1,13 +1,16 @@
import { Error } from "@mui/icons-material"; import { Flex, Text } from "@mantine/core";
import { Box, Typography } from "@mui/material"; import { IconError404 } from "@tabler/icons-react";
export default function NotFound() { export default function NotFound() {
return ( return (
<> <Flex p='sm' gap='sm' align='center' justify='center'>
<Box sx={{ display: 'flex', gap: '16px', alignItems: 'center' }}> <Flex direction='column' gap='sm' align='center'>
<Error /> <IconError404 size={100} />
<Typography>Запрашиваемая страница не найдена.</Typography> <Text size="xl" fw={500} ta='center'>
</Box> Запрашиваемая страница не найдена.
</> </Text>
</Flex>
</Flex>
) )
} }

View File

@ -1,26 +1,25 @@
import { Fragment, useEffect, useState } from "react" import { useEffect, useState } from "react"
import { Autocomplete, Box, Button, CircularProgress, IconButton, TextField } from "@mui/material"
import { DataGrid } from "@mui/x-data-grid"
import { useCities, useReport, useReportExport } from "../hooks/swrHooks" import { useCities, useReport, useReportExport } from "../hooks/swrHooks"
import { useDebounce } from "@uidotdev/usehooks" import { useDebounce } from "@uidotdev/usehooks"
import { ICity } from "../interfaces/fuel" import { ICity } from "../interfaces/fuel"
import { Update } from "@mui/icons-material"
import { mutate } from "swr" import { mutate } from "swr"
import { ActionIcon, Autocomplete, Badge, Button, CloseButton, Flex, Table } from "@mantine/core"
import { IconRefresh } from "@tabler/icons-react"
export default function Reports() { export default function Reports() {
const [download, setDownload] = useState(false) const [download, setDownload] = useState(false)
const [search, setSearch] = useState<string | null>("") const [search, setSearch] = useState<string | undefined>("")
const debouncedSearch = useDebounce(search, 500) const debouncedSearch = useDebounce(search, 500)
const [selectedOption, setSelectedOption] = useState<ICity | null>(null) const [selectedOption, setSelectedOption] = useState<number | null>(null)
const { cities, isLoading } = useCities(10, 1, debouncedSearch) const { cities } = useCities(10, 1, debouncedSearch)
const { report, isLoading: reportLoading } = useReport(selectedOption?.id) const { report } = useReport(selectedOption)
const { reportExported } = useReportExport(selectedOption?.id, download) const { reportExported } = useReportExport(selectedOption, download)
const refreshReport = async () => { const refreshReport = async () => {
mutate(`/info/reports/${selectedOption?.id}?to_export=false`) mutate(`/info/reports/${selectedOption}?to_export=false`)
} }
useEffect(() => { useEffect(() => {
@ -41,9 +40,32 @@ export default function Reports() {
} }
return ( return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', p: '16px' }}> <Flex direction='column' gap='sm' p='sm'>
<Box sx={{ display: 'flex', gap: '16px' }}> <Flex component="form" gap={'sm'}>
{/* <SearchableSelect /> */}
<Autocomplete <Autocomplete
placeholder="Населенный пункт"
flex={'1'}
data={cities ? cities.map((item: ICity) => ({ label: item.name, value: item.id.toString() })) : []}
onSelect={(e) => console.log(e.currentTarget.value)}
onChange={(value) => setSearch(value)}
onOptionSubmit={(value) => setSelectedOption(Number(value))}
rightSection={
search !== '' && (
<CloseButton
size="sm"
onMouseDown={(event) => event.preventDefault()}
onClick={() => {
setSearch('')
setSelectedOption(null)
}}
aria-label="Clear value"
/>
)
}
value={search}
/>
{/* <Autocomplete
fullWidth fullWidth
onInputChange={(_, value) => setSearch(value)} onInputChange={(_, value) => setSearch(value)}
onChange={(_, value) => setSelectedOption(value)} onChange={(_, value) => setSelectedOption(value)}
@ -68,18 +90,78 @@ export default function Reports() {
}} }}
/> />
)} )}
/> /> */}
<IconButton onClick={() => refreshReport()}> <ActionIcon size='auto' variant='transparent' onClick={() => refreshReport()}>
<Update /> <IconRefresh />
</IconButton> </ActionIcon>
<Button onClick={() => exportReport()}> <Button disabled={!selectedOption} onClick={() => exportReport()}>
Экспорт Экспорт
</Button> </Button>
</Box> </Flex>
<DataGrid {report &&
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
{[
{ field: 'id', headerName: '№', width: 70 },
...Object.keys(report).map(key => ({
field: key,
headerName: key.charAt(0).toUpperCase() + key.slice(1),
width: 150
}))
].map(column => (
<Table.Th key={column.headerName}>{column.headerName}</Table.Th>
))}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{[...new Set(Object.keys(report).flatMap(key => Object.keys(report[key])))].map(id => {
const row: any = { id: Number(id) };
Object.keys(report).forEach(key => {
row[key] = report[key][id];
});
return (<Table.Tr key={row.id}>
{[
{ field: 'id', headerName: '№', width: 70 },
...Object.keys(report).map(key => ({
field: key,
headerName: key.charAt(0).toUpperCase() + key.slice(1),
width: 150
}))
].map(column => {
if (column.field === 'Активность') {
return (
row['Активность'] ? (
<Table.Td key={`${row.id}-${column.headerName}`}>
<Badge fullWidth variant="light">
Активен
</Badge>
</Table.Td>
) : (
<Table.Td key={`${row.id}-${column.headerName}`}>
<Badge color="gray" fullWidth variant="light">
Отключен
</Badge>
</Table.Td>
)
)
}
return (
<Table.Td key={`${row.id}-${column.headerName}`}>{row[column.field]}</Table.Td>
)
})}
</Table.Tr>)
})}
</Table.Tbody>
</Table>
}
{/* <DataGrid
autoHeight autoHeight
style={{ width: "100%" }} style={{ width: "100%" }}
loading={reportLoading} loading={reportLoading}
@ -118,7 +200,7 @@ export default function Reports() {
onProcessRowUpdateError={() => { onProcessRowUpdateError={() => {
}} }}
/> /> */}
</Box> </Flex>
) )
} }

View File

@ -1,15 +1,15 @@
import { useState } from 'react' import { GridColDef } from '@mui/x-data-grid'
import { Box, Button, CircularProgress, Modal } from '@mui/material'
import { DataGrid, GridColDef } from '@mui/x-data-grid'
import { useRoles } from '../hooks/swrHooks' import { useRoles } from '../hooks/swrHooks'
import { CreateField } from '../interfaces/create' import { CreateField } from '../interfaces/create'
import RoleService from '../services/RoleService' import RoleService from '../services/RoleService'
import FormFields from '../components/FormFields' import FormFields from '../components/FormFields'
import { Button, Flex, Loader, Modal, Table } from '@mantine/core'
import { useDisclosure } from '@mantine/hooks'
export default function Roles() { export default function Roles() {
const { roles, isError, isLoading } = useRoles() const { roles, isError, isLoading } = useRoles()
const [open, setOpen] = useState(false) const [opened, { open, close }] = useDisclosure(false);
const createFields: CreateField[] = [ const createFields: CreateField[] = [
{ key: 'name', headerName: 'Название', type: 'string', required: true, defaultValue: '' }, { key: 'name', headerName: 'Название', type: 'string', required: true, defaultValue: '' },
@ -23,43 +23,42 @@ export default function Roles() {
]; ];
if (isError) return <div>Произошла ошибка при получении данных.</div> if (isError) return <div>Произошла ошибка при получении данных.</div>
if (isLoading) return <CircularProgress /> if (isLoading) return <Loader />
return ( return (
<Box sx={{ <Flex direction='column' align='flex-start' gap='sm' p='sm'>
display: 'flex', <Button onClick={open}>
flexDirection: 'column',
alignItems: 'flex-start',
gap: '16px',
flexGrow: 1,
p: '16px'
}}>
<Button onClick={() => setOpen(true)}>
Добавить роль Добавить роль
</Button> </Button>
<Modal <Modal opened={opened} onClose={close} title="Создание роли" centered>
open={open}
onClose={() => setOpen(false)}
>
<FormFields <FormFields
sx={{
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
boxShadow: 24,
p: 4,
}}
fields={createFields} fields={createFields}
submitHandler={RoleService.createRole} submitHandler={RoleService.createRole}
title="Создание роли"
/> />
</Modal> </Modal>
<DataGrid <Table highlightOnHover>
<Table.Thead>
<Table.Tr>
{columns.map(column => (
<Table.Th key={column.field}>{column.headerName}</Table.Th>
))}
</Table.Tr>
</Table.Thead>
<Table.Tbody>{roles.map((role: any) => (
<Table.Tr
key={role.id}
//bg={selectedRows.includes(element.position) ? 'var(--mantine-color-blue-light)' : undefined}
>
{columns.map(column => (
<Table.Td key={column.field}>{role[column.field]}</Table.Td>
))}
</Table.Tr>
))}</Table.Tbody>
</Table>
{/* <DataGrid
autoHeight autoHeight
style={{ width: "100%" }} style={{ width: "100%" }}
rows={roles} rows={roles}
@ -78,7 +77,7 @@ export default function Roles() {
onProcessRowUpdateError={() => { onProcessRowUpdateError={() => {
}} }}
/> /> */}
</Box> </Flex>
) )
} }

View File

@ -1,67 +1,41 @@
import { Box, Tab, Tabs } from "@mui/material"
import { useState } from "react" import { useState } from "react"
import ServersView from "../components/ServersView" import ServersView from "../components/ServersView"
import ServerIpsView from "../components/ServerIpsView" import ServerIpsView from "../components/ServerIpsView"
import ServerHardware from "../components/ServerHardware" import ServerHardware from "../components/ServerHardware"
import ServerStorage from "../components/ServerStorages" import ServerStorage from "../components/ServerStorages"
import { Flex, Tabs } from "@mantine/core"
export default function Servers() { export default function Servers() {
const [currentTab, setCurrentTab] = useState(0) const [currentTab, setCurrentTab] = useState<string | null>('0')
const handleTabChange = (newValue: number) => {
setCurrentTab(newValue);
}
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
function CustomTabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return ( return (
<div <Flex gap='sm' p='sm' direction='column'>
role="tabpanel" <Flex gap='sm' direction='column'>
hidden={value !== index} <Tabs value={currentTab} onChange={setCurrentTab}>
id={`simple-tabpanel-${index}`} <Tabs.List>
aria-labelledby={`simple-tab-${index}`} <Tabs.Tab value="0">Серверы</Tabs.Tab>
{...other} <Tabs.Tab value="1">IP-адреса</Tabs.Tab>
> <Tabs.Tab value="3">Hardware</Tabs.Tab>
{value === index && <Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>{children}</Box>} <Tabs.Tab value="4">Storages</Tabs.Tab>
</div> </Tabs.List>
);
}
return ( <Tabs.Panel value="0" pt='sm'>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', height: '100%', p: '16px' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={currentTab} onChange={(_, value) =>
handleTabChange(value)
} aria-label="basic tabs example">
<Tab label="Серверы" />
<Tab label="IP-адреса" />
<Tab label="Hardware" />
<Tab label="Storages" />
</Tabs>
</Box>
<CustomTabPanel value={currentTab} index={0}>
<ServersView /> <ServersView />
</CustomTabPanel> </Tabs.Panel>
<CustomTabPanel value={currentTab} index={1}> <Tabs.Panel value="1" pt='sm'>
<ServerIpsView /> <ServerIpsView />
</CustomTabPanel> </Tabs.Panel>
<CustomTabPanel value={currentTab} index={2}> <Tabs.Panel value="2" pt='sm'>
<ServerHardware /> <ServerHardware />
</CustomTabPanel> </Tabs.Panel>
<CustomTabPanel value={currentTab} index={3}> <Tabs.Panel value="3" pt='sm'>
<ServerStorage /> <ServerStorage />
</CustomTabPanel> </Tabs.Panel>
</Tabs>
</Flex>
{/* <BarChart {/* <BarChart
xAxis={[{ scaleType: 'band', data: ['group A', 'group B', 'group C'] }]} xAxis={[{ scaleType: 'band', data: ['group A', 'group B', 'group C'] }]}
@ -69,6 +43,6 @@ export default function Servers() {
width={500} width={500}
height={300} height={300}
/> */} /> */}
</Box> </Flex>
) )
} }

View File

@ -1,4 +1,3 @@
import { Box, Stack } from "@mui/material"
import UserService from "../services/UserService" import UserService from "../services/UserService"
import { setUserData, useAuthStore } from "../store/auth" import { setUserData, useAuthStore } from "../store/auth"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
@ -6,6 +5,7 @@ import { CreateField } from "../interfaces/create"
import { IUser } from "../interfaces/user" import { IUser } from "../interfaces/user"
import FormFields from "../components/FormFields" import FormFields from "../components/FormFields"
import AuthService from "../services/AuthService" import AuthService from "../services/AuthService"
import { Flex } from "@mantine/core"
export default function Settings() { export default function Settings() {
const { token } = useAuthStore() const { token } = useAuthStore()
@ -39,17 +39,14 @@ export default function Settings() {
] ]
return ( return (
<Box <Flex
sx={{ direction='column'
display: "flex", align='flex-start'
flexDirection: "column", gap='sm'
alignItems: "flex-start", p='sm'
gap: "16px",
}}
> >
{currentUser && {currentUser &&
<Stack spacing={2} width='100%'> <Flex direction='column' gap='sm' w='100%'>
<Stack width='100%'>
<FormFields <FormFields
fields={profileFields} fields={profileFields}
defaultValues={currentUser} defaultValues={currentUser}
@ -59,17 +56,14 @@ export default function Settings() {
submitHandler={(data) => UserService.updateUser({ id: currentUser.id, ...data })} submitHandler={(data) => UserService.updateUser({ id: currentUser.id, ...data })}
title="Пользователь" title="Пользователь"
/> />
</Stack>
<Stack width='100%'>
<FormFields <FormFields
fields={passwordFields} fields={passwordFields}
submitHandler={(data) => AuthService.updatePassword({ id: currentUser.id, ...data })} submitHandler={(data) => AuthService.updatePassword({ id: currentUser.id, ...data })}
title="Смена пароля" title="Смена пароля"
/> />
</Stack> </Flex>
</Stack>
} }
</Box> </Flex>
) )
} }

View File

@ -0,0 +1,10 @@
$ka-background-color: #2c2c2c;
$ka-border-color: #4d4d4d;
$ka-cell-hover-background-color: transparentize(#fff, 0.8);
$ka-color-base: #fefefe;
$ka-input-background-color: $ka-background-color;
$ka-input-border-color: $ka-border-color;
$ka-input-color: $ka-color-base;
$ka-row-hover-background-color: transparentize(#fff, 0.9);
$ka-thead-background-color: #1b1b1b;
$ka-thead-color: #c5c5c5;

View File

@ -0,0 +1,32 @@
import React from 'react'
import { Table, DataType } from 'ka-table'
import 'ka-table/style.css';
import { Flex } from '@mantine/core';
import styles from './TableTest.module.scss'
function TableTest() {
return (
<Flex direction='column' align='flex-start' gap='sm' p='sm'>
<Table
columns={[
{ key: 'column1', title: 'Column 1', dataType: DataType.String },
{ key: 'column2', title: 'Column 2', dataType: DataType.String },
{ key: 'column3', title: 'Column 3', dataType: DataType.String },
{ key: 'column4', title: 'Column 4', dataType: DataType.String },
]}
data={Array(100).fill(undefined).map(
(_, index) => ({
column1: `column:1 row:${index}`,
column2: `column:2 row:${index}`,
column3: `column:3 row:${index}`,
column4: `column:4 row:${index}`,
id: index,
}),
)}
rowKeyField={'id'}
/>
</Flex>
)
}
export default TableTest

View File

@ -1,18 +1,27 @@
import { Box, Button, CircularProgress, Modal } from "@mui/material" import { GridColDef } from "@mui/x-data-grid"
import { DataGrid, GridColDef } from "@mui/x-data-grid"
import { useRoles, useUsers } from "../hooks/swrHooks" import { useRoles, useUsers } from "../hooks/swrHooks"
import { IRole } from "../interfaces/role" import { IRole } from "../interfaces/role"
import { useState } from "react" import { useEffect, useState } from "react"
import { CreateField } from "../interfaces/create" import { CreateField } from "../interfaces/create"
import UserService from "../services/UserService" import UserService from "../services/UserService"
import FormFields from "../components/FormFields" import FormFields from "../components/FormFields"
import { Badge, Button, Flex, Loader, Modal, Pagination, Select, Table } from "@mantine/core"
import { useDisclosure } from "@mantine/hooks"
export default function Users() { export default function Users() {
const { users, isError, isLoading } = useUsers() const { users, isError, isLoading } = useUsers()
const { roles } = useRoles() const { roles } = useRoles()
const [open, setOpen] = useState(false) const [roleOptions, setRoleOptions] = useState<any>()
useEffect(() => {
if (Array.isArray(roles)) {
setRoleOptions(roles.map((role: IRole) => ({ label: role.name, value: role.id.toString() })))
}
}, [roles])
const [opened, { open, close }] = useDisclosure(false);
const createFields: CreateField[] = [ const createFields: CreateField[] = [
{ key: 'email', headerName: 'E-mail', type: 'string', required: true, defaultValue: '' }, { key: 'email', headerName: 'E-mail', type: 'string', required: true, defaultValue: '' },
@ -30,7 +39,7 @@ export default function Users() {
{ field: 'phone', headerName: 'Телефон', flex: 1, editable: true }, { field: 'phone', headerName: 'Телефон', flex: 1, editable: true },
{ field: 'name', headerName: 'Имя', flex: 1, editable: true }, { field: 'name', headerName: 'Имя', flex: 1, editable: true },
{ field: 'surname', headerName: 'Фамилия', flex: 1, editable: true }, { field: 'surname', headerName: 'Фамилия', flex: 1, editable: true },
{ field: 'is_active', headerName: 'Активен', type: "boolean", flex: 1, editable: true }, { field: 'is_active', headerName: 'Статус', type: "boolean", flex: 1, editable: true },
{ {
field: 'role_id', field: 'role_id',
headerName: 'Роль', headerName: 'Роль',
@ -41,44 +50,92 @@ export default function Users() {
}, },
]; ];
if (isError) return <div>Произошла ошибка при получении данных.</div> if (isError) return (
if (isLoading) return <CircularProgress /> <div>
Произошла ошибка при получении данных.
</div>
)
if (isLoading) {
return (
<Flex direction='column' align='flex-start' gap='sm' p='sm'>
<Loader />
</Flex>
)
}
return ( return (
<Box sx={{ <Flex direction='column' align='flex-start' gap='sm' p='sm'>
display: "flex", <Button onClick={open}>
flexDirection: "column",
alignItems: "flex-start",
gap: "16px",
p: '16px'
}}
>
<Button onClick={() => setOpen(true)}>
Добавить пользователя Добавить пользователя
</Button> </Button>
<Modal <Modal opened={opened} onClose={close} title="Регистрация пользователя" centered>
open={open}
onClose={() => setOpen(false)}
>
<FormFields <FormFields
fields={createFields} fields={createFields}
submitHandler={UserService.createUser} submitHandler={UserService.createUser}
title="Создание пользователя"
sx={{
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
boxShadow: 24,
p: 4,
}}
/> />
</Modal> </Modal>
<DataGrid {Array.isArray(roleOptions) &&
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
{columns.map(column => (
<Table.Th key={column.field}>{column.headerName}</Table.Th>
))}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{users.map((user: any) => (
<Table.Tr
key={user.id}
//bg={selectedRows.includes(element.position) ? 'var(--mantine-color-blue-light)' : undefined}
>
{columns.map(column => {
if (column.field === 'is_active') {
return (
user.is_active ? (
<Table.Td key={column.field}>
<Badge fullWidth variant="light">
Активен
</Badge>
</Table.Td>
) : (
<Table.Td key={column.field}>
<Badge color="gray" fullWidth variant="light">
Отключен
</Badge>
</Table.Td>
)
)
}
else if (column.field === 'role_id') {
return (
<Table.Td key={column.field}>
<Select
data={roleOptions}
defaultValue={user.role_id.toString()}
variant="unstyled"
allowDeselect={false}
/>
</Table.Td>
)
}
else return (
<Table.Td key={column.field}>{user[column.field]}</Table.Td>
)
})}
</Table.Tr>
))}
</Table.Tbody>
</Table>
}
<Pagination total={10} />
{/* <DataGrid
density="compact" density="compact"
autoHeight autoHeight
style={{ width: "100%" }} style={{ width: "100%" }}
@ -99,7 +156,7 @@ export default function Users() {
onProcessRowUpdateError={() => { onProcessRowUpdateError={() => {
}} }}
/> /> */}
</Box> </Flex>
) )
} }

View File

@ -1,8 +1,9 @@
import { Box, Button, CircularProgress, Container, Fade, Grow, Stack, TextField, Typography } from '@mui/material' import { CircularProgress, Fade, Grow } from '@mui/material'
import { useState } from 'react' import { useState } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'; import { SubmitHandler, useForm } from 'react-hook-form';
import AuthService from '../../services/AuthService'; import AuthService from '../../services/AuthService';
import { CheckCircle } from '@mui/icons-material'; import { CheckCircle } from '@mui/icons-material';
import { Button, Flex, Paper, Text, TextInput } from '@mantine/core';
interface PasswordResetProps { interface PasswordResetProps {
email: string; email: string;
@ -31,61 +32,58 @@ function PasswordReset() {
} }
return ( return (
<Container maxWidth="sm"> <Paper flex={1} maw='500' withBorder radius='md' p='xl'>
<Box my={4}> <Flex direction='column' gap='sm'>
<Typography variant="h4" component="h1" gutterBottom> <Text size="xl" fw={500}>
Восстановление пароля Восстановление пароля
</Typography> </Text>
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
{!success && <Fade in={!success}> {!success && <Fade in={!success}>
<Stack spacing={2}> <Flex direction='column' gap={'md'}>
<Typography> <Text>
Введите адрес электронной почты, на который будут отправлены новые данные для авторизации: Введите адрес электронной почты, на который будут отправлены новые данные для авторизации:
</Typography> </Text>
<TextField <TextInput
fullWidth label='E-mail'
margin="normal"
label="E-mail"
required required
{...register('email', { required: 'Введите E-mail' })} {...register('email', { required: 'Введите E-mail' })}
error={!!errors.email} error={errors.email?.message}
helperText={errors.email?.message}
/> />
<Box sx={{ display: 'flex', gap: '16px' }}> <Flex gap='sm'>
<Button fullWidth type="submit" disabled={isSubmitting || watch('email').length == 0} variant="contained" color="primary"> <Button flex={1} type="submit" disabled={isSubmitting || watch('email').length == 0} variant='filled'>
{isSubmitting ? <CircularProgress size={16} /> : 'Восстановить пароль'} {isSubmitting ? <CircularProgress size={16} /> : 'Восстановить пароль'}
</Button> </Button>
<Button fullWidth href="/auth/signin" type="button" variant="text" color="primary"> <Button flex={1} component='a' href="/auth/signin" type="button" variant='light'>
Назад Назад
</Button> </Button>
</Box> </Flex>
</Stack> </Flex>
</Fade>} </Fade>}
{success && {success &&
<Grow in={success}> <Grow in={success}>
<Stack spacing={2}> <Flex direction='column' gap='sm'>
<Stack direction='row' alignItems='center' spacing={2}> <Flex align='center' gap='sm'>
<CheckCircle color='success' /> <CheckCircle color='success' />
<Typography> <Text>
На указанный адрес было отправлено письмо с новыми данными для авторизации. На указанный адрес было отправлено письмо с новыми данными для авторизации.
</Typography> </Text>
</Stack> </Flex>
<Box sx={{ display: 'flex', gap: '16px' }}> <Flex gap='sm'>
<Button fullWidth href="/auth/signin" type="button" variant="contained" color="primary"> <Button component='a' href="/auth/signin" type="button">
Войти Войти
</Button> </Button>
</Box> </Flex>
</Stack> </Flex>
</Grow> </Grow>
} }
</form> </form>
</Box> </Flex>
</Container> </Paper>
) )
} }

View File

@ -1,14 +1,14 @@
import { useForm, SubmitHandler } from 'react-hook-form'; import { useForm, SubmitHandler } from 'react-hook-form';
import { TextField, Button, Container, Typography, Box, Stack, Link, CircularProgress } from '@mui/material';
import { AxiosResponse } from 'axios'; import { AxiosResponse } from 'axios';
import { ApiResponse, LoginFormData } from '../../interfaces/auth'; import { ApiResponse, LoginFormData } from '../../interfaces/auth';
import { login, setUserData } from '../../store/auth'; import { login, setUserData } from '../../store/auth';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import AuthService from '../../services/AuthService'; import AuthService from '../../services/AuthService';
import UserService from '../../services/UserService'; import UserService from '../../services/UserService';
import { Button, Flex, Loader, Paper, Text, TextInput } from '@mantine/core';
const SignIn = () => { const SignIn = () => {
const { register, handleSubmit, setError, formState: { errors, isSubmitting } } = useForm<LoginFormData>({ const { register, handleSubmit, setError, formState: { errors, isSubmitting, isValid } } = useForm<LoginFormData>({
defaultValues: { defaultValues: {
username: '', username: '',
password: '', password: '',
@ -47,54 +47,48 @@ const SignIn = () => {
}; };
return ( return (
<Container maxWidth="sm"> <Paper flex={1} maw='500' withBorder radius='md' p='xl'>
<Box my={4}> <Flex direction='column' gap='sm'>
<Typography variant="h4" component="h1" gutterBottom> <Text size="xl" fw={500}>
Вход Вход
</Typography> </Text>
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<Stack spacing={2}> <Flex direction='column' gap='sm'>
<TextField <TextInput
fullWidth label='Логин'
margin="normal"
label="Логин"
required required
{...register('username', { required: 'Введите логин' })} {...register('username', { required: 'Введите логин' })}
error={!!errors.username} error={errors.username?.message}
helperText={errors.username?.message}
/> />
<TextField <TextInput
fullWidth label='Пароль'
margin="normal" type='password'
type="password"
label="Пароль"
required required
{...register('password', { required: 'Введите пароль' })} {...register('password', { required: 'Введите пароль' })}
error={!!errors.password} error={errors.password?.message}
helperText={errors.password?.message}
/> />
<Box sx={{ display: 'flex', gap: '16px', justifyContent: 'flex-end' }}> <Flex justify='flex-end' gap='sm'>
<Link href="/auth/password-reset" color="primary"> <Button component='a' href='/auth/password-reset' variant='transparent'>
Восстановить пароль Восстановить пароль
</Link> </Button>
</Box> </Flex>
<Box sx={{ display: 'flex', gap: '16px' }}> <Flex gap='sm'>
<Button fullWidth type="submit" variant="contained" color="primary"> <Button disabled={!isValid} type="submit" flex={1} variant='filled'>
{isSubmitting ? <CircularProgress size={16} /> : 'Вход'} {isSubmitting ? <Loader size={16} /> : 'Вход'}
</Button> </Button>
{/* <Button fullWidth href="/auth/signup" type="button" variant="text" color="primary"> {/* <Button component='a' flex={1} href='/auth/signup' type="button" variant='light'>
Регистрация Регистрация
</Button> */} </Button> */}
</Box> </Flex>
</Stack> </Flex>
</form> </form>
</Box> </Flex>
</Container> </Paper>
); );
}; };

File diff suppressed because it is too large Load Diff