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 (
+
+ } onClick={handleClick}>
+ Добавить
+
+
+ );
+}
+
+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 (
-
-
+
+
+
+
+
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 (
+
+
+
+
+
+ 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}
+
+ )
+ }}
+ />
+ )}
+ />
+
+ {
+ 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)
+ }}
+ />
+ }
+
+ {/* */}
+
+
+ )
+}
\ No newline at end of file