forked from VinokurovVE/tests
DataGrid cell autocomplete
This commit is contained in:
@ -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>
|
||||
|
23
frontend_reactjs/src/components/CardInfo/CardInfo.tsx
Normal file
23
frontend_reactjs/src/components/CardInfo/CardInfo.tsx
Normal 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>
|
||||
)
|
||||
}
|
27
frontend_reactjs/src/components/CardInfo/CardInfoChip.tsx
Normal file
27
frontend_reactjs/src/components/CardInfo/CardInfoChip.tsx
Normal 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"}
|
||||
/>
|
||||
)
|
||||
}
|
24
frontend_reactjs/src/components/CardInfo/CardInfoLabel.tsx
Normal file
24
frontend_reactjs/src/components/CardInfo/CardInfoLabel.tsx
Normal 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>
|
||||
)
|
||||
}
|
79
frontend_reactjs/src/components/DataGridCellAutocomplete.tsx
Normal file
79
frontend_reactjs/src/components/DataGridCellAutocomplete.tsx
Normal 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>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
@ -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)
|
||||
}}
|
||||
|
@ -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)
|
||||
}}
|
||||
|
@ -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)
|
||||
}}
|
||||
|
@ -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)
|
||||
}}
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user