DataGrid cell autocomplete

This commit is contained in:
cracklesparkle
2024-07-15 17:48:48 +09:00
parent e566e23f6d
commit 4283bd20bb
13 changed files with 457 additions and 517 deletions

View File

@ -15,6 +15,90 @@ 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, People, Shield, Storage } from "@mui/icons-material"
export const pages = [
{
label: "",
path: "/auth/signin",
icon: <Login />,
component: <SignIn />,
drawer: false,
dashboard: false,
},
{
label: "",
path: "/auth/signup",
icon: <Login />,
component: <SignUp />,
drawer: false,
dashboard: false,
},
{
label: "Главная",
path: "/",
icon: <Home />,
component: <Main />,
drawer: true,
dashboard: true
},
{
label: "Пользователи",
path: "/user",
icon: <People />,
component: <Users />,
drawer: true,
dashboard: true
},
{
label: "Роли",
path: "/role",
icon: <Shield />,
component: <Roles />,
drawer: true,
dashboard: true
},
{
label: "Документы",
path: "/documents",
icon: <Storage />,
component: <Documents />,
drawer: true,
dashboard: true
},
{
label: "Отчеты",
path: "/reports",
icon: <Assignment />,
component: <Reports />,
drawer: true,
dashboard: true
},
{
label: "Серверы",
path: "/servers",
icon: <Cloud />,
component: <Servers />,
drawer: true,
dashboard: true
},
{
label: "Котельные",
path: "/boilers",
icon: <Factory />,
component: <Boilers />,
drawer: true,
dashboard: true
},
{
label: "API Test",
path: "/api-test",
icon: <Api />,
component: <ApiTest />,
drawer: true,
dashboard: true
},
]
function App() {
const auth = useAuthStore()
@ -44,19 +128,15 @@ function App() {
<Router>
<Routes>
<Route element={<MainLayout />}>
<Route path="/auth/signin" element={<SignIn />} />
<Route path="/auth/signup" element={<SignUp />} />
{pages.filter((page) => !page.dashboard).map((page, index) => (
<Route key={`ml-${index}`} path={page.path} element={page.component} />
))}
</Route>
<Route element={auth.isAuthenticated ? <DashboardLayout /> : <Navigate to={"/auth/signin"} />}>
<Route path="/" element={<Main />} />
<Route path="/user" element={<Users />} />
<Route path="/role" element={<Roles />} />
<Route path="/documents" element={<Documents />} />
<Route path="/reports" element={<Reports />} />
<Route path="/servers" element={<Servers />} />
<Route path="/boilers" element={<Boilers />} />
<Route path="/api-test" element={<ApiTest />} />
{pages.filter((page) => page.dashboard).map((page, index) => (
<Route key={`dl-${index}`} path={page.path} element={page.component} />
))}
<Route path="*" element={<NotFound />} />
</Route>
</Routes>

View File

@ -0,0 +1,23 @@
import { Box, Chip, Divider, Paper, Typography } from '@mui/material'
import React, { PropsWithChildren, ReactElement, ReactNode } 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>
)
}

View File

@ -0,0 +1,27 @@
import { Cloud } from '@mui/icons-material';
import { Chip, SvgIconTypeMap } from '@mui/material'
import { OverridableComponent } from '@mui/material/OverridableComponent';
import React, { 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"}
/>
)
}

View File

@ -0,0 +1,24 @@
import { Box, Typography } from '@mui/material'
import React from 'react'
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>
)
}

View File

@ -0,0 +1,79 @@
import { Autocomplete, CircularProgress, TextField } from '@mui/material'
import React, { Fragment, useEffect, useState } from 'react'
interface DataGridCellAutocompleteProps {
selectedOption: any;
setSelectedOption: any;
isLoading: boolean;
setDebouncedSearch: any;
options: any;
setOptions: any;
}
export default function DataGridCellAutocomplete({
selectedOption,
setSelectedOption,
isLoading,
setDebouncedSearch,
options,
setOptions
}: DataGridCellAutocompleteProps) {
const [open, setOpen] = useState(false)
const [search, setSearch] = useState<string | null>("")
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedSearch(search)
}, 500)
return () => {
clearTimeout(handler)
}
}, [search])
const handleInputChange = (value: string) => {
setSearch(value)
}
const handleOptionChange = (value: any | null) => {
setSelectedOption(value)
}
return (
<Autocomplete
sx={{ flexGrow: '1' }}
open={open}
onOpen={() => {
setOpen(true)
}}
onClose={() => {
setOpen(false)
}}
onInputChange={(_, value) => handleInputChange(value)}
onChange={(_, value) => handleOptionChange(value)}
filterOptions={(x) => x}
isOptionEqualToValue={(option: any, value: any) => option.name === value.name}
getOptionLabel={(option: any) => option.name ? option.name : ""}
options={options}
loading={isLoading}
value={selectedOption}
renderInput={(params) => (
<TextField
{...params}
size='small'
label="Район"
variant='standard'
InputProps={{
...params.InputProps,
endAdornment: (
<Fragment>
{isLoading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</Fragment>
)
}}
/>
)}
/>
)
}

View File

@ -90,44 +90,46 @@ export default function ServerHardware() {
}
</Dialog>
<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={options}
loading={isLoading}
value={selectedOption}
renderInput={(params) => (
<TextField
{...params}
label="Сервер"
InputProps={{
...params.InputProps,
endAdornment: (
<Fragment>
{isLoading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</Fragment>
)
}}
/>
)}
/>
{serversLoading ?
<CircularProgress />
:
servers &&
hardwares &&
<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={options}
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={(id: any) => {
console.log(id)
}}

View File

@ -88,44 +88,46 @@ export default function ServerIpsView() {
}
</Dialog>
<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={options}
loading={isLoading}
value={selectedOption}
renderInput={(params) => (
<TextField
{...params}
label="Сервер"
InputProps={{
...params.InputProps,
endAdornment: (
<Fragment>
{isLoading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</Fragment>
)
}}
/>
)}
/>
{serversLoading ?
<CircularProgress />
:
servers &&
serverIps &&
<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={options}
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={(id: any) => {
console.log(id)
}}

View File

@ -87,44 +87,46 @@ export default function ServerStorage() {
}
</Dialog>
<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={options}
loading={isLoading}
value={selectedOption}
renderInput={(params) => (
<TextField
{...params}
label="Hardware"
InputProps={{
...params.InputProps,
endAdornment: (
<Fragment>
{isLoading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</Fragment>
)
}}
/>
)}
/>
{serversLoading ?
<CircularProgress />
:
storages &&
<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={options}
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={(id: any) => {
console.log(id)
}}

View File

@ -4,13 +4,18 @@ import { IRegion } from '../interfaces/fuel'
import { useRegions, useServers, useServersInfo } from '../hooks/swrHooks'
import FullFeaturedCrudGrid from './TableEditable'
import ServerService from '../services/ServersService'
import { GridColDef } from '@mui/x-data-grid'
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 DataGridCellAutocomplete from './DataGridCellAutocomplete'
export default function ServersView() {
const [open, setOpen] = useState(false)
const [editOpen, setEditOpen] = useState(false)
const [options, setOptions] = useState<IRegion[]>([])
const [search, setSearch] = useState<string | null>("")
const [debouncedSearch, setDebouncedSearch] = useState<string | null>("")
@ -52,6 +57,23 @@ export default function ServersView() {
const serversColumns: GridColDef[] = [
//{ field: 'id', headerName: 'ID', type: "number" },
{ field: 'name', headerName: 'Название', type: "string", editable: true },
{
field: 'region_id',
editable: true,
renderEditCell: (params: GridRenderCellParams) => (
<DataGridCellAutocomplete
selectedOption={params.value}
setSelectedOption={(value: any) => {
params.value = value
}}
isLoading={isLoading}
setDebouncedSearch={setDebouncedSearch}
options={options}
setOptions={setOptions}
/>
),
width: 200
}
]
return (
@ -88,83 +110,24 @@ export default function ServersView() {
}
</Dialog>
<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={options}
loading={isLoading}
value={selectedOption}
renderInput={(params) => (
<TextField
{...params}
label="Район"
InputProps={{
...params.InputProps,
endAdornment: (
<Fragment>
{isLoading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</Fragment>
)
}}
/>
)}
/>
{servers &&
{serversInfo &&
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', height: '100%' }}>
{serversInfo &&
<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}>
<Paper sx={{ display: 'flex', flexDirection: 'column', gap: '16px', p: '16px' }}>
<Typography fontWeight={600}>
{serverInfo.name}
</Typography>
<Divider />
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography>
Количество IP:
</Typography>
<Typography variant="h6" fontWeight={600}>
{serverInfo.IPs_count}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography>
Количество серверов:
</Typography>
<Typography variant="h6" fontWeight={600}>
{serverInfo.servers_count}
</Typography>
</Box>
<Chip
icon={serverInfo.status === "Online" ? <Cloud /> : serverInfo.status === "Offline" ? <CloudOff /> : <CloudOff />}
variant="outlined"
label={serverInfo.status}
color={serverInfo.status === "Online" ? "success" : serverInfo.status === "Offline" ? "error" : "error"}
/>
</Paper>
</Grid>
))}
</Grid>
}
<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>
}
@ -173,6 +136,41 @@ export default function ServersView() {
:
servers &&
<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={options}
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={(id: any) => {
console.log(id)
}}

View File

@ -24,6 +24,7 @@ import {
useGridApiContext,
} from '@mui/x-data-grid';
import { AxiosResponse } from 'axios';
import { Autocomplete } from '@mui/material';
interface EditToolbarProps {
setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void;
@ -31,10 +32,11 @@ interface EditToolbarProps {
newModel: (oldModel: GridRowModesModel) => GridRowModesModel,
) => void;
columns: GridColDef[];
autoComplete?: React.ReactElement | null;
}
function EditToolbar(props: EditToolbarProps) {
const { setRows, setRowModesModel, columns } = props;
const { setRows, setRowModesModel, columns, autoComplete } = props;
const handleClick = () => {
const id = Date.now().toString(36)
@ -50,6 +52,12 @@ function EditToolbar(props: EditToolbarProps) {
} else {
newValues[column.field] = undefined
}
if (column.field === 'region_id') {
// column.valueGetter = (value: any) => {
// console.log(value)
// }
}
})
setRows((oldRows) => [...oldRows, { id, ...newValues, isNew: true }]);
@ -60,7 +68,13 @@ function EditToolbar(props: EditToolbarProps) {
};
return (
<GridToolbarContainer>
<GridToolbarContainer sx={{ px: '16px', py: '16px' }}>
{autoComplete &&
<Box sx={{ flexGrow: '1' }}>
{autoComplete}
</Box>
}
<Button color="primary" startIcon={<AddIcon />} onClick={handleClick}>
Добавить
</Button>
@ -75,6 +89,7 @@ interface DataGridProps {
onRowClick: GridEventListener<"rowClick">;
onSave: any;
onDelete: (data: any) => Promise<AxiosResponse<any, any>>;
autoComplete?: React.ReactElement | null;
}
export default function FullFeaturedCrudGrid({
@ -83,7 +98,8 @@ export default function FullFeaturedCrudGrid({
actions = false,
onRowClick,
onSave,
onDelete
onDelete,
autoComplete
}: DataGridProps) {
const [rows, setRows] = React.useState(initialRows);
const [rowModesModel, setRowModesModel] = React.useState<GridRowModesModel>({});
@ -206,7 +222,7 @@ export default function FullFeaturedCrudGrid({
toolbar: EditToolbar as GridSlots['toolbar'],
}}
slotProps={{
toolbar: { setRows, setRowModesModel, columns },
toolbar: { setRows, setRowModesModel, columns, autoComplete },
}}
/>
</Box>

View File

@ -189,8 +189,7 @@ export function useServers(region_id?: number, offset?: number, limit?: number)
region_id ? `/api/servers?region_id=${region_id}&offset=${offset || 0}&limit=${limit || 10}` : `/api/servers?offset=${offset || 0}&limit=${limit || 10}`,
(url: string) => fetcher(url, BASE_URL.servers),
{
revalidateOnFocus: false,
revalidateOnMount: false
revalidateOnFocus: false
}
)
@ -203,11 +202,10 @@ export function useServers(region_id?: number, offset?: number, limit?: number)
export function useServersInfo(region_id?: number, offset?: number, limit?: number) {
const { data, error, isLoading } = useSWR(
region_id ? `/api/servers_info?region_id=${region_id}&offset=${offset || 0}&limit=${limit || 10}` : null,
(url) => fetcher(url, BASE_URL.servers),
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),
{
revalidateOnFocus: false,
revalidateOnMount: false
}
)
@ -224,7 +222,6 @@ export function useServer(server_id?: number) {
(url) => fetcher(url, BASE_URL.servers),
{
revalidateOnFocus: false,
revalidateOnMount: false
}
)
@ -241,7 +238,6 @@ export function useServerIps(server_id?: number | null, offset?: number, limit?:
(url: string) => fetcher(url, BASE_URL.servers),
{
revalidateOnFocus: false,
revalidateOnMount: false
}
)
@ -260,7 +256,6 @@ export function useHardwares(server_id?: number, offset?: number, limit?: number
(url: string) => fetcher(url, BASE_URL.servers),
{
revalidateOnFocus: false,
revalidateOnMount: false
}
)
@ -296,7 +291,6 @@ export function useStorages(hardware_id?: number, offset?: number, limit?: numbe
(url: string) => fetcher(url, BASE_URL.servers),
{
revalidateOnFocus: false,
revalidateOnMount: false
}
)

View File

@ -11,13 +11,14 @@ import Divider from '@mui/material/Divider';
import IconButton from '@mui/material/IconButton';
import Container from '@mui/material/Container';
import MenuIcon from '@mui/icons-material/Menu';
import { Api, Assignment, Cloud, Factory, Home, People, Shield, Storage, } from '@mui/icons-material';
import { ListItem, ListItemButton, ListItemIcon, ListItemText, } from '@mui/material';
import { Api, Assignment, Cloud, Dashboard, Factory, Home, People, Shield, Storage, } from '@mui/icons-material';
import { colors, ListItem, ListItemButton, ListItemIcon, ListItemText, } from '@mui/material';
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';
const drawerWidth: number = 240;
@ -69,49 +70,6 @@ const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open'
}),
);
const pages = [
{
label: "Главная",
path: "/",
icon: <Home />
},
{
label: "Пользователи",
path: "/user",
icon: <People />
},
{
label: "Роли",
path: "/role",
icon: <Shield />
},
{
label: "Документы",
path: "/documents",
icon: <Storage />
},
{
label: "Отчеты",
path: "/reports",
icon: <Assignment />
},
{
label: "Серверы",
path: "/servers",
icon: <Cloud />
},
{
label: "Котельные",
path: "/boilers",
icon: <Factory />
},
{
label: "API Test",
path: "/api-test",
icon: <Api />
},
]
export default function DashboardLayout() {
const theme = useTheme()
const innerTheme = createTheme(theme)
@ -186,17 +144,12 @@ export default function DashboardLayout() {
<Typography variant="caption">{userData?.login}</Typography>
</Box>
{/* <IconButton color="inherit">
<Badge badgeContent={0} color="secondary">
<AccountCircle />
</Badge>
</IconButton> */}
<AccountMenu />
</Box>
</Toolbar>
</AppBar>
<Drawer variant="permanent" open={open}>
<Toolbar
sx={{
@ -214,7 +167,7 @@ export default function DashboardLayout() {
<Divider />
<List component="nav">
{pages.map((item, index) => (
{pages.filter((page) => page.drawer).map((item, index) => (
<ListItem
key={index}
disablePadding
@ -231,6 +184,7 @@ export default function DashboardLayout() {
</ListItemIcon>
<ListItemText
primary={item.label}
sx={{ color: location.pathname === item.path ? colors.blue[700] : innerTheme.palette.text.primary }}
/>
</ListItemButton>
</ListItem>
@ -253,10 +207,7 @@ export default function DashboardLayout() {
<Toolbar />
<Container
maxWidth="lg"
sx={{
mt: 4,
mb: 4
}}
sx={{ mt: 4, mb: 4 }}
>
<Outlet />
</Container>

View File

@ -1,268 +1,10 @@
import { AppBar, Autocomplete, Box, Chip, CircularProgress, Dialog, Divider, Grid, IconButton, Paper, TextField, Toolbar, Typography, colors } from "@mui/material"
import { useBoilers, useCities, useRegions, useServers, useServersInfo } from "../hooks/swrHooks"
import { Fragment, useEffect, useState } from "react"
import { IBoiler, ICity, IRegion } from "../interfaces/fuel"
import { DataGrid, GridColDef } from "@mui/x-data-grid"
import ServerData from "../components/ServerData"
import { IServer, IServersInfo } from "../interfaces/servers"
import { Close, Cloud, CloudOff, Storage } from "@mui/icons-material"
import { Box } from "@mui/material"
export default function ApiTest() {
const [open, setOpen] = useState(false)
const [options, setOptions] = useState<IRegion[]>([])
const [search, setSearch] = useState<string | null>(null)
const [debouncedSearch, setDebouncedSearch] = useState<string | null>("")
const [selectedOption, setSelectedOption] = useState<IRegion | null>(null)
const { regions, isLoading } = useRegions(10, 1, debouncedSearch)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedSearch(search)
}, 500)
return () => {
clearTimeout(handler)
}
}, [search])
useEffect(() => {
if (regions) {
setOptions([...regions])
}
}, [regions])
const handleInputChange = (value: string) => {
setSearch(value)
}
const handleOptionChange = (value: IRegion | null) => {
setSelectedOption(value)
}
const [citiesOpen, setCitiesOpen] = useState(false)
const [citiesPage, setCitiesPage] = useState(1)
const [citiesSearch, setCitiesSearch] = useState('')
const [debouncedCitySearch, setDebouncedCitySearch] = useState('')
const { cities, isLoading: citiesLoading } = useCities(10, citiesPage, debouncedCitySearch)
const [citiesOptions, setCitiesOptions] = useState<ICity[]>([])
const [selectedCityOption, setSelectedCityOption] = useState<ICity | null>(null)
const handleCityInputChange = (value: string) => {
setCitiesSearch(value)
}
const handleCityOptionChange = (value: ICity | null) => {
setSelectedCityOption(value)
}
useEffect(() => {
if (cities) {
setCitiesOptions([...cities])
}
}, [cities])
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedCitySearch(citiesSearch)
}, 500)
return () => {
clearTimeout(handler)
}
}, [citiesSearch])
useEffect(() => {
setCitiesPage(1)
setCitiesSearch("")
}, [])
const { servers, isLoading: serversLoading } = useServers(selectedOption?.id, 0, 10)
const serversColumns: GridColDef[] = [
{ field: 'id', headerName: 'ID', type: "number" },
{ field: 'name', headerName: 'Название', type: "string" },
]
const [serverDataOpen, setServerDataOpen] = useState(false)
const [currentServerData, setCurrentServerData] = useState<any | null>(null)
const { serversInfo } = useServersInfo(selectedOption?.id)
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', height: '100%' }}>
<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>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', height: '100%', p: '16px' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Storage />
<Typography variant='h6' fontWeight='600'>
Servers
</Typography>
</Box>
<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={options}
loading={isLoading}
value={selectedOption}
renderInput={(params) => (
<TextField
{...params}
label="Район"
InputProps={{
...params.InputProps,
endAdornment: (
<Fragment>
{isLoading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</Fragment>
)
}}
/>
)}
/>
<Autocomplete
open={citiesOpen}
onOpen={() => {
setCitiesOpen(true)
}}
onClose={() => {
setCitiesOpen(false)
}}
onInputChange={(_, value) => handleCityInputChange(value)}
onChange={(_, value) => handleCityOptionChange(value)}
filterOptions={(x) => x}
isOptionEqualToValue={(option: ICity, value: ICity) => option.name === value.name}
getOptionLabel={(option: ICity) => option.name ? option.name : ""}
options={citiesOptions}
loading={citiesLoading}
value={selectedCityOption}
renderInput={(params) => (
<TextField
{...params}
label="Город"
InputProps={{
...params.InputProps,
endAdornment: (
<Fragment>
{citiesLoading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</Fragment>
)
}}
/>
)}
/>
{servers &&
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', height: '100%' }}>
<Typography variant='h6' fontWeight='600'>
Информация
</Typography>
{serversInfo &&
<Grid container spacing={{ xs: 2, md: 3 }} columns={{ xs: 1, sm: 1, md: 2, lg: 3, xl: 4 }}>
{serversInfo.map((serverInfo: IServersInfo) => (
<Grid item xs={1} sm={1} md={1}>
<Paper sx={{ display: 'flex', flexDirection: 'column', gap: '16px', p: '16px' }}>
<Typography fontWeight={600}>
{serverInfo.name}
</Typography>
<Divider />
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography>
Количество IP:
</Typography>
<Typography variant="h6" fontWeight={600}>
{serverInfo.IPs_count}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography>
Количество серверов:
</Typography>
<Typography variant="h6" fontWeight={600}>
{serverInfo.servers_count}
</Typography>
</Box>
<Chip
icon={serverInfo.status === "Online" ? <Cloud /> : serverInfo.status === "Offline" ? <CloudOff /> : <CloudOff />}
variant="outlined"
label={serverInfo.status}
color={serverInfo.status === "Online" ? "success" : serverInfo.status === "Offline" ? "error" : "error"}
/>
</Paper>
</Grid>
))}
</Grid>
}
</Box>
}
{serversLoading ?
<CircularProgress />
:
servers &&
<DataGrid
rowSelection={false}
rows={servers}
columns={serversColumns}
onRowClick={(params, event, details) => {
setCurrentServerData(params.row)
setServerDataOpen(true)
}}
/>
}
</Box>
</Box>
)
}