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 { Box, CircularProgress } from "@mui/material"
|
||||||
import Documents from "./pages/Documents"
|
import Documents from "./pages/Documents"
|
||||||
import Reports from "./pages/Reports"
|
import Reports from "./pages/Reports"
|
||||||
|
import Boilers from "./pages/Boilers"
|
||||||
|
import Servers from "./pages/Servers"
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
@ -52,6 +54,8 @@ function App() {
|
|||||||
<Route path="/role" element={<Roles />} />
|
<Route path="/role" element={<Roles />} />
|
||||||
<Route path="/documents" element={<Documents />} />
|
<Route path="/documents" element={<Documents />} />
|
||||||
<Route path="/reports" element={<Reports />} />
|
<Route path="/reports" element={<Reports />} />
|
||||||
|
<Route path="/servers" element={<Servers />} />
|
||||||
|
<Route path="/boilers" element={<Boilers />} />
|
||||||
<Route path="/api-test" element={<ApiTest />} />
|
<Route path="/api-test" element={<ApiTest />} />
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
@ -120,7 +120,7 @@ export default function AccountMenu() {
|
|||||||
}}>
|
}}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Android12Switch
|
<Android12Switch
|
||||||
defaultChecked={prefStore.darkMode}
|
checked={prefStore.darkMode}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setDarkMode(e.target.checked)
|
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) {
|
export function useServerIps(server_id?: number, offset?: number, limit?: number) {
|
||||||
const { data, error, isLoading } = useSWR(
|
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),
|
(url) => fetcher(url, BASE_URL.servers),
|
||||||
{
|
{
|
||||||
revalidateOnFocus: false
|
revalidateOnFocus: false
|
||||||
|
@ -11,7 +11,7 @@ import Divider from '@mui/material/Divider';
|
|||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import Container from '@mui/material/Container';
|
import Container from '@mui/material/Container';
|
||||||
import MenuIcon from '@mui/icons-material/Menu';
|
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 { ListItem, ListItemButton, ListItemIcon, ListItemText, } from '@mui/material';
|
||||||
import { Outlet, useNavigate } from 'react-router-dom';
|
import { Outlet, useNavigate } from 'react-router-dom';
|
||||||
import { UserData } from '../interfaces/auth';
|
import { UserData } from '../interfaces/auth';
|
||||||
@ -95,6 +95,16 @@ const pages = [
|
|||||||
path: "/reports",
|
path: "/reports",
|
||||||
icon: <Assignment />
|
icon: <Assignment />
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Серверы",
|
||||||
|
path: "/servers",
|
||||||
|
icon: <Cloud />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Котельные",
|
||||||
|
path: "/boilers",
|
||||||
|
icon: <Factory />
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "API Test",
|
label: "API Test",
|
||||||
path: "/api-test",
|
path: "/api-test",
|
||||||
@ -241,7 +251,13 @@ export default function DashboardLayout() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Toolbar />
|
<Toolbar />
|
||||||
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
|
<Container
|
||||||
|
maxWidth="lg"
|
||||||
|
sx={{
|
||||||
|
mt: 4,
|
||||||
|
mb: 4
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Container>
|
</Container>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import { Autocomplete, Box, CircularProgress, Paper, TextField, Typography } from "@mui/material"
|
import { AppBar, Autocomplete, Box, Chip, CircularProgress, Dialog, Divider, Grid, IconButton, Paper, TextField, Toolbar, Typography, colors } from "@mui/material"
|
||||||
import { useBoilers, useRegions, useServers } from "../hooks/swrHooks"
|
import { useBoilers, useCities, useRegions, useServers, useServersInfo } from "../hooks/swrHooks"
|
||||||
import { Fragment, useEffect, useState } from "react"
|
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 { 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() {
|
export default function ApiTest() {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
@ -36,27 +39,43 @@ export default function ApiTest() {
|
|||||||
setSelectedOption(value)
|
setSelectedOption(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const [boilersPage, setBoilersPage] = useState(1)
|
const [citiesOpen, setCitiesOpen] = useState(false)
|
||||||
const [boilerSearch, setBoilerSearch] = useState("")
|
const [citiesPage, setCitiesPage] = useState(1)
|
||||||
const [debouncedBoilerSearch, setDebouncedBoilerSearch] = useState("")
|
const [citiesSearch, setCitiesSearch] = useState('')
|
||||||
const { boilers } = useBoilers(10, boilersPage, debouncedBoilerSearch)
|
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(() => {
|
useEffect(() => {
|
||||||
const handler = setTimeout(() => {
|
const handler = setTimeout(() => {
|
||||||
setDebouncedBoilerSearch(boilerSearch)
|
setDebouncedCitySearch(citiesSearch)
|
||||||
}, 500)
|
}, 500)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearTimeout(handler)
|
clearTimeout(handler)
|
||||||
}
|
}
|
||||||
}, [boilerSearch])
|
}, [citiesSearch])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBoilersPage(1)
|
setCitiesPage(1)
|
||||||
setBoilerSearch("")
|
setCitiesSearch("")
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
//const { cities } = useCities(10, 1)
|
|
||||||
const { servers, isLoading: serversLoading } = useServers(selectedOption?.id, 0, 10)
|
const { servers, isLoading: serversLoading } = useServers(selectedOption?.id, 0, 10)
|
||||||
|
|
||||||
const serversColumns: GridColDef[] = [
|
const serversColumns: GridColDef[] = [
|
||||||
@ -64,21 +83,53 @@ export default function ApiTest() {
|
|||||||
{ field: 'name', headerName: 'Название', type: "string" },
|
{ field: 'name', headerName: 'Название', type: "string" },
|
||||||
]
|
]
|
||||||
|
|
||||||
const boilersColumns: GridColDef[] = [
|
const [serverDataOpen, setServerDataOpen] = useState(false)
|
||||||
{ field: 'id', headerName: 'ID', type: "number" },
|
const [currentServerData, setCurrentServerData] = useState<any | null>(null)
|
||||||
{ field: 'boiler_name', headerName: 'Название', type: "string" },
|
|
||||||
{ field: 'boiler_code', headerName: 'Код', type: "string" },
|
const { serversInfo } = useServersInfo(selectedOption?.id)
|
||||||
{ field: 'id_city', headerName: 'Город', type: "string" },
|
|
||||||
{ field: 'activity', headerName: 'Активен', type: "boolean" },
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '16px', height: '100%' }}>
|
<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', flexDirection: 'column', gap: '16px', height: '100%', p: '16px' }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
|
<Storage />
|
||||||
<Typography variant='h6' fontWeight='600'>
|
<Typography variant='h6' fontWeight='600'>
|
||||||
Servers
|
Servers
|
||||||
</Typography>
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
open={open}
|
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 ?
|
{serversLoading ?
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
:
|
:
|
||||||
@ -121,28 +256,13 @@ export default function ApiTest() {
|
|||||||
rowSelection={false}
|
rowSelection={false}
|
||||||
rows={servers}
|
rows={servers}
|
||||||
columns={serversColumns}
|
columns={serversColumns}
|
||||||
|
onRowClick={(params, event, details) => {
|
||||||
|
setCurrentServerData(params.row)
|
||||||
|
setServerDataOpen(true)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</Box>
|
</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>
|
</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