diff --git a/frontend_reactjs/src/App.tsx b/frontend_reactjs/src/App.tsx index e2df4d0..9c4d1b3 100644 --- a/frontend_reactjs/src/App.tsx +++ b/frontend_reactjs/src/App.tsx @@ -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() { } /> } /> } /> + } /> + } /> } /> } /> diff --git a/frontend_reactjs/src/components/AccountMenu.tsx b/frontend_reactjs/src/components/AccountMenu.tsx index 04b0444..10bddaf 100644 --- a/frontend_reactjs/src/components/AccountMenu.tsx +++ b/frontend_reactjs/src/components/AccountMenu.tsx @@ -120,7 +120,7 @@ export default function AccountMenu() { }}> { setDarkMode(e.target.checked) }} /> diff --git a/frontend_reactjs/src/components/ServerData.tsx b/frontend_reactjs/src/components/ServerData.tsx new file mode 100644 index 0000000..3aaf26a --- /dev/null +++ b/frontend_reactjs/src/components/ServerData.tsx @@ -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 ( + + {serverIps && + JSON.stringify(serverIps) + } + + ) +} + +export default ServerData \ No newline at end of file diff --git a/frontend_reactjs/src/components/TableEditable.tsx b/frontend_reactjs/src/components/TableEditable.tsx new file mode 100644 index 0000000..637f6ec --- /dev/null +++ b/frontend_reactjs/src/components/TableEditable.tsx @@ -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 ( + + + + ); +} + +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({}); + + 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 [ + } + label="Save" + sx={{ + color: 'primary.main', + }} + onClick={handleSaveClick(id)} + />, + } + label="Cancel" + className="textPrimary" + onClick={handleCancelClick(id)} + color="inherit" + />, + ]; + } + + return [ + } + label="Edit" + className="textPrimary" + onClick={handleEditClick(id)} + color="inherit" + />, + } + label="Delete" + onClick={handleDeleteClick(id)} + color="inherit" + />, + ]; + }, + } + ] + + return ( + + + + ); +} + +function CustomRow(props: GridRowProps) { + const { id, row, rowId, ...other } = props; + const apiRef = useGridApiContext(); + + // Custom styles or logic can go here + + return ( + + {Object.entries(row).map(([field, value]) => ( + + {apiRef.current.getCellParams(rowId, field).formattedValue as React.ReactNode} + + ))} + + ); +} \ No newline at end of file diff --git a/frontend_reactjs/src/hooks/swrHooks.ts b/frontend_reactjs/src/hooks/swrHooks.ts index d317639..2524b58 100644 --- a/frontend_reactjs/src/hooks/swrHooks.ts +++ b/frontend_reactjs/src/hooks/swrHooks.ts @@ -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 diff --git a/frontend_reactjs/src/layouts/DashboardLayout.tsx b/frontend_reactjs/src/layouts/DashboardLayout.tsx index 0680b41..3fb3472 100644 --- a/frontend_reactjs/src/layouts/DashboardLayout.tsx +++ b/frontend_reactjs/src/layouts/DashboardLayout.tsx @@ -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: }, + { + label: "Серверы", + path: "/servers", + icon: + }, + { + label: "Котельные", + path: "/boilers", + icon: + }, { label: "API Test", path: "/api-test", @@ -241,7 +251,13 @@ export default function DashboardLayout() { }} > - + diff --git a/frontend_reactjs/src/pages/ApiTest.tsx b/frontend_reactjs/src/pages/ApiTest.tsx index a02d780..7d0388c 100644 --- a/frontend_reactjs/src/pages/ApiTest.tsx +++ b/frontend_reactjs/src/pages/ApiTest.tsx @@ -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([]) + const [selectedCityOption, setSelectedCityOption] = useState(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,85 +83,186 @@ 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(null) + + const { serversInfo } = useServersInfo(selectedOption?.id) return ( - - + { + setServerDataOpen(false) + }} + aria-labelledby="modal-modal-title" + aria-describedby="modal-modal-description"> + + + { + setServerDataOpen(false) + }} + aria-label="close" + > + + + + + + {currentServerData && + + } + + + + + Servers + - { - setOpen(true) + + { + 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) => ( + + {isLoading ? : null} + {params.InputProps.endAdornment} + + ) + }} + /> + )} + /> + + { + 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) => ( + + {citiesLoading ? : null} + {params.InputProps.endAdornment} + + ) + }} + /> + )} + /> + + {servers && + + + Информация + + + {serversInfo && + + {serversInfo.map((serverInfo: IServersInfo) => ( + + + + {serverInfo.name} + + + + + + + Количество IP: + + + + {serverInfo.IPs_count} + + + + + + Количество серверов: + + + + {serverInfo.servers_count} + + + + : serverInfo.status === "Offline" ? : } + variant="outlined" + label={serverInfo.status} + color={serverInfo.status === "Online" ? "success" : serverInfo.status === "Offline" ? "error" : "error"} + /> + + + ))} + + } + + } + + {serversLoading ? + + : + servers && + { + setCurrentServerData(params.row) + setServerDataOpen(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) => ( - - {isLoading ? : null} - {params.InputProps.endAdornment} - - ) - }} - /> - )} /> - - {serversLoading ? - - : - servers && - - } - - - - - - - Boilers - - - {boilers && - { - return { ...boiler, id: boiler.id_object } - })} - columns={boilersColumns} - /> - } - - - + } + ) } \ No newline at end of file diff --git a/frontend_reactjs/src/pages/Boilers.tsx b/frontend_reactjs/src/pages/Boilers.tsx new file mode 100644 index 0000000..5711522 --- /dev/null +++ b/frontend_reactjs/src/pages/Boilers.tsx @@ -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 ( + + + + Котельные + + + {boilers && + { + return { ...boiler, id: boiler.id_object } + })} + columns={boilersColumns} + /> + } + + + + ) +} + +export default Boilers \ No newline at end of file diff --git a/frontend_reactjs/src/pages/Servers.tsx b/frontend_reactjs/src/pages/Servers.tsx new file mode 100644 index 0000000..1cff584 --- /dev/null +++ b/frontend_reactjs/src/pages/Servers.tsx @@ -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([]) + const [search, setSearch] = useState(null) + const [debouncedSearch, setDebouncedSearch] = useState("") + const [selectedOption, setSelectedOption] = useState(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([]) + const [selectedCityOption, setSelectedCityOption] = useState(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(null) + + const { serversInfo } = useServersInfo(selectedOption?.id) + + return ( + + { + setServerDataOpen(false) + }} + aria-labelledby="modal-modal-title" + aria-describedby="modal-modal-description"> + + + { + setServerDataOpen(false) + }} + aria-label="close" + > + + + + + + {currentServerData && + + } + + + + + Servers by region + + + { + 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) => ( + + {isLoading ? : null} + {params.InputProps.endAdornment} + + ) + }} + /> + )} + /> + + + + ) +} \ No newline at end of file