forked from VinokurovVE/tests
Rename; Added EMS server; redis compose
This commit is contained in:
156
client/src/components/AccountMenu.tsx
Normal file
156
client/src/components/AccountMenu.tsx
Normal file
@ -0,0 +1,156 @@
|
||||
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>
|
||||
);
|
||||
}
|
23
client/src/components/CardInfo/CardInfo.tsx
Normal file
23
client/src/components/CardInfo/CardInfo.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { Divider, Paper, Typography } from '@mui/material'
|
||||
import { PropsWithChildren } from 'react'
|
||||
|
||||
interface CardInfoProps extends PropsWithChildren {
|
||||
label: string;
|
||||
}
|
||||
|
||||
export default function CardInfo({
|
||||
children,
|
||||
label
|
||||
}: CardInfoProps) {
|
||||
return (
|
||||
<Paper sx={{ display: 'flex', flexDirection: 'column', gap: '16px', p: '16px' }}>
|
||||
<Typography fontWeight={600}>
|
||||
{label}
|
||||
</Typography>
|
||||
|
||||
<Divider />
|
||||
|
||||
{children}
|
||||
</Paper>
|
||||
)
|
||||
}
|
25
client/src/components/CardInfo/CardInfoChip.tsx
Normal file
25
client/src/components/CardInfo/CardInfoChip.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { Chip } from '@mui/material'
|
||||
import { ReactElement } from 'react'
|
||||
|
||||
interface CardInfoChipProps {
|
||||
status: boolean;
|
||||
label: string;
|
||||
iconOn: ReactElement
|
||||
iconOff: ReactElement
|
||||
}
|
||||
|
||||
export default function CardInfoChip({
|
||||
status,
|
||||
label,
|
||||
iconOn,
|
||||
iconOff
|
||||
}: CardInfoChipProps) {
|
||||
return (
|
||||
<Chip
|
||||
icon={status ? iconOn : iconOff}
|
||||
variant="outlined"
|
||||
label={label}
|
||||
color={status ? "success" : "error"}
|
||||
/>
|
||||
)
|
||||
}
|
22
client/src/components/CardInfo/CardInfoLabel.tsx
Normal file
22
client/src/components/CardInfo/CardInfoLabel.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { Box, Typography } from '@mui/material'
|
||||
interface CardInfoLabelProps {
|
||||
label: string;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export default function CardInfoLabel({
|
||||
label,
|
||||
value
|
||||
}: CardInfoLabelProps) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography>
|
||||
{label}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h6" fontWeight={600}>
|
||||
{value}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
21
client/src/components/FetchingData.ts
Normal file
21
client/src/components/FetchingData.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import axiosInstance from '../http/axiosInstance'
|
||||
export function useDataFetching<T>(url: string, initData: T): T {
|
||||
const [data, setData] = useState<T>(initData)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const response = await axiosInstance.get(url)
|
||||
const result = await response.data
|
||||
setData(result)
|
||||
}
|
||||
|
||||
fetchData()
|
||||
}, [url])
|
||||
|
||||
// Memoize the data value
|
||||
const memoizedData = useMemo<T>(() => data, [data])
|
||||
return memoizedData
|
||||
}
|
||||
|
||||
export default useDataFetching;
|
343
client/src/components/FolderViewer.tsx
Normal file
343
client/src/components/FolderViewer.tsx
Normal file
@ -0,0 +1,343 @@
|
||||
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 DocumentService from '../services/DocumentService'
|
||||
import { mutate } from 'swr'
|
||||
import FileViewer from './modals/FileViewer'
|
||||
|
||||
interface FolderProps {
|
||||
folder: IDocumentFolder;
|
||||
index: number;
|
||||
handleFolderClick: (folder: IDocumentFolder) => void;
|
||||
}
|
||||
|
||||
interface DocumentProps {
|
||||
doc: IDocument;
|
||||
index: number;
|
||||
handleDocumentClick: (index: number) => void;
|
||||
}
|
||||
|
||||
const FileItemStyle: SxProps = {
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
gap: '8px',
|
||||
alignItems: 'center',
|
||||
padding: '8px'
|
||||
}
|
||||
|
||||
function ItemFolder({ folder, handleFolderClick, ...props }: FolderProps) {
|
||||
return (
|
||||
<ListItemButton
|
||||
onClick={() => handleFolderClick(folder)}
|
||||
>
|
||||
<Box
|
||||
sx={FileItemStyle}
|
||||
{...props}
|
||||
>
|
||||
<Folder />
|
||||
{folder.name}
|
||||
</Box>
|
||||
</ListItemButton>
|
||||
)
|
||||
}
|
||||
|
||||
const handleSave = async (file: Blob, filename: string) => {
|
||||
const link = document.createElement('a')
|
||||
link.href = window.URL.createObjectURL(file)
|
||||
link.download = filename
|
||||
link.click()
|
||||
link.remove()
|
||||
window.URL.revokeObjectURL(link.href)
|
||||
}
|
||||
|
||||
function ItemDocument({ doc, index, handleDocumentClick, ...props }: DocumentProps) {
|
||||
const [shouldFetch, setShouldFetch] = useState(false)
|
||||
|
||||
const { file, isLoading } = useDownload(shouldFetch ? doc?.document_folder_id : null, shouldFetch ? doc?.id : null)
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldFetch) {
|
||||
if (file) {
|
||||
handleSave(file, doc.name)
|
||||
setShouldFetch(false)
|
||||
}
|
||||
}
|
||||
}, [shouldFetch, file])
|
||||
|
||||
return (
|
||||
<ListItemButton>
|
||||
<Box
|
||||
sx={FileItemStyle}
|
||||
onClick={() => handleDocumentClick(index)}
|
||||
{...props}
|
||||
>
|
||||
<InsertDriveFile />
|
||||
{doc.name}
|
||||
</Box>
|
||||
<Box>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (!isLoading) {
|
||||
setShouldFetch(true)
|
||||
}
|
||||
}}
|
||||
sx={{ ml: 'auto' }}
|
||||
>
|
||||
{isLoading ?
|
||||
<CircularProgress size={24} variant='indeterminate' />
|
||||
:
|
||||
<Download />
|
||||
}
|
||||
</IconButton>
|
||||
</Box>
|
||||
</ListItemButton>
|
||||
)
|
||||
}
|
||||
|
||||
export default function FolderViewer() {
|
||||
const [currentFolder, setCurrentFolder] = useState<IDocumentFolder | null>(null)
|
||||
const [breadcrumbs, setBreadcrumbs] = useState<IDocumentFolder[]>([])
|
||||
const { folders, isLoading: foldersLoading } = useFolders()
|
||||
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)
|
||||
|
||||
const [dragOver, setDragOver] = useState(false)
|
||||
const [filesToUpload, setFilesToUpload] = useState<File[]>([])
|
||||
|
||||
const handleFolderClick = (folder: IDocumentFolder) => {
|
||||
setCurrentFolder(folder)
|
||||
setBreadcrumbs((prev) => [...prev, folder])
|
||||
}
|
||||
|
||||
const handleDocumentClick = async (index: number) => {
|
||||
setCurrentFileNo(index)
|
||||
setFileViewerModal(true)
|
||||
}
|
||||
|
||||
const handleBreadcrumbClick = (index: number) => {
|
||||
const newBreadcrumbs = breadcrumbs.slice(0, index + 1);
|
||||
setBreadcrumbs(newBreadcrumbs)
|
||||
setCurrentFolder(newBreadcrumbs[newBreadcrumbs.length - 1])
|
||||
}
|
||||
|
||||
const handleUploadClick = () => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click()
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setDragOver(true)
|
||||
}
|
||||
|
||||
const handleDragLeave = () => {
|
||||
setDragOver(false)
|
||||
}
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setDragOver(false)
|
||||
const files = Array.from(e.dataTransfer.files)
|
||||
setFilesToUpload((prevFiles) => [...prevFiles, ...files])
|
||||
}
|
||||
|
||||
const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files || [])
|
||||
setFilesToUpload((prevFiles) => [...prevFiles, ...files])
|
||||
}
|
||||
|
||||
const uploadFiles = async () => {
|
||||
setIsUploading(true)
|
||||
if (filesToUpload.length > 0 && currentFolder && currentFolder.id) {
|
||||
const formData = new FormData()
|
||||
for (const file of filesToUpload) {
|
||||
formData.append('files', file)
|
||||
}
|
||||
try {
|
||||
await DocumentService.uploadFiles(currentFolder.id, formData, setUploadProgress);
|
||||
setIsUploading(false);
|
||||
setFilesToUpload([]);
|
||||
mutate(`/info/documents/${currentFolder.id}`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setIsUploading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foldersLoading || documentsLoading) {
|
||||
return (
|
||||
<CircularProgress />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '16px'
|
||||
}}>
|
||||
<FileViewer
|
||||
open={fileViewerModal}
|
||||
setOpen={setFileViewerModal}
|
||||
currentFileNo={currentFileNo}
|
||||
setCurrentFileNo={setCurrentFileNo}
|
||||
docs={documents}
|
||||
/>
|
||||
|
||||
<Breadcrumbs>
|
||||
<Link
|
||||
underline='hover'
|
||||
color='inherit'
|
||||
onClick={() => {
|
||||
setCurrentFolder(null)
|
||||
setBreadcrumbs([])
|
||||
}}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
>
|
||||
Главная
|
||||
</Link>
|
||||
|
||||
{breadcrumbs.map((breadcrumb, index) => (
|
||||
<Link
|
||||
key={breadcrumb.id}
|
||||
underline="hover"
|
||||
color="inherit"
|
||||
onClick={() => handleBreadcrumbClick(index)}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
>
|
||||
{breadcrumb.name}
|
||||
</Link>
|
||||
))}
|
||||
</Breadcrumbs>
|
||||
|
||||
{currentFolder &&
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '16px',
|
||||
border: filesToUpload.length > 0 ? '1px dashed gray' : 'none',
|
||||
borderRadius: '8px',
|
||||
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>
|
||||
|
||||
{filesToUpload.length > 0 &&
|
||||
<>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<Upload />}
|
||||
onClick={uploadFiles}
|
||||
>
|
||||
Загрузить все
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant='outlined'
|
||||
startIcon={<Cancel />}
|
||||
onClick={() => {
|
||||
setFilesToUpload([])
|
||||
}}
|
||||
>
|
||||
Отмена
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
{filesToUpload.length > 0 &&
|
||||
<Box>
|
||||
{filesToUpload.map((file, index) => (
|
||||
<Box key={index} sx={{ display: 'flex', alignItems: 'center', gap: '8px', marginTop: '8px' }}>
|
||||
<Box>
|
||||
<InsertDriveFile />
|
||||
<span>{file.name}</span>
|
||||
</Box>
|
||||
|
||||
<IconButton sx={{ ml: 'auto' }} onClick={() => {
|
||||
setFilesToUpload(prev => {
|
||||
return prev.filter((_, i) => i != index)
|
||||
})
|
||||
}}>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
|
||||
<List
|
||||
dense
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
sx={{
|
||||
backgroundColor: dragOver ? 'rgba(0, 0, 0, 0.1)' : 'inherit'
|
||||
}}
|
||||
>
|
||||
{currentFolder ? (
|
||||
documents?.map((doc: IDocument, index: number) => (
|
||||
<div key={`${doc.id}-${doc.name}`}>
|
||||
<ItemDocument
|
||||
doc={doc}
|
||||
index={index}
|
||||
handleDocumentClick={handleDocumentClick}
|
||||
/>
|
||||
{index < documents.length - 1 && <Divider />}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
folders?.map((folder: IDocumentFolder, index: number) => (
|
||||
<div key={`${folder.id}-${folder.name}`}>
|
||||
<ItemFolder
|
||||
folder={folder}
|
||||
index={index}
|
||||
handleFolderClick={handleFolderClick}
|
||||
/>
|
||||
{index < folders.length - 1 && <Divider />}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</List>
|
||||
</Box>
|
||||
)
|
||||
}
|
100
client/src/components/FormFields.tsx
Normal file
100
client/src/components/FormFields.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
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';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
submitHandler?: (data: any) => Promise<AxiosResponse<any, any>>;
|
||||
fields: CreateField[];
|
||||
submitButtonText?: string;
|
||||
mutateHandler?: any;
|
||||
defaultValues?: {};
|
||||
watchValues?: string[];
|
||||
sx?: SxProps | null;
|
||||
}
|
||||
|
||||
function FormFields({
|
||||
title = '',
|
||||
submitHandler,
|
||||
fields,
|
||||
submitButtonText = 'Сохранить',
|
||||
mutateHandler,
|
||||
defaultValues,
|
||||
sx
|
||||
}: Props) {
|
||||
const getDefaultValues = (fields: CreateField[]) => {
|
||||
let result: { [key: string]: string | boolean } = {}
|
||||
fields.forEach((field: CreateField) => {
|
||||
result[field.key] = field.defaultValue || defaultValues?.[field.key as keyof {}]
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
const { register, handleSubmit, reset, watch, formState: { errors, isSubmitting, dirtyFields, isValid } } = useForm({
|
||||
mode: 'onChange',
|
||||
defaultValues: defaultValues ? getDefaultValues(fields) : {}
|
||||
})
|
||||
|
||||
const onSubmit: SubmitHandler<any> = async (data) => {
|
||||
fields.forEach((field: CreateField) => {
|
||||
if (field.include === false) {
|
||||
delete data[field.key]
|
||||
}
|
||||
})
|
||||
try {
|
||||
const submitResponse = await submitHandler?.(data)
|
||||
mutateHandler?.(JSON.stringify(submitResponse?.data))
|
||||
reset(submitResponse?.data)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Stack sx={sx} spacing={2} width='100%'>
|
||||
<Typography variant="h6" component="h6" gutterBottom>
|
||||
{title}
|
||||
</Typography>
|
||||
|
||||
{fields.map((field: CreateField) => {
|
||||
return (
|
||||
<TextField
|
||||
fullWidth
|
||||
margin='normal'
|
||||
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}
|
||||
{...register(field.key, {
|
||||
required: field.required ? `${field.headerName} обязателен` : false,
|
||||
validate: (val: string | boolean) => {
|
||||
if (field.watch) {
|
||||
if (watch(field.watch) != val) {
|
||||
return field.watchMessage || ''
|
||||
}
|
||||
}
|
||||
},
|
||||
})}
|
||||
error={!!errors[field.key]}
|
||||
helperText={errors[field.key]?.message}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
<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>
|
||||
</Box>
|
||||
</Stack>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default FormFields
|
39
client/src/components/ServerData.tsx
Normal file
39
client/src/components/ServerData.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
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'
|
||||
|
||||
function ServerData({ id }: IServer) {
|
||||
const { serverIps } = useServerIps(id, 0, 10)
|
||||
|
||||
const serverIpsColumns: GridColDef[] = [
|
||||
{ field: 'id', headerName: 'ID', type: 'number' },
|
||||
{ field: 'server_id', headerName: 'Server ID', type: 'number' },
|
||||
{ field: 'name', headerName: 'Название', type: 'string' },
|
||||
{ field: 'is_actual', headerName: 'Действителен', type: 'boolean' },
|
||||
{ field: 'ip', headerName: 'IP', type: 'string' },
|
||||
{ field: 'servername', headerName: 'Название сервера', type: 'string' },
|
||||
]
|
||||
|
||||
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}
|
||||
/>
|
||||
}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServerData
|
125
client/src/components/ServerHardware.tsx
Normal file
125
client/src/components/ServerHardware.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
import { AppBar, Autocomplete, CircularProgress, Dialog, IconButton, TextField, 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'
|
||||
|
||||
export default function ServerHardware() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [selectedOption, setSelectedOption] = useState<IRegion | null>(null)
|
||||
const { servers, isLoading } = useServers()
|
||||
|
||||
const [serverDataOpen, setServerDataOpen] = useState(false)
|
||||
const [currentServerData, setCurrentServerData] = useState<any | null>(null)
|
||||
|
||||
const handleInputChange = (value: string) => {
|
||||
return value
|
||||
}
|
||||
|
||||
const handleOptionChange = (value: IRegion | null) => {
|
||||
setSelectedOption(value)
|
||||
}
|
||||
|
||||
const { hardwares, isLoading: serversLoading } = useHardwares(selectedOption?.id, 0, 10)
|
||||
|
||||
const hardwareColumns: GridColDef[] = [
|
||||
{ field: 'id', headerName: 'ID', type: 'number' },
|
||||
{ field: 'name', headerName: 'Название', type: 'string' },
|
||||
{ field: 'server_id', headerName: 'Server ID', type: 'number' },
|
||||
{ field: 'servername', headerName: 'Название сервера', type: 'string' },
|
||||
{ field: 'os_info', headerName: 'ОС', type: 'string' },
|
||||
{ field: 'ram', headerName: 'ОЗУ', type: 'string' },
|
||||
{ field: 'processor', headerName: 'Проц.', type: 'string' },
|
||||
{ field: 'storages_count', headerName: 'Кол-во хранилищ', type: 'number' },
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
fullScreen
|
||||
open={serverDataOpen}
|
||||
onClose={() => {
|
||||
setServerDataOpen(false)
|
||||
}}
|
||||
aria-labelledby="modal-modal-title"
|
||||
aria-describedby="modal-modal-description">
|
||||
<AppBar sx={{ position: 'sticky' }}>
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
onClick={() => {
|
||||
setServerDataOpen(false)
|
||||
}}
|
||||
aria-label="close"
|
||||
>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
{currentServerData &&
|
||||
<ServerData
|
||||
id={currentServerData?.id}
|
||||
region_id={currentServerData?.region_id}
|
||||
name={currentServerData?.name}
|
||||
/>
|
||||
}
|
||||
</Dialog>
|
||||
|
||||
{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}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
121
client/src/components/ServerIpsView.tsx
Normal file
121
client/src/components/ServerIpsView.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import { AppBar, Autocomplete, 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'
|
||||
|
||||
export default function ServerIpsView() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [selectedOption, setSelectedOption] = useState<IRegion | null>(null)
|
||||
const { servers, isLoading } = useServers()
|
||||
|
||||
const [serverDataOpen, setServerDataOpen] = useState(false)
|
||||
const [currentServerData, setCurrentServerData] = useState<any | null>(null)
|
||||
|
||||
const handleInputChange = (value: string) => {
|
||||
return value
|
||||
}
|
||||
|
||||
const handleOptionChange = (value: IRegion | null) => {
|
||||
setSelectedOption(value)
|
||||
}
|
||||
|
||||
const { serverIps, isLoading: serversLoading } = useServerIps(selectedOption?.id, 0, 10)
|
||||
|
||||
const serverIpsColumns: GridColDef[] = [
|
||||
{ field: 'id', headerName: 'ID', type: 'number' },
|
||||
{ field: 'server_id', headerName: 'Server ID', type: 'number' },
|
||||
{ field: 'name', headerName: 'Название', type: 'string' },
|
||||
{ field: 'is_actual', headerName: 'Действителен', type: 'boolean' },
|
||||
{ field: 'ip', headerName: 'IP', type: 'string' },
|
||||
{ field: 'servername', headerName: 'Название сервера', type: 'string' },
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
fullScreen
|
||||
open={serverDataOpen}
|
||||
onClose={() => {
|
||||
setServerDataOpen(false)
|
||||
}}
|
||||
aria-labelledby="modal-modal-title"
|
||||
aria-describedby="modal-modal-description">
|
||||
<AppBar sx={{ position: 'sticky' }}>
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
onClick={() => {
|
||||
setServerDataOpen(false)
|
||||
}}
|
||||
aria-label="close"
|
||||
>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
{currentServerData &&
|
||||
<ServerData
|
||||
id={currentServerData?.id}
|
||||
region_id={currentServerData?.region_id}
|
||||
name={currentServerData?.name}
|
||||
/>
|
||||
}
|
||||
</Dialog>
|
||||
|
||||
{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} />
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
122
client/src/components/ServerStorages.tsx
Normal file
122
client/src/components/ServerStorages.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
import { AppBar, Autocomplete, CircularProgress, Dialog, IconButton, TextField, Toolbar } from '@mui/material'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { IRegion } from '../interfaces/fuel'
|
||||
import { useHardwares, useStorages } 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'
|
||||
|
||||
export default function ServerStorage() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [selectedOption, setSelectedOption] = useState<IRegion | null>(null)
|
||||
const { hardwares, isLoading } = useHardwares()
|
||||
|
||||
const [serverDataOpen, setServerDataOpen] = useState(false)
|
||||
const [currentServerData, setCurrentServerData] = useState<any | null>(null)
|
||||
|
||||
const handleInputChange = (value: string) => {
|
||||
return value
|
||||
}
|
||||
|
||||
const handleOptionChange = (value: IRegion | null) => {
|
||||
setSelectedOption(value)
|
||||
}
|
||||
|
||||
const { storages, isLoading: serversLoading } = useStorages(selectedOption?.id, 0, 10)
|
||||
|
||||
const storageColumns: GridColDef[] = [
|
||||
{ field: 'id', headerName: 'ID', type: 'number' },
|
||||
{ field: 'hardware_id', headerName: 'Hardware ID', type: 'number' },
|
||||
{ field: 'name', headerName: 'Название', type: 'string' },
|
||||
{ field: 'size', headerName: 'Размер', type: 'string' },
|
||||
{ field: 'storage_type', headerName: 'Тип хранилища', type: 'string' },
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
fullScreen
|
||||
open={serverDataOpen}
|
||||
onClose={() => {
|
||||
setServerDataOpen(false)
|
||||
}}
|
||||
aria-labelledby="modal-modal-title"
|
||||
aria-describedby="modal-modal-description">
|
||||
<AppBar sx={{ position: 'sticky' }}>
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
onClick={() => {
|
||||
setServerDataOpen(false)
|
||||
}}
|
||||
aria-label="close"
|
||||
>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
{currentServerData &&
|
||||
<ServerData
|
||||
id={currentServerData?.id}
|
||||
region_id={currentServerData?.region_id}
|
||||
name={currentServerData?.name}
|
||||
/>
|
||||
}
|
||||
</Dialog>
|
||||
|
||||
{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={hardwares || []}
|
||||
loading={isLoading}
|
||||
value={selectedOption}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
size='small'
|
||||
label="Hardware"
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
endAdornment: (
|
||||
<Fragment>
|
||||
{isLoading ? <CircularProgress color="inherit" size={20} /> : null}
|
||||
{params.InputProps.endAdornment}
|
||||
</Fragment>
|
||||
)
|
||||
}} />
|
||||
)} />}
|
||||
onSave={() => {
|
||||
}}
|
||||
onDelete={ServerService.removeServer}
|
||||
initialRows={storages || []}
|
||||
columns={storageColumns}
|
||||
actions
|
||||
onRowClick={(params) => {
|
||||
setCurrentServerData(params.row)
|
||||
setServerDataOpen(true)
|
||||
}}
|
||||
loading={false}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
177
client/src/components/ServersView.tsx
Normal file
177
client/src/components/ServersView.tsx
Normal file
@ -0,0 +1,177 @@
|
||||
import { AppBar, Autocomplete, 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'
|
||||
import ServerData from './ServerData'
|
||||
import { IServersInfo } from '../interfaces/servers'
|
||||
import CardInfo from './CardInfo/CardInfo'
|
||||
import CardInfoLabel from './CardInfo/CardInfoLabel'
|
||||
import CardInfoChip from './CardInfo/CardInfoChip'
|
||||
import { useDebounce } from '@uidotdev/usehooks'
|
||||
|
||||
export default function ServersView() {
|
||||
const [search, setSearch] = useState<string | null>("")
|
||||
|
||||
const debouncedSearch = useDebounce(search, 500)
|
||||
|
||||
const [selectedOption, setSelectedOption] = useState<IRegion | null>(null)
|
||||
|
||||
const { regions, isLoading } = useRegions(10, 1, debouncedSearch)
|
||||
|
||||
const { serversInfo } = useServersInfo(selectedOption?.id)
|
||||
|
||||
const [serverDataOpen, setServerDataOpen] = useState(false)
|
||||
const [currentServerData, setCurrentServerData] = useState<any | null>(null)
|
||||
|
||||
const { servers, isLoading: serversLoading } = useServers(selectedOption?.id, 0, 10)
|
||||
|
||||
const serversColumns: GridColDef[] = [
|
||||
//{ field: 'id', headerName: 'ID', type: "number" },
|
||||
{
|
||||
field: 'name', headerName: 'Название', type: "string", editable: true,
|
||||
},
|
||||
{
|
||||
field: 'region_id',
|
||||
editable: true,
|
||||
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>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
width: 200
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
fullScreen
|
||||
open={serverDataOpen}
|
||||
onClose={() => {
|
||||
setServerDataOpen(false)
|
||||
}}
|
||||
aria-labelledby="modal-modal-title"
|
||||
aria-describedby="modal-modal-description">
|
||||
<AppBar sx={{ position: 'sticky' }}>
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
onClick={() => {
|
||||
setServerDataOpen(false)
|
||||
}}
|
||||
aria-label="close"
|
||||
>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
{currentServerData &&
|
||||
<ServerData
|
||||
id={currentServerData?.id}
|
||||
region_id={currentServerData?.region_id}
|
||||
name={currentServerData?.name}
|
||||
/>
|
||||
}
|
||||
</Dialog>
|
||||
|
||||
{serversInfo &&
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', height: '100%' }}>
|
||||
<Grid container spacing={{ xs: 2, md: 3 }} columns={{ xs: 1, sm: 1, md: 2, lg: 3, xl: 4 }}>
|
||||
{serversInfo.map((serverInfo: IServersInfo) => (
|
||||
<Grid key={`si-${serverInfo.id}`} item xs={1} sm={1} md={1}>
|
||||
<CardInfo label={serverInfo.name}>
|
||||
<CardInfoLabel label='Количество IP' value={serverInfo.IPs_count} />
|
||||
<CardInfoLabel label='Количество серверов' value={serverInfo.servers_count} />
|
||||
<CardInfoChip
|
||||
status={serverInfo.status === "Online"}
|
||||
label={serverInfo.status}
|
||||
iconOn={<Cloud />}
|
||||
iconOff={<CloudOff />}
|
||||
/>
|
||||
</CardInfo>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
}
|
||||
|
||||
<FullFeaturedCrudGrid
|
||||
loading={serversLoading}
|
||||
autoComplete={
|
||||
<Autocomplete
|
||||
onInputChange={(_, value) => setSearch(value)}
|
||||
onChange={(_, value) => setSelectedOption(value)}
|
||||
isOptionEqualToValue={(option: IRegion, value: IRegion) => option.id === value.id}
|
||||
getOptionLabel={(option: IRegion) => option.name ? option.name : ""}
|
||||
options={regions || []}
|
||||
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={servers}
|
||||
columns={serversColumns}
|
||||
actions
|
||||
onRowClick={(params) => {
|
||||
setCurrentServerData(params.row)
|
||||
setServerDataOpen(true)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
233
client/src/components/TableEditable.tsx
Normal file
233
client/src/components/TableEditable.tsx
Normal file
@ -0,0 +1,233 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/DeleteOutlined';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import CancelIcon from '@mui/icons-material/Close';
|
||||
import {
|
||||
GridRowsProp,
|
||||
GridRowModesModel,
|
||||
GridRowModes,
|
||||
DataGrid,
|
||||
GridColDef,
|
||||
GridToolbarContainer,
|
||||
GridActionsCellItem,
|
||||
GridEventListener,
|
||||
GridRowId,
|
||||
GridRowModel,
|
||||
GridRowEditStopReasons,
|
||||
GridSlots,
|
||||
} from '@mui/x-data-grid';
|
||||
|
||||
interface EditToolbarProps {
|
||||
setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void;
|
||||
setRowModesModel: (
|
||||
newModel: (oldModel: GridRowModesModel) => GridRowModesModel,
|
||||
) => void;
|
||||
columns: GridColDef[];
|
||||
autoComplete?: React.ReactElement | null;
|
||||
}
|
||||
|
||||
function EditToolbar(props: EditToolbarProps) {
|
||||
const { setRows, setRowModesModel, columns, autoComplete } = props;
|
||||
|
||||
const handleClick = () => {
|
||||
const id = Date.now().toString(36)
|
||||
const newValues: any = {};
|
||||
|
||||
columns.forEach(column => {
|
||||
if (column.type === 'number') {
|
||||
newValues[column.field] = 0
|
||||
} else if (column.type === 'string') {
|
||||
newValues[column.field] = ''
|
||||
} else if (column.type === 'boolean') {
|
||||
newValues[column.field] = false
|
||||
} else {
|
||||
newValues[column.field] = undefined
|
||||
}
|
||||
|
||||
if (column.field === 'region_id') {
|
||||
// column.valueGetter = (value: any) => {
|
||||
// console.log(value)
|
||||
// }
|
||||
}
|
||||
})
|
||||
|
||||
setRows((oldRows) => [...oldRows, { id, ...newValues, isNew: true }]);
|
||||
setRowModesModel((oldModel) => ({
|
||||
...oldModel,
|
||||
[id]: { mode: GridRowModes.Edit, fieldToFocus: columns[0].field },
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<GridToolbarContainer sx={{ px: '16px', py: '16px' }}>
|
||||
{autoComplete &&
|
||||
<Box sx={{ flexGrow: '1' }}>
|
||||
{autoComplete}
|
||||
</Box>
|
||||
}
|
||||
|
||||
<Button color="primary" startIcon={<AddIcon />} onClick={handleClick}>
|
||||
Добавить
|
||||
</Button>
|
||||
</GridToolbarContainer>
|
||||
);
|
||||
}
|
||||
|
||||
interface DataGridProps {
|
||||
initialRows: GridRowsProp;
|
||||
columns: GridColDef[];
|
||||
actions: boolean;
|
||||
onRowClick: GridEventListener<"rowClick">;
|
||||
onSave: any;
|
||||
onDelete: any;
|
||||
autoComplete?: React.ReactElement | null;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export default function FullFeaturedCrudGrid({
|
||||
initialRows,
|
||||
columns,
|
||||
actions = false,
|
||||
onRowClick,
|
||||
onSave,
|
||||
onDelete,
|
||||
autoComplete,
|
||||
loading
|
||||
}: DataGridProps) {
|
||||
const [rows, setRows] = useState(initialRows);
|
||||
const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({});
|
||||
|
||||
const handleRowEditStop: GridEventListener<'rowEditStop'> = (params, event) => {
|
||||
if (params.reason === GridRowEditStopReasons.rowFocusOut) {
|
||||
event.defaultMuiPrevented = true;
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditClick = (id: GridRowId) => () => {
|
||||
setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } });
|
||||
};
|
||||
|
||||
const handleSaveClick = (id: GridRowId) => () => {
|
||||
setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } });
|
||||
onSave?.(id)
|
||||
};
|
||||
|
||||
const handleDeleteClick = (id: GridRowId) => () => {
|
||||
setRows(rows.filter((row) => row.id !== id));
|
||||
onDelete?.(id)
|
||||
};
|
||||
|
||||
const handleCancelClick = (id: GridRowId) => () => {
|
||||
setRowModesModel({
|
||||
...rowModesModel,
|
||||
[id]: { mode: GridRowModes.View, ignoreModifications: true },
|
||||
});
|
||||
|
||||
const editedRow = rows.find((row) => row.id === id);
|
||||
if (editedRow!.isNew) {
|
||||
setRows(rows.filter((row) => row.id !== id));
|
||||
}
|
||||
};
|
||||
|
||||
const processRowUpdate = (newRow: GridRowModel) => {
|
||||
const updatedRow = { ...newRow, isNew: false };
|
||||
setRows(rows.map((row) => (row.id === newRow.id ? updatedRow : row)));
|
||||
return updatedRow;
|
||||
};
|
||||
|
||||
const handleRowModesModelChange = (newRowModesModel: GridRowModesModel) => {
|
||||
setRowModesModel(newRowModesModel);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (initialRows) {
|
||||
setRows(initialRows)
|
||||
}
|
||||
}, [initialRows])
|
||||
|
||||
const actionColumns: GridColDef[] = [
|
||||
{
|
||||
field: 'actions',
|
||||
type: 'actions',
|
||||
headerName: 'Действия',
|
||||
width: 100,
|
||||
cellClassName: 'actions',
|
||||
getActions: ({ id }) => {
|
||||
const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit;
|
||||
|
||||
if (isInEditMode) {
|
||||
return [
|
||||
<GridActionsCellItem
|
||||
icon={<SaveIcon />}
|
||||
label="Save"
|
||||
sx={{
|
||||
color: 'primary.main',
|
||||
}}
|
||||
onClick={handleSaveClick(id)}
|
||||
/>,
|
||||
<GridActionsCellItem
|
||||
icon={<CancelIcon />}
|
||||
label="Cancel"
|
||||
className="textPrimary"
|
||||
onClick={handleCancelClick(id)}
|
||||
color="inherit"
|
||||
/>,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
<GridActionsCellItem
|
||||
icon={<EditIcon />}
|
||||
label="Edit"
|
||||
className="textPrimary"
|
||||
onClick={handleEditClick(id)}
|
||||
color="inherit"
|
||||
/>,
|
||||
<GridActionsCellItem
|
||||
icon={<DeleteIcon />}
|
||||
label="Delete"
|
||||
onClick={handleDeleteClick(id)}
|
||||
color="inherit"
|
||||
/>,
|
||||
];
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: 500,
|
||||
width: '100%',
|
||||
'& .actions': {
|
||||
color: 'text.secondary',
|
||||
},
|
||||
'& .textPrimary': {
|
||||
color: 'text.primary',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DataGrid
|
||||
loading={loading}
|
||||
rows={rows || []}
|
||||
columns={actions ? [...columns, ...actionColumns] : columns}
|
||||
editMode="row"
|
||||
rowModesModel={rowModesModel}
|
||||
//onRowClick={onRowClick}
|
||||
onRowModesModelChange={handleRowModesModelChange}
|
||||
onRowEditStop={handleRowEditStop}
|
||||
processRowUpdate={processRowUpdate}
|
||||
slots={{
|
||||
toolbar: EditToolbar as GridSlots['toolbar'],
|
||||
}}
|
||||
slotProps={{
|
||||
toolbar: { setRows, setRowModesModel, columns, autoComplete },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
18
client/src/components/UserData.ts
Normal file
18
client/src/components/UserData.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import UserService from "../services/UserService";
|
||||
|
||||
export default function useUserData<T>(token: string, initData: T): T {
|
||||
const [userData, setUserData] = useState<T>(initData)
|
||||
|
||||
useEffect(()=> {
|
||||
const fetchUserData = async (token: string) => {
|
||||
const response = await UserService.getCurrentUser(token)
|
||||
setUserData(response.data)
|
||||
}
|
||||
|
||||
fetchUserData(token)
|
||||
}, [token])
|
||||
|
||||
const memoizedData = useMemo<T>(() => userData, [userData])
|
||||
return memoizedData
|
||||
}
|
175
client/src/components/map/MapComponent.tsx
Normal file
175
client/src/components/map/MapComponent.tsx
Normal file
@ -0,0 +1,175 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import GeoJSON from 'ol/format/GeoJSON'
|
||||
import 'ol/ol.css'
|
||||
import Map from 'ol/Map'
|
||||
import View from 'ol/View'
|
||||
import { Draw, Modify, Snap } from 'ol/interaction'
|
||||
import { OSM, Vector as VectorSource } from 'ol/source'
|
||||
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'
|
||||
import { transform, transformExtent } from 'ol/proj'
|
||||
import { Divider, IconButton, Stack } from '@mui/material'
|
||||
import { Adjust, Api, CircleOutlined, RectangleOutlined, Timeline, Undo, Warning } from '@mui/icons-material'
|
||||
import { Type } from 'ol/geom/Geometry'
|
||||
|
||||
const MapComponent = () => {
|
||||
const mapElement = useRef<HTMLDivElement | null>(null)
|
||||
const [currentTool, setCurrentTool] = useState<Type>('Point')
|
||||
|
||||
const map = useRef<Map | null>(null)
|
||||
const source = useRef<VectorSource>(new VectorSource())
|
||||
|
||||
const draw = useRef<Draw | null>(null)
|
||||
const snap = useRef<Snap | null>(null)
|
||||
|
||||
const drawingLayer = useRef<VectorLayer | null>(null)
|
||||
|
||||
const addInteractions = () => {
|
||||
draw.current = new Draw({
|
||||
source: source.current,
|
||||
type: currentTool,
|
||||
})
|
||||
map?.current?.addInteraction(draw.current)
|
||||
snap.current = new Snap({ source: source.current })
|
||||
map?.current?.addInteraction(snap.current)
|
||||
}
|
||||
|
||||
// Function to save features to localStorage
|
||||
const saveFeatures = () => {
|
||||
const features = drawingLayer.current?.getSource()?.getFeatures()
|
||||
if (features && features.length > 0) {
|
||||
const geoJSON = new GeoJSON()
|
||||
const featuresJSON = geoJSON.writeFeatures(features)
|
||||
localStorage.setItem('savedFeatures', featuresJSON)
|
||||
}
|
||||
}
|
||||
|
||||
// Function to load features from localStorage
|
||||
const loadFeatures = () => {
|
||||
const savedFeatures = localStorage.getItem('savedFeatures')
|
||||
if (savedFeatures) {
|
||||
const geoJSON = new GeoJSON()
|
||||
const features = geoJSON.readFeatures(savedFeatures, {
|
||||
featureProjection: 'EPSG:4326', // Ensure the projection is correct
|
||||
})
|
||||
source.current?.addFeatures(features) // Add features to the vector source
|
||||
//drawingLayer.current?.getSource()?.changed()
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const geoLayer = new VectorLayer({
|
||||
background: '#1a2b39',
|
||||
source: new VectorSource({
|
||||
url: 'https://openlayers.org/data/vector/ecoregions.json',
|
||||
format: new GeoJSON(),
|
||||
}),
|
||||
style: {
|
||||
'fill-color': ['string', ['get', 'COLOR'], '#eee'],
|
||||
},
|
||||
})
|
||||
|
||||
const raster = new TileLayer({
|
||||
source: new OSM(),
|
||||
})
|
||||
|
||||
drawingLayer.current = new VectorLayer({
|
||||
source: source.current,
|
||||
style: {
|
||||
'fill-color': 'rgba(255, 255, 255, 0.2)',
|
||||
'stroke-color': '#ffcc33',
|
||||
'stroke-width': 2,
|
||||
'circle-radius': 7,
|
||||
'circle-fill-color': '#ffcc33',
|
||||
},
|
||||
})
|
||||
|
||||
// Center coordinates of Yakutia in EPSG:3857
|
||||
const center = transform([129.7694, 66.9419], 'EPSG:4326', 'EPSG:3857')
|
||||
|
||||
// Extent for Yakutia in EPSG:4326
|
||||
const extent4326 = [105.0, 55.0, 170.0, 75.0] // Approximate bounding box
|
||||
// Transform extent to EPSG:3857
|
||||
const extent = transformExtent(extent4326, 'EPSG:4326', 'EPSG:3857')
|
||||
|
||||
map.current = new Map({
|
||||
layers: [geoLayer, raster, drawingLayer.current],
|
||||
target: mapElement.current as HTMLDivElement,
|
||||
view: new View({
|
||||
center,
|
||||
zoom: 4,
|
||||
extent,
|
||||
}),
|
||||
})
|
||||
|
||||
const modify = new Modify({ source: source.current })
|
||||
map.current.addInteraction(modify)
|
||||
|
||||
addInteractions()
|
||||
|
||||
loadFeatures()
|
||||
|
||||
return () => {
|
||||
map?.current?.setTarget(undefined)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (currentTool) {
|
||||
if (draw.current) map?.current?.removeInteraction(draw.current)
|
||||
if (snap.current) map?.current?.removeInteraction(snap.current)
|
||||
addInteractions()
|
||||
}
|
||||
}, [currentTool])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Stack my={1} spacing={1} direction='row' divider={<Divider orientation='vertical' flexItem />}>
|
||||
<IconButton onClick={() => {
|
||||
fetch(`${import.meta.env.VITE_API_EMS_URL}/hello`, { method: 'GET' }).then(res => console.log(res))
|
||||
}}>
|
||||
<Api />
|
||||
</IconButton>
|
||||
|
||||
<IconButton onClick={() => {
|
||||
saveFeatures()
|
||||
}}>
|
||||
<Warning />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
draw.current?.removeLastPoint()
|
||||
}}>
|
||||
<Undo />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
sx={{ backgroundColor: currentTool === 'Point' ? 'Highlight' : 'transparent' }}
|
||||
onClick={() => setCurrentTool('Point')}>
|
||||
<Adjust />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
sx={{ backgroundColor: currentTool === 'LineString' ? 'Highlight' : 'transparent' }}
|
||||
onClick={() => setCurrentTool('LineString')}>
|
||||
<Timeline />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
sx={{ backgroundColor: currentTool === 'Polygon' ? 'Highlight' : 'transparent' }}
|
||||
onClick={() => setCurrentTool('Polygon')}>
|
||||
<RectangleOutlined />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
sx={{ backgroundColor: currentTool === 'Circle' ? 'Highlight' : 'transparent' }}
|
||||
onClick={() => setCurrentTool('Circle')}>
|
||||
<CircleOutlined />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<div ref={mapElement} style={{ width: '100%', height: '400px' }}></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MapComponent
|
268
client/src/components/modals/FileViewer.tsx
Normal file
268
client/src/components/modals/FileViewer.tsx
Normal file
@ -0,0 +1,268 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { AppBar, Box, Button, CircularProgress, Dialog, IconButton, Toolbar, Typography } from '@mui/material';
|
||||
import { ChevronLeft, ChevronRight, Close, Warning } from '@mui/icons-material';
|
||||
import { useDownload, useFileType } from '../../hooks/swrHooks';
|
||||
|
||||
import jsPreviewExcel from "@js-preview/excel"
|
||||
import '@js-preview/excel/lib/index.css'
|
||||
|
||||
import jsPreviewDocx from "@js-preview/docx"
|
||||
import '@js-preview/docx/lib/index.css'
|
||||
|
||||
import jsPreviewPdf from '@js-preview/pdf'
|
||||
import { IDocument } from '../../interfaces/documents';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
setOpen: (state: boolean) => void;
|
||||
docs: IDocument[];
|
||||
currentFileNo: number;
|
||||
setCurrentFileNo: (state: number) => void;
|
||||
}
|
||||
|
||||
interface ViewerProps {
|
||||
url: string
|
||||
}
|
||||
|
||||
function PdfViewer({
|
||||
url
|
||||
}: ViewerProps) {
|
||||
const previewContainerRef = useRef(null)
|
||||
|
||||
const pdfPreviewer = jsPreviewPdf
|
||||
|
||||
useEffect(() => {
|
||||
if (previewContainerRef && previewContainerRef.current) {
|
||||
pdfPreviewer.init(previewContainerRef.current)
|
||||
.preview(url)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (previewContainerRef && previewContainerRef.current) {
|
||||
previewContainerRef.current = null
|
||||
}
|
||||
}
|
||||
}, [previewContainerRef])
|
||||
|
||||
return (
|
||||
<Box ref={previewContainerRef} sx={{
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}} />
|
||||
)
|
||||
}
|
||||
|
||||
function DocxViewer({
|
||||
url
|
||||
}: ViewerProps) {
|
||||
const previewContainerRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (previewContainerRef && previewContainerRef.current) {
|
||||
jsPreviewDocx.init(previewContainerRef.current, {
|
||||
breakPages: true,
|
||||
inWrapper: true,
|
||||
ignoreHeight: true,
|
||||
})
|
||||
.preview(url)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (previewContainerRef && previewContainerRef.current) {
|
||||
previewContainerRef.current = null
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Box ref={previewContainerRef} sx={{
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}} />
|
||||
)
|
||||
}
|
||||
|
||||
function ExcelViewer({
|
||||
url
|
||||
}: ViewerProps) {
|
||||
const previewContainerRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (previewContainerRef && previewContainerRef.current) {
|
||||
jsPreviewExcel.init(previewContainerRef.current)
|
||||
.preview(url)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (previewContainerRef && previewContainerRef.current) {
|
||||
previewContainerRef.current = null
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Box ref={previewContainerRef} sx={{
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}} />
|
||||
)
|
||||
}
|
||||
|
||||
function ImageViewer({
|
||||
url
|
||||
}: ViewerProps) {
|
||||
return (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
objectFit: 'contain',
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}}>
|
||||
<img alt='image-preview' src={url} style={{
|
||||
display: 'flex',
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%'
|
||||
}} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default function FileViewer({
|
||||
open,
|
||||
setOpen,
|
||||
docs,
|
||||
currentFileNo,
|
||||
setCurrentFileNo
|
||||
}: Props) {
|
||||
const { file, isLoading: fileIsLoading } = useDownload(currentFileNo >= 0 ? docs[currentFileNo]?.document_folder_id : null, currentFileNo >= 0 ? docs[currentFileNo]?.id : null)
|
||||
|
||||
const { fileType, isLoading: fileTypeIsLoading } = useFileType(currentFileNo >= 0 ? docs[currentFileNo]?.name : null, currentFileNo >= 0 ? file : null)
|
||||
|
||||
const handleSave = async () => {
|
||||
const url = window.URL.createObjectURL(file)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.setAttribute('download', docs[currentFileNo].name)
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
link.remove()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
fullScreen
|
||||
open={open}
|
||||
onClose={() => {
|
||||
setOpen(false)
|
||||
setCurrentFileNo(-1)
|
||||
}}
|
||||
aria-labelledby="modal-modal-title"
|
||||
aria-describedby="modal-modal-description"
|
||||
>
|
||||
<AppBar sx={{ position: 'sticky' }}>
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
setCurrentFileNo(-1)
|
||||
}}
|
||||
aria-label="close"
|
||||
>
|
||||
<Close />
|
||||
</IconButton>
|
||||
|
||||
<Typography sx={{ ml: 2, flex: 1 }} variant="h6" component="div">
|
||||
{currentFileNo != -1 && docs[currentFileNo].name}
|
||||
</Typography>
|
||||
|
||||
<div>
|
||||
<IconButton
|
||||
color='inherit'
|
||||
onClick={() => {
|
||||
if (currentFileNo >= 0 && currentFileNo > 0) {
|
||||
setCurrentFileNo(currentFileNo - 1)
|
||||
}
|
||||
}}
|
||||
disabled={currentFileNo >= 0 && currentFileNo === 0}
|
||||
>
|
||||
<ChevronLeft />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
color='inherit'
|
||||
onClick={() => {
|
||||
if (currentFileNo >= 0 && currentFileNo < docs.length) {
|
||||
setCurrentFileNo(currentFileNo + 1)
|
||||
}
|
||||
}}
|
||||
disabled={currentFileNo >= 0 && currentFileNo >= docs.length - 1}
|
||||
>
|
||||
<ChevronRight />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
autoFocus
|
||||
color="inherit"
|
||||
onClick={handleSave}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
<Box sx={{
|
||||
flexGrow: '1',
|
||||
overflowY: 'hidden'
|
||||
}}>
|
||||
{fileIsLoading || fileTypeIsLoading ?
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
:
|
||||
fileType === 'application/pdf' ?
|
||||
<PdfViewer url={window.URL.createObjectURL(file)} />
|
||||
:
|
||||
fileType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ?
|
||||
<ExcelViewer url={window.URL.createObjectURL(file)} />
|
||||
:
|
||||
fileType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ?
|
||||
<DocxViewer url={window.URL.createObjectURL(file)} />
|
||||
:
|
||||
fileType?.startsWith('image/') ?
|
||||
<ImageViewer url={window.URL.createObjectURL(file)} />
|
||||
:
|
||||
fileType && file ?
|
||||
<Box sx={{ display: 'flex', gap: '16px', flexDirection: 'column', p: '16px' }}>
|
||||
<Box sx={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
|
||||
<Warning />
|
||||
<Typography>
|
||||
Предпросмотр данного файла невозможен.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Button variant='contained' onClick={() => {
|
||||
handleSave()
|
||||
}}>
|
||||
Сохранить
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
:
|
||||
null
|
||||
}
|
||||
</Box>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
171
client/src/components/navigation/Drawer/ResponsiveDrawer.tsx
Normal file
171
client/src/components/navigation/Drawer/ResponsiveDrawer.tsx
Normal file
@ -0,0 +1,171 @@
|
||||
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>
|
||||
);
|
||||
}
|
30
client/src/components/navigation/NavTabs.tsx
Normal file
30
client/src/components/navigation/NavTabs.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
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>
|
||||
);
|
||||
|
||||
}
|
Reference in New Issue
Block a user