forked from VinokurovVE/tests
Tables, cards, (servers)
This commit is contained in:
@ -13,6 +13,8 @@ import { useEffect, useState } from "react"
|
||||
import { Box, CircularProgress } from "@mui/material"
|
||||
import Documents from "./pages/Documents"
|
||||
import Reports from "./pages/Reports"
|
||||
import Boilers from "./pages/Boilers"
|
||||
import Servers from "./pages/Servers"
|
||||
|
||||
function App() {
|
||||
const auth = useAuthStore()
|
||||
@ -52,6 +54,8 @@ function App() {
|
||||
<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 />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Route>
|
||||
|
@ -120,7 +120,7 @@ export default function AccountMenu() {
|
||||
}}>
|
||||
<ListItemIcon>
|
||||
<Android12Switch
|
||||
defaultChecked={prefStore.darkMode}
|
||||
checked={prefStore.darkMode}
|
||||
onChange={(e) => {
|
||||
setDarkMode(e.target.checked)
|
||||
}} />
|
||||
|
17
frontend_reactjs/src/components/ServerData.tsx
Normal file
17
frontend_reactjs/src/components/ServerData.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { Box } from '@mui/material'
|
||||
import { IServer } from '../interfaces/servers'
|
||||
import { useServerIps } from '../hooks/swrHooks'
|
||||
|
||||
function ServerData({ id, name, region_id }: IServer) {
|
||||
const { serverIps } = useServerIps(id, 0, 10)
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{serverIps &&
|
||||
JSON.stringify(serverIps)
|
||||
}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServerData
|
227
frontend_reactjs/src/components/TableEditable.tsx
Normal file
227
frontend_reactjs/src/components/TableEditable.tsx
Normal file
@ -0,0 +1,227 @@
|
||||
import * as React 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,
|
||||
GridRowProps,
|
||||
GridRow,
|
||||
useGridApiContext,
|
||||
} from '@mui/x-data-grid';
|
||||
|
||||
interface EditToolbarProps {
|
||||
setRows: (newRows: (oldRows: GridRowsProp) => GridRowsProp) => void;
|
||||
setRowModesModel: (
|
||||
newModel: (oldModel: GridRowModesModel) => GridRowModesModel,
|
||||
) => void;
|
||||
columns: GridColDef[];
|
||||
}
|
||||
|
||||
function EditToolbar(props: EditToolbarProps) {
|
||||
const { setRows, setRowModesModel, columns } = 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
|
||||
}
|
||||
})
|
||||
|
||||
setRows((oldRows) => [...oldRows, { id, ...newValues, isNew: true }]);
|
||||
setRowModesModel((oldModel) => ({
|
||||
...oldModel,
|
||||
[id]: { mode: GridRowModes.Edit, fieldToFocus: columns[0].field },
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<GridToolbarContainer>
|
||||
<Button color="primary" startIcon={<AddIcon />} onClick={handleClick}>
|
||||
Добавить
|
||||
</Button>
|
||||
</GridToolbarContainer>
|
||||
);
|
||||
}
|
||||
|
||||
interface DataGridProps {
|
||||
initialRows: GridRowsProp;
|
||||
columns: GridColDef[];
|
||||
actions: boolean;
|
||||
onRowClick: GridEventListener<"rowClick">
|
||||
}
|
||||
|
||||
export default function FullFeaturedCrudGrid({
|
||||
initialRows,
|
||||
columns,
|
||||
actions = false,
|
||||
onRowClick
|
||||
}: DataGridProps) {
|
||||
const [rows, setRows] = React.useState(initialRows);
|
||||
const [rowModesModel, setRowModesModel] = React.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 } });
|
||||
};
|
||||
|
||||
const handleDeleteClick = (id: GridRowId) => () => {
|
||||
setRows(rows.filter((row) => row.id !== 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);
|
||||
};
|
||||
|
||||
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
|
||||
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'],
|
||||
row: CustomRow as GridSlots['row']
|
||||
}}
|
||||
slotProps={{
|
||||
toolbar: { setRows, setRowModesModel, columns },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function CustomRow(props: GridRowProps) {
|
||||
const { id, row, rowId, ...other } = props;
|
||||
const apiRef = useGridApiContext();
|
||||
|
||||
// Custom styles or logic can go here
|
||||
|
||||
return (
|
||||
<Box
|
||||
component="div"
|
||||
{...other}
|
||||
>
|
||||
{Object.entries(row).map(([field, value]) => (
|
||||
<Box key={field} sx={{ flex: 1, padding: '0 8px' }}>
|
||||
{apiRef.current.getCellParams(rowId, field).formattedValue as React.ReactNode}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
@ -234,7 +234,7 @@ export function useServer(server_id?: number) {
|
||||
|
||||
export function useServerIps(server_id?: number, offset?: number, limit?: number) {
|
||||
const { data, error, isLoading } = useSWR(
|
||||
server_id ? `/api/servers?server_id=${server_id}&offset=${offset || 0}&limit=${limit || 10}` : null,
|
||||
server_id ? `/api/server_ips?server_id=${server_id}&offset=${offset || 0}&limit=${limit || 10}` : null,
|
||||
(url) => fetcher(url, BASE_URL.servers),
|
||||
{
|
||||
revalidateOnFocus: false
|
||||
|
@ -11,7 +11,7 @@ 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, Home, People, Shield, Storage, } from '@mui/icons-material';
|
||||
import { Api, Assignment, Cloud, Factory, Home, People, Shield, Storage, } from '@mui/icons-material';
|
||||
import { ListItem, ListItemButton, ListItemIcon, ListItemText, } from '@mui/material';
|
||||
import { Outlet, useNavigate } from 'react-router-dom';
|
||||
import { UserData } from '../interfaces/auth';
|
||||
@ -95,6 +95,16 @@ const pages = [
|
||||
path: "/reports",
|
||||
icon: <Assignment />
|
||||
},
|
||||
{
|
||||
label: "Серверы",
|
||||
path: "/servers",
|
||||
icon: <Cloud />
|
||||
},
|
||||
{
|
||||
label: "Котельные",
|
||||
path: "/boilers",
|
||||
icon: <Factory />
|
||||
},
|
||||
{
|
||||
label: "API Test",
|
||||
path: "/api-test",
|
||||
@ -241,7 +251,13 @@ export default function DashboardLayout() {
|
||||
}}
|
||||
>
|
||||
<Toolbar />
|
||||
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
|
||||
<Container
|
||||
maxWidth="lg"
|
||||
sx={{
|
||||
mt: 4,
|
||||
mb: 4
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
</Container>
|
||||
</Box>
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { Autocomplete, Box, CircularProgress, Paper, TextField, Typography } from "@mui/material"
|
||||
import { useBoilers, useRegions, useServers } from "../hooks/swrHooks"
|
||||
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, IRegion } from "../interfaces/fuel"
|
||||
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"
|
||||
|
||||
export default function ApiTest() {
|
||||
const [open, setOpen] = useState(false)
|
||||
@ -36,27 +39,43 @@ export default function ApiTest() {
|
||||
setSelectedOption(value)
|
||||
}
|
||||
|
||||
const [boilersPage, setBoilersPage] = useState(1)
|
||||
const [boilerSearch, setBoilerSearch] = useState("")
|
||||
const [debouncedBoilerSearch, setDebouncedBoilerSearch] = useState("")
|
||||
const { boilers } = useBoilers(10, boilersPage, debouncedBoilerSearch)
|
||||
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(() => {
|
||||
setDebouncedBoilerSearch(boilerSearch)
|
||||
setDebouncedCitySearch(citiesSearch)
|
||||
}, 500)
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler)
|
||||
}
|
||||
}, [boilerSearch])
|
||||
}, [citiesSearch])
|
||||
|
||||
useEffect(() => {
|
||||
setBoilersPage(1)
|
||||
setBoilerSearch("")
|
||||
setCitiesPage(1)
|
||||
setCitiesSearch("")
|
||||
}, [])
|
||||
|
||||
//const { cities } = useCities(10, 1)
|
||||
const { servers, isLoading: serversLoading } = useServers(selectedOption?.id, 0, 10)
|
||||
|
||||
const serversColumns: GridColDef[] = [
|
||||
@ -64,21 +83,53 @@ export default function ApiTest() {
|
||||
{ field: 'name', headerName: 'Название', type: "string" },
|
||||
]
|
||||
|
||||
const boilersColumns: GridColDef[] = [
|
||||
{ field: 'id', headerName: 'ID', type: "number" },
|
||||
{ field: 'boiler_name', headerName: 'Название', type: "string" },
|
||||
{ field: 'boiler_code', headerName: 'Код', type: "string" },
|
||||
{ field: 'id_city', headerName: 'Город', type: "string" },
|
||||
{ field: 'activity', headerName: 'Активен', type: "boolean" },
|
||||
]
|
||||
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%' }}>
|
||||
<Paper elevation={1}>
|
||||
<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}
|
||||
@ -113,6 +164,90 @@ export default function ApiTest() {
|
||||
)}
|
||||
/>
|
||||
|
||||
<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 />
|
||||
:
|
||||
@ -121,28 +256,13 @@ export default function ApiTest() {
|
||||
rowSelection={false}
|
||||
rows={servers}
|
||||
columns={serversColumns}
|
||||
onRowClick={(params, event, details) => {
|
||||
setCurrentServerData(params.row)
|
||||
setServerDataOpen(true)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<Paper elevation={1}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', height: '100%', p: '16px' }}>
|
||||
<Typography variant='h6' fontWeight='600'>
|
||||
Boilers
|
||||
</Typography>
|
||||
|
||||
{boilers &&
|
||||
<DataGrid
|
||||
rows={boilers.map((boiler: IBoiler) => {
|
||||
return { ...boiler, id: boiler.id_object }
|
||||
})}
|
||||
columns={boilersColumns}
|
||||
/>
|
||||
}
|
||||
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
)
|
||||
}
|
57
frontend_reactjs/src/pages/Boilers.tsx
Normal file
57
frontend_reactjs/src/pages/Boilers.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { Box, Typography } from '@mui/material'
|
||||
import { DataGrid, GridColDef } from '@mui/x-data-grid'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { IBoiler } from '../interfaces/fuel'
|
||||
import { useBoilers } from '../hooks/swrHooks'
|
||||
|
||||
function Boilers() {
|
||||
const [boilersPage, setBoilersPage] = useState(1)
|
||||
const [boilerSearch, setBoilerSearch] = useState("")
|
||||
const [debouncedBoilerSearch, setDebouncedBoilerSearch] = useState("")
|
||||
const { boilers } = useBoilers(10, boilersPage, debouncedBoilerSearch)
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedBoilerSearch(boilerSearch)
|
||||
}, 500)
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler)
|
||||
}
|
||||
}, [boilerSearch])
|
||||
|
||||
useEffect(() => {
|
||||
setBoilersPage(1)
|
||||
setBoilerSearch("")
|
||||
}, [])
|
||||
|
||||
const boilersColumns: GridColDef[] = [
|
||||
{ field: 'id', headerName: 'ID', type: "number" },
|
||||
{ field: 'boiler_name', headerName: 'Название', type: "string" },
|
||||
{ field: 'boiler_code', headerName: 'Код', type: "string" },
|
||||
{ field: 'id_city', headerName: 'Город', type: "string" },
|
||||
{ field: 'activity', headerName: 'Активен', type: "boolean" },
|
||||
]
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', height: '100%' }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', height: '100%', p: '16px' }}>
|
||||
<Typography variant='h6' fontWeight='600'>
|
||||
Котельные
|
||||
</Typography>
|
||||
|
||||
{boilers &&
|
||||
<DataGrid
|
||||
rows={boilers.map((boiler: IBoiler) => {
|
||||
return { ...boiler, id: boiler.id_object }
|
||||
})}
|
||||
columns={boilersColumns}
|
||||
/>
|
||||
}
|
||||
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default Boilers
|
275
frontend_reactjs/src/pages/Servers.tsx
Normal file
275
frontend_reactjs/src/pages/Servers.tsx
Normal file
@ -0,0 +1,275 @@
|
||||
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 { BarChart } from "@mui/x-charts"
|
||||
import FullFeaturedCrudGrid from "../components/TableEditable"
|
||||
|
||||
export default function Servers() {
|
||||
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", editable: true },
|
||||
]
|
||||
|
||||
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' }}>
|
||||
<Typography variant='h6' fontWeight='600'>
|
||||
Servers by region
|
||||
</Typography>
|
||||
|
||||
<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
|
||||
hidden
|
||||
|
||||
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 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>
|
||||
}
|
||||
</Box>
|
||||
}
|
||||
|
||||
{serversLoading ?
|
||||
<CircularProgress />
|
||||
:
|
||||
servers &&
|
||||
<FullFeaturedCrudGrid
|
||||
initialRows={servers}
|
||||
columns={serversColumns}
|
||||
actions
|
||||
onRowClick={(params, event, details) => {
|
||||
setCurrentServerData(params.row)
|
||||
setServerDataOpen(true)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
{/* <BarChart
|
||||
xAxis={[{ scaleType: 'band', data: ['group A', 'group B', 'group C'] }]}
|
||||
series={[{ data: [4, 3, 5] }, { data: [1, 6, 3] }, { data: [2, 5, 6] }]}
|
||||
width={500}
|
||||
height={300}
|
||||
/> */}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user