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/excel": "^1.7.8",
"@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/material": "^5.15.20",
"@mui/x-charts": "^7.8.0",
"@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",
"@uidotdev/usehooks": "^2.4.1",
"autoprefixer": "^10.4.19",
"axios": "^1.7.2",
"buffer": "^6.0.3",
"dayjs": "^1.11.13",
"embla-carousel-react": "^8.3.0",
"file-type": "^19.0.0",
"ka-table": "^11.3.0",
"ol": "^10.0.0",
"ol-ext": "^4.0.23",
"postcss": "^8.4.38",
"proj4": "^2.12.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.52.0",
"react-router-dom": "^6.23.1",
"recharts": "^2.12.7",
"swr": "^2.2.5",
"zustand": "^4.5.2"
},
@ -50,6 +70,9 @@
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
"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",
"tailwindcss": "^3.4.4",
"typescript": "^5.2.2",

View File

@ -1,6 +1,14 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
'postcss-preset-mantine': {},
'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 Roles from "./pages/Roles"
import NotFound from "./pages/NotFound"
import DashboardLayout from "./layouts/DashboardLayout"
import MainLayout from "./layouts/MainLayout"
import SignIn from "./pages/auth/SignIn"
import ApiTest from "./pages/ApiTest"
import SignUp from "./pages/auth/SignUp"
import { initAuth, useAuthStore } from "./store/auth"
import { useEffect, useState } from "react"
import { Box, CircularProgress } from "@mui/material"
import Documents from "./pages/Documents"
import Reports from "./pages/Reports"
import Boilers from "./pages/Boilers"
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 PasswordReset from "./pages/auth/PasswordReset"
import MapTest from "./pages/MapTest"
import MonitorPage from "./pages/MonitorPage"
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 = [
{
label: "",
path: "/auth/signin",
icon: <Login />,
icon: <IconLogin2 />,
component: <SignIn />,
drawer: false,
dashboard: false,
@ -36,7 +37,7 @@ export const pages = [
{
label: "",
path: "/auth/signup",
icon: <Login />,
icon: <IconLogin />,
component: <SignUp />,
drawer: false,
dashboard: false,
@ -45,7 +46,7 @@ export const pages = [
{
label: "",
path: "/auth/password-reset",
icon: <Password />,
icon: <IconPassword />,
component: <PasswordReset />,
drawer: false,
dashboard: false,
@ -54,7 +55,7 @@ export const pages = [
{
label: "Настройки",
path: "/settings",
icon: <SettingsIcon />,
icon: <IconSettings />,
component: <Settings />,
drawer: false,
dashboard: true,
@ -63,7 +64,7 @@ export const pages = [
{
label: "Главная",
path: "/",
icon: <Home />,
icon: <IconHome />,
component: <Main />,
drawer: true,
dashboard: true,
@ -72,7 +73,7 @@ export const pages = [
{
label: "Пользователи",
path: "/user",
icon: <People />,
icon: <IconUsers />,
component: <Users />,
drawer: true,
dashboard: true,
@ -81,7 +82,7 @@ export const pages = [
{
label: "Роли",
path: "/role",
icon: <Shield />,
icon: <IconShield />,
component: <Roles />,
drawer: true,
dashboard: true,
@ -90,7 +91,7 @@ export const pages = [
{
label: "Документы",
path: "/documents",
icon: <Storage />,
icon: <IconFiles />,
component: <Documents />,
drawer: true,
dashboard: true,
@ -99,7 +100,7 @@ export const pages = [
{
label: "Отчеты",
path: "/reports",
icon: <Assignment />,
icon: <IconReport />,
component: <Reports />,
drawer: true,
dashboard: true,
@ -108,7 +109,7 @@ export const pages = [
{
label: "Серверы",
path: "/servers",
icon: <Cloud />,
icon: <IconServer />,
component: <Servers />,
drawer: true,
dashboard: true,
@ -117,7 +118,7 @@ export const pages = [
{
label: "Котельные",
path: "/boilers",
icon: <Factory />,
icon: <IconBuildingFactory2 />,
component: <Boilers />,
drawer: true,
dashboard: true,
@ -126,7 +127,7 @@ export const pages = [
{
label: "API Test",
path: "/api-test",
icon: <Api />,
icon: <IconApi />,
component: <ApiTest />,
drawer: true,
dashboard: true,
@ -135,16 +136,16 @@ export const pages = [
{
label: "ИКС",
path: "/map-test",
icon: <Map />,
icon: <IconMap />,
component: <MapTest />,
drawer: true,
dashboard: true,
enabled: false,
enabled: true,
},
{
label: "Chunk test",
path: "/chunk-test",
icon: <Warning />,
icon: <IconFlag2 />,
component: <ChunkedUpload />,
drawer: true,
dashboard: true,
@ -153,12 +154,21 @@ export const pages = [
{
label: "Монитор",
path: "/monitor",
icon: <MonitorHeart />,
icon: <IconDeviceDesktopAnalytics />,
component: <MonitorPage />,
drawer: true,
dashboard: true,
enabled: false,
},
{
label: "Table test",
path: "/table-test",
icon: <IconTable />,
component: <TableTest />,
drawer: true,
dashboard: true,
enabled: true,
},
]
function App() {
@ -178,14 +188,11 @@ function App() {
if (isLoading) {
return (
<CircularProgress />
<Loader />
)
} else {
return (
<Box sx={{
width: "100%",
height: "100vh"
}}>
<Box w='100%' h='100vh'>
<Router>
<Routes>
<Route element={<MainLayout />}>
@ -194,7 +201,7 @@ function App() {
))}
</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) => (
<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 { IDocument, IDocumentFolder } from '../interfaces/documents'
import { Box, Breadcrumbs, Button, CircularProgress, Divider, IconButton, Link, List, ListItemButton, SxProps } from '@mui/material'
import { Cancel, Close, Download, Folder, InsertDriveFile, Upload, UploadFile } from '@mui/icons-material'
import React, { useEffect, useRef, useState } from 'react'
import { Box, CircularProgress, Divider, SxProps } from '@mui/material'
import { Folder, InsertDriveFile } from '@mui/icons-material'
import React, { useEffect, useState } from 'react'
import DocumentService from '../services/DocumentService'
import { mutate } from 'swr'
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 {
folder: IDocumentFolder;
@ -31,7 +33,7 @@ const FileItemStyle: SxProps = {
function ItemFolder({ folder, handleFolderClick, ...props }: FolderProps) {
return (
<ListItemButton
<Flex
onClick={() => handleFolderClick(folder)}
>
<Box
@ -41,7 +43,7 @@ function ItemFolder({ folder, handleFolderClick, ...props }: FolderProps) {
<Folder />
{folder.name}
</Box>
</ListItemButton>
</Flex>
)
}
@ -69,7 +71,7 @@ function ItemDocument({ doc, index, handleDocumentClick, ...props }: DocumentPro
}, [shouldFetch, file])
return (
<ListItemButton>
<Flex align='center'>
<Box
sx={FileItemStyle}
onClick={() => handleDocumentClick(index)}
@ -79,22 +81,21 @@ function ItemDocument({ doc, index, handleDocumentClick, ...props }: DocumentPro
{doc.name}
</Box>
<Box>
<IconButton
<ActionIcon
onClick={() => {
if (!isLoading) {
setShouldFetch(true)
}
}}
sx={{ ml: 'auto' }}
>
variant='subtle'>
{isLoading ?
<CircularProgress size={24} variant='indeterminate' />
<Loader size='sm' />
:
<Download />
<IconDownload />
}
</IconButton>
</ActionIcon>
</Box>
</ListItemButton>
</Flex>
)
}
@ -105,7 +106,6 @@ export default function FolderViewer() {
const { documents, isLoading: documentsLoading } = useDocuments(currentFolder?.id)
const [uploadProgress, setUploadProgress] = useState(0)
const [isUploading, setIsUploading] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)
const [fileViewerModal, setFileViewerModal] = useState(false)
const [currentFileNo, setCurrentFileNo] = useState<number>(-1)
@ -128,12 +128,6 @@ export default function FolderViewer() {
setCurrentFolder(newBreadcrumbs[newBreadcrumbs.length - 1])
}
const handleUploadClick = () => {
if (fileInputRef.current) {
fileInputRef.current.click()
}
}
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault()
setDragOver(true)
@ -150,10 +144,11 @@ export default function FolderViewer() {
setFilesToUpload((prevFiles) => [...prevFiles, ...files])
}
const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files || [])
const handleFileInput = (files: File[] | null) => {
if (files !== null) {
setFilesToUpload((prevFiles) => [...prevFiles, ...files])
}
}
const uploadFiles = async () => {
setIsUploading(true)
@ -196,28 +191,21 @@ export default function FolderViewer() {
/>
<Breadcrumbs>
<Link
underline='hover'
color='inherit'
<Anchor
onClick={() => {
setCurrentFolder(null)
setBreadcrumbs([])
}}
sx={{ cursor: 'pointer' }}
>
Главная
</Link>
</Anchor>
{breadcrumbs.map((breadcrumb, index) => (
<Link
<Anchor
key={breadcrumb.id}
underline="hover"
color="inherit"
onClick={() => handleBreadcrumbClick(index)}
sx={{ cursor: 'pointer' }}
>
{breadcrumb.name}
</Link>
</Anchor>
))}
</Breadcrumbs>
@ -232,44 +220,23 @@ export default function FolderViewer() {
p: '16px'
}}>
<Box sx={{ display: 'flex', gap: '16px' }}>
<Button
LinkComponent="label"
role={undefined}
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>
<FileButton multiple onChange={handleFileInput}>
{(props) => <Button variant='filled' leftSection={isUploading ? <Loader /> : <IconFilePlus />} {...props}>Добавить</Button>}
</FileButton>
{filesToUpload.length > 0 &&
<>
<Button
variant="contained"
color="primary"
startIcon={<Upload />}
variant='filled'
leftSection={isUploading ? <RingProgress sections={[{ value: uploadProgress, color: 'blue' }]} /> : <IconFileUpload />}
onClick={uploadFiles}
>
Загрузить все
</Button>
<Button
variant='outlined'
startIcon={<Cancel />}
variant='outline'
leftSection={<IconCancel />}
onClick={() => {
setFilesToUpload([])
}}
@ -283,62 +250,69 @@ export default function FolderViewer() {
<Divider />
{filesToUpload.length > 0 &&
<Box>
<Flex direction='column'>
{filesToUpload.map((file, index) => (
<Box key={index} sx={{ display: 'flex', alignItems: 'center', gap: '8px', marginTop: '8px' }}>
<Box>
<InsertDriveFile />
<span>{file.name}</span>
</Box>
<Flex key={index} p='8px'>
<Flex gap='sm' direction='row' align='center'>
<IconFile />
<Text>{file.name}</Text>
</Flex>
<IconButton sx={{ ml: 'auto' }} onClick={() => {
<ActionIcon onClick={() => {
setFilesToUpload(prev => {
return prev.filter((_, i) => i != index)
})
}}>
<Close />
</IconButton>
</Box>
}} ml='auto' variant='subtle'>
<IconX />
</ActionIcon>
</Flex>
))}
</Box>
</Flex>
}
</Box>
</Box>
}
<List
dense
<Table
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
sx={{
backgroundColor: dragOver ? 'rgba(0, 0, 0, 0.1)' : 'inherit'
}}
>
bg={dragOver ? 'rgba(0, 0, 0, 0.1)' : 'inherit'}
highlightOnHover>
<Table.Thead>
<Table.Tr>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{currentFolder ? (
documents?.map((doc: IDocument, index: number) => (
<div key={`${doc.id}-${doc.name}`}>
<Table.Tr key={doc.id}>
<Table.Td>
<ItemDocument
doc={doc}
index={index}
handleDocumentClick={handleDocumentClick}
/>
{index < documents.length - 1 && <Divider />}
</div>
</Table.Td>
</Table.Tr>
))
) : (
folders?.map((folder: IDocumentFolder, index: number) => (
<div key={`${folder.id}-${folder.name}`}>
<Table.Tr key={folder.id}>
<Table.Td>
<ItemFolder
folder={folder}
index={index}
handleFolderClick={handleFolderClick}
/>
{index < folders.length - 1 && <Divider />}
</div>
</Table.Td>
</Table.Tr>
))
)}
</List>
</Table.Tbody>
</Table>
</Box>
)
}

View File

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

View File

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

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 { IRegion } from '../interfaces/fuel'
import { useHardwares, useServers } from '../hooks/swrHooks'
import FullFeaturedCrudGrid from './TableEditable'
import ServerService from '../services/ServersService'
import { GridColDef } from '@mui/x-data-grid'
import { Close } from '@mui/icons-material'
import ServerData from './ServerData'
import { Autocomplete, CloseButton, Table } from '@mantine/core'
import { IServer } from '../interfaces/servers'
export default function ServerHardware() {
const [open, setOpen] = useState(false)
const [selectedOption, setSelectedOption] = useState<IRegion | null>(null)
const [selectedOption, setSelectedOption] = useState<number | null>(null)
const { servers, isLoading } = useServers()
const [serverDataOpen, setServerDataOpen] = useState(false)
@ -71,54 +72,96 @@ export default function ServerHardware() {
}
</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 ?
<CircularProgress />
:
<FullFeaturedCrudGrid
autoComplete={
<Autocomplete
open={open}
onOpen={() => {
setOpen(true)
}}
onClose={() => {
setOpen(false)
}}
onInputChange={(_, value) => handleInputChange(value)}
onChange={(_, value) => handleOptionChange(value)}
filterOptions={(x) => x}
isOptionEqualToValue={(option: IRegion, value: IRegion) => option.name === value.name}
getOptionLabel={(option: IRegion) => option.name ? option.name : ""}
options={servers || []}
loading={isLoading}
value={selectedOption}
renderInput={(params) => (
<TextField
{...params}
label="Сервер"
size='small'
InputProps={{
...params.InputProps,
endAdornment: (
<Fragment>
{isLoading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</Fragment>
)
}} />
)} />}
onSave={() => {
}}
onDelete={ServerService.removeServer}
initialRows={hardwares || []}
columns={hardwareColumns}
actions
onRowClick={(params) => {
setCurrentServerData(params.row)
setServerDataOpen(true)
}}
loading={false}
/>
// <FullFeaturedCrudGrid
// autoComplete={
// <Autocomplete
// open={open}
// onOpen={() => {
// setOpen(true)
// }}
// onClose={() => {
// setOpen(false)
// }}
// onInputChange={(_, value) => handleInputChange(value)}
// onChange={(_, value) => handleOptionChange(value)}
// filterOptions={(x) => x}
// isOptionEqualToValue={(option: IRegion, value: IRegion) => option.name === value.name}
// getOptionLabel={(option: IRegion) => option.name ? option.name : ""}
// options={servers || []}
// loading={isLoading}
// value={selectedOption}
// renderInput={(params) => (
// <TextField
// {...params}
// label="Сервер"
// size='small'
// InputProps={{
// ...params.InputProps,
// endAdornment: (
// <Fragment>
// {isLoading ? <CircularProgress color="inherit" size={20} /> : null}
// {params.InputProps.endAdornment}
// </Fragment>
// )
// }} />
// )} />}
// onSave={() => {
// }}
// onDelete={ServerService.removeServer}
// initialRows={hardwares || []}
// columns={hardwareColumns}
// actions
// onRowClick={(params) => {
// setCurrentServerData(params.row)
// setServerDataOpen(true)
// }}
// 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 { IRegion } from '../interfaces/fuel'
import { useServerIps, useServers } from '../hooks/swrHooks'
import FullFeaturedCrudGrid from './TableEditable'
import ServerService from '../services/ServersService'
import { GridColDef } from '@mui/x-data-grid'
import { Close } from '@mui/icons-material'
import ServerData from './ServerData'
import { Autocomplete, CloseButton, Table } from '@mantine/core'
import { IServer } from '../interfaces/servers'
export default function ServerIpsView() {
const [open, setOpen] = useState(false)
const [selectedOption, setSelectedOption] = useState<IRegion | null>(null)
const [selectedOption, setSelectedOption] = useState<number | null>(null)
const { servers, isLoading } = useServers()
const [serverDataOpen, setServerDataOpen] = useState(false)
@ -69,52 +70,97 @@ export default function ServerIpsView() {
}
</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 ?
<CircularProgress />
:
<FullFeaturedCrudGrid
autoComplete={
<Autocomplete
open={open}
onOpen={() => {
setOpen(true)
}}
onClose={() => {
setOpen(false)
}}
onInputChange={(_, value) => handleInputChange(value)}
onChange={(_, value) => handleOptionChange(value)}
filterOptions={(x) => x}
isOptionEqualToValue={(option: IRegion, value: IRegion) => option.name === value.name}
getOptionLabel={(option: IRegion) => option.name ? option.name : ""}
options={servers || []}
loading={isLoading}
value={selectedOption}
renderInput={(params) => (
<TextField
{...params}
size='small'
label="Сервер"
InputProps={{
...params.InputProps,
endAdornment: (
<Fragment>
{isLoading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</Fragment>
)
}} />
)} />}
onSave={() => {
}}
onDelete={ServerService.removeServer}
initialRows={serverIps || []}
columns={serverIpsColumns}
actions
onRowClick={(params) => {
setCurrentServerData(params.row)
setServerDataOpen(true)
}} loading={false} />
// <FullFeaturedCrudGrid
// autoComplete={
// <Autocomplete
// open={open}
// onOpen={() => {
// setOpen(true)
// }}
// onClose={() => {
// setOpen(false)
// }}
// onInputChange={(_, value) => handleInputChange(value)}
// onChange={(_, value) => handleOptionChange(value)}
// filterOptions={(x) => x}
// isOptionEqualToValue={(option: IRegion, value: IRegion) => option.name === value.name}
// getOptionLabel={(option: IRegion) => option.name ? option.name : ""}
// options={servers || []}
// loading={isLoading}
// value={selectedOption}
// renderInput={(params) => (
// <TextField
// {...params}
// size='small'
// label="Сервер"
// InputProps={{
// ...params.InputProps,
// endAdornment: (
// <Fragment>
// {isLoading ? <CircularProgress color="inherit" size={20} /> : null}
// {params.InputProps.endAdornment}
// </Fragment>
// )
// }} />
// )} />}
// onSave={() => {
// }}
// onDelete={ServerService.removeServer}
// initialRows={serverIps || []}
// columns={serverIpsColumns}
// actions
// onRowClick={(params) => {
// setCurrentServerData(params.row)
// setServerDataOpen(true)
// }} 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 { IRegion } from '../interfaces/fuel'
import { useRegions, useServers, useServersInfo } from '../hooks/swrHooks'
import FullFeaturedCrudGrid from './TableEditable'
import ServerService from '../services/ServersService'
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid'
import { Close, Cloud, CloudOff } from '@mui/icons-material'
@ -12,22 +11,23 @@ import CardInfo from './CardInfo/CardInfo'
import CardInfoLabel from './CardInfo/CardInfoLabel'
import CardInfoChip from './CardInfo/CardInfoChip'
import { useDebounce } from '@uidotdev/usehooks'
import { Autocomplete, CloseButton, Table } from '@mantine/core'
export default function ServersView() {
const [search, setSearch] = useState<string | null>("")
const [search, setSearch] = useState<string | undefined>("")
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 { serversInfo } = useServersInfo(selectedOption?.id)
const { serversInfo } = useServersInfo(selectedOption)
const [serverDataOpen, setServerDataOpen] = useState(false)
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[] = [
//{ field: 'id', headerName: 'ID', type: "number" },
@ -37,42 +37,43 @@ export default function ServersView() {
{
field: 'region_id',
editable: true,
headerName: 'region_id',
renderCell: (params) => (
<div>
{params.value}
</div>
),
renderEditCell: (params: GridRenderCellParams) => (
<Autocomplete
sx={{ display: 'flex', flexGrow: '1' }}
onInputChange={(_, value) => setSearch(value)}
onChange={(_, value) => {
params.value = value
}}
isOptionEqualToValue={(option: IRegion, value: IRegion) => option.name === value.name}
getOptionLabel={(option: IRegion) => option.name ? option.name : ""}
options={regions || []}
loading={isLoading}
value={params.value}
renderInput={(params) => (
<TextField
{...params}
size='small'
variant='standard'
label="Район"
InputProps={{
...params.InputProps,
endAdornment: (
<Fragment>
{isLoading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</Fragment>
)
}}
/>
)}
/>
),
// renderEditCell: (params: GridRenderCellParams) => (
// <Autocomplete
// sx={{ display: 'flex', flexGrow: '1' }}
// onInputChange={(_, value) => setSearch(value)}
// onChange={(_, value) => {
// params.value = value
// }}
// isOptionEqualToValue={(option: IRegion, value: IRegion) => option.name === value.name}
// getOptionLabel={(option: IRegion) => option.name ? option.name : ""}
// options={regions || []}
// loading={isLoading}
// value={params.value}
// renderInput={(params) => (
// <TextField
// {...params}
// size='small'
// variant='standard'
// label="Район"
// InputProps={{
// ...params.InputProps,
// endAdornment: (
// <Fragment>
// {isLoading ? <CircularProgress color="inherit" size={20} /> : null}
// {params.InputProps.endAdornment}
// </Fragment>
// )
// }}
// />
// )}
// />
// ),
flex: 1
}
]
@ -132,7 +133,50 @@ export default function ServersView() {
</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}
autoComplete={
<Autocomplete
@ -171,7 +215,7 @@ export default function ServersView() {
setCurrentServerData(params.row)
setServerDataOpen(true)
}}
/>
/> */}
</>
)
}

View File

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

View File

@ -1,96 +1,28 @@
import * as React from 'react';
import { styled, createTheme, ThemeProvider } from '@mui/material/styles';
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 { AppShell, Avatar, Burger, Button, Flex, Group, Menu, NavLink, rem, Text, useMantineColorScheme } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
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 { 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;
interface AppBarProps extends MuiAppBarProps {
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();
function DashboardLayout() {
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure()
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true)
const navigate = useNavigate()
const getPageTitle = () => {
const currentPath = location.pathname;
const allPages = [...pages];
const currentPage = allPages.find(page => page.path === currentPath);
return currentPage ? currentPage.label : "Dashboard";
};
const currentPath = location.pathname
const allPages = [...pages]
const currentPage = allPages.find(page => page.path === currentPath)
return currentPage ? currentPage.label : "Панель управления"
}
const [userData, setUserData] = React.useState<UserData>();
const authStore = useAuthStore()
const [userData, setUserData] = useState<UserData>()
React.useEffect(() => {
useEffect(() => {
if (authStore) {
const stored = getUserData()
if (stored) {
@ -99,113 +31,95 @@ export default function DashboardLayout() {
}
}, [authStore])
const { colorScheme, setColorScheme } = useMantineColorScheme();
return (
<ThemeProvider theme={innerTheme}>
<Box sx={{
display: 'flex',
height: "100%"
}}>
<CssBaseline />
<AppBar position="absolute" open={open}>
<Toolbar
sx={{
pr: '24px', // keep right padding when drawer closed
<AppShell
header={{ height: 60 }}
navbar={{
width: desktopOpened ? 200 : 50,
breakpoint: 'sm',
collapsed: { mobile: !mobileOpened },
}}
>
<IconButton
edge="start"
color="inherit"
aria-label="open drawer"
onClick={toggleDrawer}
sx={{
marginRight: '36px',
//...(open && { display: 'none' }),
}}
>
<MenuIcon />
</IconButton>
<AppShell.Header>
<Flex h="100%" px="md" w='100%' align='center' gap='sm'>
<Group>
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
<Burger opened={desktopOpened} onClick={toggleDesktop} visibleFrom="sm" size="sm" />
</Group>
<Typography
component="h1"
variant="h6"
color="inherit"
noWrap
sx={{ flexGrow: 1 }}
>
<Group w='100%'>
{getPageTitle()}
</Typography>
</Group>
<Box sx={{ display: "flex", gap: "8px" }}>
<Box>
<Typography>{userData?.name} {userData?.surname}</Typography>
<Divider />
<Typography variant="caption">{userData?.login}</Typography>
</Box>
<AccountMenu />
</Box>
</Toolbar>
</AppBar>
<Drawer variant="permanent" open={open}>
<Toolbar
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
px: [1],
}}
<Group style={{ flexShrink: 0 }}>
<Menu
width={260}
position="bottom-end"
transitionProps={{ transition: 'pop-top-right' }}
withinPortal
>
<Box display="flex" justifyContent={'space-between'} width={"100%"}>
</Box>
</Toolbar>
<Divider />
<List component="nav">
{pages.filter((page) => page.drawer).filter((page) => page.enabled).map((item, index) => (
<ListItem
key={index}
disablePadding
>
<ListItemButton
onClick={() => {
navigate(item.path)
}}
style={{ background: location.pathname === item.path ? innerTheme.palette.action.selected : "transparent" }}
selected={location.pathname === item.path}
>
<ListItemIcon>
{item.icon}
</ListItemIcon>
<ListItemText
primary={item.label}
sx={{ color: location.pathname === item.path ? colors.blue[700] : innerTheme.palette.text.primary }}
/>
</ListItemButton>
</ListItem>
))}
</List>
</Drawer>
<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 />
</Box>
</Box>
</ThemeProvider>
);
<Menu.Target>
<Button variant='transparent'>
<Group gap={7}>
<Avatar name={`${userData?.name} ${userData?.surname}`} radius="xl" size={30} />
<Text fw={500} size="sm" lh={1} mr={3}>
{`${userData?.name} ${userData?.surname}`}
</Text>
<IconChevronDown style={{ width: rem(12), height: rem(12) }} stroke={1.5} />
</Group>
</Button>
</Menu.Target>
<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')}
>
Тема: {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={() => {
logout()
navigate("/auth/signin")
}}
leftSection={<IconLogout style={{ width: rem(16), height: rem(16) }} stroke={1.5} />}
>
Выход
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Group>
</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'}}
/>
))}
</AppShell.Navbar>
<AppShell.Main>
<Outlet />
</AppShell.Main>
</AppShell>
)
}
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
import { Box, createTheme, ThemeProvider, useTheme } from "@mui/material";
import { Flex } from "@mantine/core";
import { Outlet } from "react-router-dom";
export default function MainLayout() {
const theme = useTheme()
const innerTheme = createTheme(theme)
return (
<ThemeProvider theme={innerTheme}>
<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',
}}
>
<Flex align='center' justify='center' h='100%' w='100%'>
<Outlet />
</Box>
</ThemeProvider>
</Flex>
)
}

View File

@ -1,98 +1,15 @@
import "@fontsource/inter";
import React, { useEffect } from 'react'
import '@mantine/core/styles.css';
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import { ThemeProvider } from '@emotion/react'
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>
)
}
import { MantineProvider } from '@mantine/core';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<ThemedApp />
<MantineProvider defaultColorScheme="light">
<App />
</MantineProvider>
</React.StrictMode>,
)

View File

@ -1,8 +1,7 @@
import { Box, Typography } from '@mui/material'
import { DataGrid, GridColDef } from '@mui/x-data-grid'
import { GridColDef } from '@mui/x-data-grid'
import { useEffect, useState } from 'react'
import { IBoiler } from '../interfaces/fuel'
import { useBoilers } from '../hooks/swrHooks'
import { Badge, Flex, Table, Text } from '@mantine/core'
function Boilers() {
const [boilersPage, setBoilersPage] = useState(1)
@ -26,7 +25,7 @@ function Boilers() {
}, [])
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_code', headerName: 'Код', type: "string", flex: 1 },
{ field: 'id_city', headerName: 'Город', type: "string", flex: 1 },
@ -34,23 +33,52 @@ function Boilers() {
]
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', height: '100%' }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', height: '100%', p: '16px' }}>
<Typography variant='h6' fontWeight='600'>
<Flex direction='column' gap='sm' p='sm'>
<Text size="xl" fw={600}>
Котельные
</Typography>
</Text>
{boilers &&
<DataGrid
rows={boilers.map((boiler: IBoiler) => {
return { ...boiler, id: boiler.id_object }
})}
columns={boilersColumns}
/>
}
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
{boilersColumns.map(column => (
<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() {
const navigate = useNavigate()
interface CustomCardProps {
link: string;
icon: ReactNode;
label: string;
}
const CustomCard = ({
link,
icon,
label
}: CustomCardProps) => {
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', p: '16px' }}>
<Typography variant='h6' fontWeight='700'>
Последние файлы
</Typography>
<Card>
<Card
onClick={() => navigate(link)}
withBorder
style={{ cursor: 'pointer', userSelect: 'none' }}
>
<Flex mih='50'>
{icon}
</Flex>
<Text fw={500} size="lg" mt="md">
{label}
</Text>
</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 { Box, Typography } from "@mui/material";
import { Flex, Text } from "@mantine/core";
import { IconError404 } from "@tabler/icons-react";
export default function NotFound() {
return (
<>
<Box sx={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
<Error />
<Typography>Запрашиваемая страница не найдена.</Typography>
</Box>
</>
<Flex p='sm' gap='sm' align='center' justify='center'>
<Flex direction='column' gap='sm' align='center'>
<IconError404 size={100} />
<Text size="xl" fw={500} ta='center'>
Запрашиваемая страница не найдена.
</Text>
</Flex>
</Flex>
)
}

View File

@ -1,26 +1,25 @@
import { Fragment, useEffect, useState } from "react"
import { Autocomplete, Box, Button, CircularProgress, IconButton, TextField } from "@mui/material"
import { DataGrid } from "@mui/x-data-grid"
import { useEffect, useState } from "react"
import { useCities, useReport, useReportExport } from "../hooks/swrHooks"
import { useDebounce } from "@uidotdev/usehooks"
import { ICity } from "../interfaces/fuel"
import { Update } from "@mui/icons-material"
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() {
const [download, setDownload] = useState(false)
const [search, setSearch] = useState<string | null>("")
const [search, setSearch] = useState<string | undefined>("")
const debouncedSearch = useDebounce(search, 500)
const [selectedOption, setSelectedOption] = useState<ICity | null>(null)
const { cities, isLoading } = useCities(10, 1, debouncedSearch)
const [selectedOption, setSelectedOption] = useState<number | null>(null)
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 () => {
mutate(`/info/reports/${selectedOption?.id}?to_export=false`)
mutate(`/info/reports/${selectedOption}?to_export=false`)
}
useEffect(() => {
@ -41,9 +40,32 @@ export default function Reports() {
}
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', p: '16px' }}>
<Box sx={{ display: 'flex', gap: '16px' }}>
<Flex direction='column' gap='sm' p='sm'>
<Flex component="form" gap={'sm'}>
{/* <SearchableSelect /> */}
<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
onInputChange={(_, value) => setSearch(value)}
onChange={(_, value) => setSelectedOption(value)}
@ -68,18 +90,78 @@ export default function Reports() {
}}
/>
)}
/>
/> */}
<IconButton onClick={() => refreshReport()}>
<Update />
</IconButton>
<ActionIcon size='auto' variant='transparent' onClick={() => refreshReport()}>
<IconRefresh />
</ActionIcon>
<Button onClick={() => exportReport()}>
<Button disabled={!selectedOption} onClick={() => exportReport()}>
Экспорт
</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
style={{ width: "100%" }}
loading={reportLoading}
@ -118,7 +200,7 @@ export default function Reports() {
onProcessRowUpdateError={() => {
}}
/>
</Box>
/> */}
</Flex>
)
}

View File

@ -1,15 +1,15 @@
import { useState } from 'react'
import { Box, Button, CircularProgress, Modal } from '@mui/material'
import { DataGrid, GridColDef } from '@mui/x-data-grid'
import { GridColDef } from '@mui/x-data-grid'
import { useRoles } from '../hooks/swrHooks'
import { CreateField } from '../interfaces/create'
import RoleService from '../services/RoleService'
import FormFields from '../components/FormFields'
import { Button, Flex, Loader, Modal, Table } from '@mantine/core'
import { useDisclosure } from '@mantine/hooks'
export default function Roles() {
const { roles, isError, isLoading } = useRoles()
const [open, setOpen] = useState(false)
const [opened, { open, close }] = useDisclosure(false);
const createFields: CreateField[] = [
{ key: 'name', headerName: 'Название', type: 'string', required: true, defaultValue: '' },
@ -23,43 +23,42 @@ export default function Roles() {
];
if (isError) return <div>Произошла ошибка при получении данных.</div>
if (isLoading) return <CircularProgress />
if (isLoading) return <Loader />
return (
<Box sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
gap: '16px',
flexGrow: 1,
p: '16px'
}}>
<Button onClick={() => setOpen(true)}>
<Flex direction='column' align='flex-start' gap='sm' p='sm'>
<Button onClick={open}>
Добавить роль
</Button>
<Modal
open={open}
onClose={() => setOpen(false)}
>
<Modal opened={opened} onClose={close} title="Создание роли" centered>
<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}
submitHandler={RoleService.createRole}
title="Создание роли"
/>
</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
style={{ width: "100%" }}
rows={roles}
@ -78,7 +77,7 @@ export default function Roles() {
onProcessRowUpdateError={() => {
}}
/>
</Box>
/> */}
</Flex>
)
}

View File

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

View File

@ -1,4 +1,3 @@
import { Box, Stack } from "@mui/material"
import UserService from "../services/UserService"
import { setUserData, useAuthStore } from "../store/auth"
import { useEffect, useState } from "react"
@ -6,6 +5,7 @@ import { CreateField } from "../interfaces/create"
import { IUser } from "../interfaces/user"
import FormFields from "../components/FormFields"
import AuthService from "../services/AuthService"
import { Flex } from "@mantine/core"
export default function Settings() {
const { token } = useAuthStore()
@ -39,17 +39,14 @@ export default function Settings() {
]
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
gap: "16px",
}}
<Flex
direction='column'
align='flex-start'
gap='sm'
p='sm'
>
{currentUser &&
<Stack spacing={2} width='100%'>
<Stack width='100%'>
<Flex direction='column' gap='sm' w='100%'>
<FormFields
fields={profileFields}
defaultValues={currentUser}
@ -59,17 +56,14 @@ export default function Settings() {
submitHandler={(data) => UserService.updateUser({ id: currentUser.id, ...data })}
title="Пользователь"
/>
</Stack>
<Stack width='100%'>
<FormFields
fields={passwordFields}
submitHandler={(data) => AuthService.updatePassword({ id: currentUser.id, ...data })}
title="Смена пароля"
/>
</Stack>
</Stack>
</Flex>
}
</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 { DataGrid, GridColDef } from "@mui/x-data-grid"
import { GridColDef } from "@mui/x-data-grid"
import { useRoles, useUsers } from "../hooks/swrHooks"
import { IRole } from "../interfaces/role"
import { useState } from "react"
import { useEffect, useState } from "react"
import { CreateField } from "../interfaces/create"
import UserService from "../services/UserService"
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() {
const { users, isError, isLoading } = useUsers()
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[] = [
{ 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: 'name', 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',
headerName: 'Роль',
@ -41,44 +50,92 @@ export default function Users() {
},
];
if (isError) return <div>Произошла ошибка при получении данных.</div>
if (isLoading) return <CircularProgress />
if (isError) return (
<div>
Произошла ошибка при получении данных.
</div>
)
if (isLoading) {
return (
<Flex direction='column' align='flex-start' gap='sm' p='sm'>
<Loader />
</Flex>
)
}
return (
<Box sx={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
gap: "16px",
p: '16px'
}}
>
<Button onClick={() => setOpen(true)}>
<Flex direction='column' align='flex-start' gap='sm' p='sm'>
<Button onClick={open}>
Добавить пользователя
</Button>
<Modal
open={open}
onClose={() => setOpen(false)}
>
<Modal opened={opened} onClose={close} title="Регистрация пользователя" centered>
<FormFields
fields={createFields}
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>
<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"
autoHeight
style={{ width: "100%" }}
@ -99,7 +156,7 @@ export default function Users() {
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 { SubmitHandler, useForm } from 'react-hook-form';
import AuthService from '../../services/AuthService';
import { CheckCircle } from '@mui/icons-material';
import { Button, Flex, Paper, Text, TextInput } from '@mantine/core';
interface PasswordResetProps {
email: string;
@ -31,61 +32,58 @@ function PasswordReset() {
}
return (
<Container maxWidth="sm">
<Box my={4}>
<Typography variant="h4" component="h1" gutterBottom>
<Paper flex={1} maw='500' withBorder radius='md' p='xl'>
<Flex direction='column' gap='sm'>
<Text size="xl" fw={500}>
Восстановление пароля
</Typography>
</Text>
<form onSubmit={handleSubmit(onSubmit)}>
{!success && <Fade in={!success}>
<Stack spacing={2}>
<Typography>
<Flex direction='column' gap={'md'}>
<Text>
Введите адрес электронной почты, на который будут отправлены новые данные для авторизации:
</Typography>
</Text>
<TextField
fullWidth
margin="normal"
label="E-mail"
<TextInput
label='E-mail'
required
{...register('email', { required: 'Введите E-mail' })}
error={!!errors.email}
helperText={errors.email?.message}
error={errors.email?.message}
/>
<Box sx={{ display: 'flex', gap: '16px' }}>
<Button fullWidth type="submit" disabled={isSubmitting || watch('email').length == 0} variant="contained" color="primary">
<Flex gap='sm'>
<Button flex={1} type="submit" disabled={isSubmitting || watch('email').length == 0} variant='filled'>
{isSubmitting ? <CircularProgress size={16} /> : 'Восстановить пароль'}
</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>
</Box>
</Flex>
</Stack>
</Flex>
</Fade>}
{success &&
<Grow in={success}>
<Stack spacing={2}>
<Stack direction='row' alignItems='center' spacing={2}>
<Flex direction='column' gap='sm'>
<Flex align='center' gap='sm'>
<CheckCircle color='success' />
<Typography>
<Text>
На указанный адрес было отправлено письмо с новыми данными для авторизации.
</Typography>
</Stack>
<Box sx={{ display: 'flex', gap: '16px' }}>
<Button fullWidth href="/auth/signin" type="button" variant="contained" color="primary">
</Text>
</Flex>
<Flex gap='sm'>
<Button component='a' href="/auth/signin" type="button">
Войти
</Button>
</Box>
</Stack>
</Flex>
</Flex>
</Grow>
}
</form>
</Box>
</Container>
</Flex>
</Paper>
)
}

View File

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

File diff suppressed because it is too large Load Diff