From fd278d3a9748759275034eb64276328db3ef1cb0 Mon Sep 17 00:00:00 2001 From: Caio Cabral Date: Wed, 27 Nov 2024 17:38:39 -0500 Subject: [PATCH] feat: abstracting table pagination --- .../infrastructureMonitorsSlice.js | 14 +- .../UptimeMonitors/uptimeMonitorsSlice.js | 1 + .../TablePagination/Actions/index.jsx | 80 ++++++ .../components/TablePagination/index.jsx | 117 ++++++++ Client/src/Pages/Infrastructure/index.jsx | 258 ++++++++---------- .../Monitors/Home/MonitorTable/index.jsx | 203 +++----------- Client/src/Pages/Monitors/Home/index.jsx | 2 + Client/src/Pages/Monitors/utils.jsx | 4 +- 8 files changed, 350 insertions(+), 329 deletions(-) create mode 100644 Client/src/Pages/Infrastructure/components/TablePagination/Actions/index.jsx create mode 100644 Client/src/Pages/Infrastructure/components/TablePagination/index.jsx diff --git a/Client/src/Features/InfrastructureMonitors/infrastructureMonitorsSlice.js b/Client/src/Features/InfrastructureMonitors/infrastructureMonitorsSlice.js index b593edff3..7e0739cbd 100644 --- a/Client/src/Features/InfrastructureMonitors/infrastructureMonitorsSlice.js +++ b/Client/src/Features/InfrastructureMonitors/infrastructureMonitorsSlice.js @@ -40,7 +40,7 @@ export const checkInfrastructureEndpointResolution = createAsyncThunk( const res = await networkService.checkEndpointResolution({ authToken: authToken, monitorURL: monitorURL, - }) + }); return res.data; } catch (error) { if (error.response && error.response.data) { @@ -53,7 +53,7 @@ export const checkInfrastructureEndpointResolution = createAsyncThunk( return thunkApi.rejectWithValue(payload); } } -) +); export const getInfrastructureMonitorById = createAsyncThunk( "infrastructureMonitors/getMonitorById", @@ -87,6 +87,7 @@ export const getInfrastructureMonitorsByTeamId = createAsyncThunk( authToken: token, teamId: user.teamId, types: ["hardware"], + limit: 1, }); return res.data; } catch (error) { @@ -112,7 +113,7 @@ export const updateInfrastructureMonitor = createAsyncThunk( description: monitor.description, interval: monitor.interval, notifications: monitor.notifications, - threshold: monitor.threshold + threshold: monitor.threshold, }; const res = await networkService.updateMonitor({ authToken: authToken, @@ -307,7 +308,9 @@ const infrastructureMonitorsSlice = createSlice({ .addCase(getInfrastructureMonitorById.rejected, (state, action) => { state.isLoading = false; state.success = false; - state.msg = action.payload ? action.payload.msg : "Failed to get infrastructure monitor"; + state.msg = action.payload + ? action.payload.msg + : "Failed to get infrastructure monitor"; }) // ***************************************************** // update Monitor @@ -401,6 +404,7 @@ const infrastructureMonitorsSlice = createSlice({ }, }); -export const { setInfrastructureMonitors, clearInfrastructureMonitorState } = infrastructureMonitorsSlice.actions; +export const { setInfrastructureMonitors, clearInfrastructureMonitorState } = + infrastructureMonitorsSlice.actions; export default infrastructureMonitorsSlice.reducer; diff --git a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js index d472a673a..00ef87bc0 100644 --- a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js +++ b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js @@ -221,6 +221,7 @@ export const addDemoMonitors = createAsyncThunk( } } ); + export const deleteAllMonitors = createAsyncThunk( "monitors/deleteAllMonitors", async (data, thunkApi) => { diff --git a/Client/src/Pages/Infrastructure/components/TablePagination/Actions/index.jsx b/Client/src/Pages/Infrastructure/components/TablePagination/Actions/index.jsx new file mode 100644 index 000000000..7e267c486 --- /dev/null +++ b/Client/src/Pages/Infrastructure/components/TablePagination/Actions/index.jsx @@ -0,0 +1,80 @@ +import PropTypes from "prop-types"; +import { Box, Button } from "@mui/material"; +import LeftArrowDouble from "../../../../../Assets/icons/left-arrow-double.svg?react"; +import RightArrowDouble from "../../../../../Assets/icons/right-arrow-double.svg?react"; +import LeftArrow from "../../../../../Assets/icons/left-arrow.svg?react"; +import RightArrow from "../../../../../Assets/icons/right-arrow.svg?react"; + +TablePaginationActions.propTypes = { + count: PropTypes.number.isRequired, + page: PropTypes.number.isRequired, + rowsPerPage: PropTypes.number.isRequired, + onPageChange: PropTypes.func.isRequired, +}; + +/** + * Component for pagination actions (first, previous, next, last). + * + * @component + * @param {Object} props + * @param {number} props.count - Total number of items. + * @param {number} props.page - Current page number. + * @param {number} props.rowsPerPage - Number of rows per page. + * @param {function} props.onPageChange - Callback function to handle page change. + * + * @returns {JSX.Element} Pagination actions component. + */ + +function TablePaginationActions({ count, page, rowsPerPage, onPageChange }) { + const handleFirstPageButtonClick = (event) => { + onPageChange(event, 0); + }; + const handleBackButtonClick = (event) => { + onPageChange(event, page - 1); + }; + const handleNextButtonClick = (event) => { + onPageChange(event, page + 1); + }; + const handleLastPageButtonClick = (event) => { + onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1)); + }; + + return ( + + + + + + + ); +} + +export { TablePaginationActions }; diff --git a/Client/src/Pages/Infrastructure/components/TablePagination/index.jsx b/Client/src/Pages/Infrastructure/components/TablePagination/index.jsx new file mode 100644 index 000000000..8a935fec3 --- /dev/null +++ b/Client/src/Pages/Infrastructure/components/TablePagination/index.jsx @@ -0,0 +1,117 @@ +import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; +import { Stack, TablePagination, Typography } from "@mui/material"; +import { TablePaginationActions } from "./Actions"; +import SelectorVertical from "../../../../Assets/icons/selector-vertical.svg?react"; + +Pagination.propTypes = { + monitorCount: PropTypes.number.isRequired, // Total number of items for pagination. + page: PropTypes.number.isRequired, // Current page index. + rowsPerPage: PropTypes.number.isRequired, // Number of rows displayed per page. + handleChangePage: PropTypes.func.isRequired, // Function to handle page changes. + handleChangeRowsPerPage: PropTypes.func.isRequired, // Function to handle changes in rows per page. +}; + +const ROWS_PER_PAGE_OPTIONS = [5, 10, 15, 25]; + +/** + * Pagination component for table navigation with customized styling and behavior. + * + * @param {object} props - Component properties. + * @param {number} props.monitorCount - Total number of monitors to paginate. + * @param {number} props.page - Current page index (0-based). + * @param {number} props.rowsPerPage - Number of rows to display per page. + * @param {function} props.handleChangePage - Callback for handling page changes. + * @param {function} props.handleChangeRowsPerPage - Callback for handling changes to rows per page. + * @returns {JSX.Element} The Pagination component. + */ +function Pagination({ + monitorCount, + page, + rowsPerPage, + handleChangePage, + handleChangeRowsPerPage, +}) { + const theme = useTheme(); + + const start = page * rowsPerPage + 1; + const end = Math.min(page * rowsPerPage + rowsPerPage, monitorCount); + const range = `${start} - ${end}`; + + return ( + + + Showing {range} of {monitorCount} monitor(s) + + + `Page ${page + 1} of ${Math.max(0, Math.ceil(count / rowsPerPage))}` + } + slotProps={{ + select: { + MenuProps: { + keepMounted: true, + disableScrollLock: true, + PaperProps: { + className: "pagination-dropdown", + sx: { + mt: 0, + mb: theme.spacing(2), + }, + }, + transformOrigin: { vertical: "bottom", horizontal: "left" }, + anchorOrigin: { vertical: "top", horizontal: "left" }, + sx: { + mt: theme.spacing(-2), + }, + }, + inputProps: { id: "pagination-dropdown" }, + IconComponent: SelectorVertical, + sx: { + ml: theme.spacing(4), + mr: theme.spacing(12), + minWidth: theme.spacing(20), + textAlign: "left", + "&.Mui-focused > div": { + backgroundColor: theme.palette.background.main, + }, + }, + }, + }} + sx={{ + color: theme.palette.text.secondary, + "& svg path": { + stroke: theme.palette.text.tertiary, + strokeWidth: 1.3, + }, + "& .MuiSelect-select": { + border: 1, + borderColor: theme.palette.border.light, + borderRadius: theme.shape.borderRadius, + }, + }} + /> + + ); +} + +export { Pagination }; diff --git a/Client/src/Pages/Infrastructure/index.jsx b/Client/src/Pages/Infrastructure/index.jsx index 9735bf6b2..2d45fdb74 100644 --- a/Client/src/Pages/Infrastructure/index.jsx +++ b/Client/src/Pages/Infrastructure/index.jsx @@ -13,27 +13,22 @@ import { // TablePagination, // Typography, } from "@mui/material"; -import { Heading } from "../../Components/Heading"; - +import { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { useDispatch, useSelector } from "react-redux"; import { useTheme } from "@emotion/react"; import Greeting from "../../Utils/greeting"; -import Breadcrumbs from "../../Components/Breadcrumbs"; -import { StatusLabel } from "../../Components/Label"; -import { Gauge } from "../../Components/Charts/Gauge"; import GearIcon from "../../Assets/icons/settings-bold.svg?react"; import CPUChipIcon from "../../Assets/icons/cpu-chip.svg?react"; +import Breadcrumbs from "../../Components/Breadcrumbs"; +import { StatusLabel } from "../../Components/Label"; +import { Heading } from "../../Components/Heading"; +import { Gauge } from "../../Components/Charts/Gauge"; +import { getInfrastructureMonitorsByTeamId } from "../../Features/InfrastructureMonitors/infrastructureMonitorsSlice"; +import useUtils from "../Monitors/utils"; +import { Pagination } from "./components/TablePagination"; -const mockedData = { - ip: "https://192.168.1.30", - status: "up", - processor: "2Ghz", - cpu: 80, - mem: 50, - disk: 70, -}; - -const ROWS = Array.from(Array(20).keys()).map(() => mockedData); -console.log(ROWS); +// const ROWS = Array.from(Array(20).keys()).map(() => mockedData); const columns = [ { label: "Host" }, @@ -69,8 +64,6 @@ Apply to Monitor Table, and Account/Team. Analyze existing BasicTable */ -import { useNavigate } from "react-router-dom"; - /** * This is the Infrastructure monitoring page. This is a work in progress * @@ -78,9 +71,33 @@ import { useNavigate } from "react-router-dom"; * @returns {JSX.Element} The infrastructure monitoring page. */ -function Infrastructure() { +function Infrastructure(/* {isAdmin} */) { const theme = useTheme(); + const { determineState } = useUtils(); const navigate = useNavigate(); + const dispatch = useDispatch(); + const { authToken } = useSelector((state) => state.auth); + const { isLoading, monitorsSummary, msg, success, ...rest } = useSelector( + (state) => state.infrastructureMonitors + ); + + const { monitorCounts = {}, monitors = [] } = monitorsSummary; + const { total: totalMonitors = 0 } = monitorCounts; + console.log({ monitors }); + const monitorsAsRows = monitors.map((monitor) => ({ + ip: monitor.name, + status: determineState(monitor), + processor: "2Ghz" /* How to retrieve that? */, + cpu: 80 /* How to retrieve that? */, + mem: 50 /* How to retrieve that? */, + disk: 70 /* How to retrieve that? */, + })); + + useEffect(() => { + dispatch(getInfrastructureMonitorsByTeamId(authToken)); + }, [authToken]); + // console.log({ isLoading, monitorsSummary, msg, success, rest }); + return ( - 5 + {totalMonitors} - {ROWS.map((row, index) => ( - - {/* TODO iterate over column and get column id, applying row[column.id] */} - {row.ip} - - - - - - ( + + {/* TODO iterate over column and get column id, applying row[column.id] */} + {row.ip} + + - {row.processor} - - - - - - - - - - - - - {/* Get ActionsMenu from Monitor Table and create a component */} - - + + + + {row.processor} + + + + - - - - ))} + + + + + + + + + {/* Get ActionsMenu from Monitor Table and create a component */} + + + + + + )) + } - {/* - - Showing {getRange()} of {monitorCount} monitor(s) - - - `Page ${page + 1} of ${Math.max(0, Math.ceil(count / rowsPerPage))}` - } - slotProps={{ - select: { - MenuProps: { - keepMounted: true, - disableScrollLock: true, - PaperProps: { - className: "pagination-dropdown", - sx: { - mt: 0, - mb: theme.spacing(2), - }, - }, - transformOrigin: { vertical: "bottom", horizontal: "left" }, - anchorOrigin: { vertical: "top", horizontal: "left" }, - sx: { - mt: theme.spacing(-2), - }, - }, - inputProps: { id: "pagination-dropdown" }, - IconComponent: SelectorVertical, - sx: { - ml: theme.spacing(4), - mr: theme.spacing(12), - minWidth: theme.spacing(20), - textAlign: "left", - "&.Mui-focused > div": { - backgroundColor: theme.palette.background.main, - }, - }, - }, - }} - sx={{ - color: theme.palette.text.secondary, - "& svg path": { - stroke: theme.palette.text.tertiary, - strokeWidth: 1.3, - }, - "& .MuiSelect-select": { - border: 1, - borderColor: theme.palette.border.light, - borderRadius: theme.shape.borderRadius, - }, - }} - /> - */} + {/* + TODO continue creating pagination component. It should change the current page, which will trigger refetch? + + */} + {/* */} ); diff --git a/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx b/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx index edfb47da3..bb3f5a65a 100644 --- a/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx +++ b/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx @@ -1,4 +1,15 @@ import PropTypes from "prop-types"; +import { useState, useEffect, memo, useCallback, useRef } from "react"; +import { useNavigate } from "react-router"; +import { useDispatch, useSelector } from "react-redux"; +import { useTheme } from "@emotion/react"; +import useUtils from "../../utils"; + +import { setRowsPerPage } from "../../../../Features/UI/uiSlice"; +import { logger } from "../../../../Utils/Logger"; +import { jwtDecode } from "jwt-decode"; +import { networkService } from "../../../../main"; + import { TableContainer, Table, @@ -8,106 +19,18 @@ import { TableBody, Paper, Box, - TablePagination, - Stack, - Typography, - Button, CircularProgress, } from "@mui/material"; +import ActionsMenu from "../actionsMenu"; +import Host from "../host"; +import { StatusLabel } from "../../../../Components/Label"; +import { TableBodySkeleton } from "./Skeleton"; +import BarChart from "../../../../Components/Charts/BarChart"; + import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded"; import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded"; -import { setRowsPerPage } from "../../../../Features/UI/uiSlice"; -import { useState, useEffect, memo, useCallback, useRef } from "react"; -import { useNavigate } from "react-router"; -import { logger } from "../../../../Utils/Logger"; -import Host from "../host"; -import { StatusLabel } from "../../../../Components/Label"; -import { jwtDecode } from "jwt-decode"; -import { useDispatch, useSelector } from "react-redux"; -import { networkService } from "../../../../main"; -import { useTheme } from "@emotion/react"; -import BarChart from "../../../../Components/Charts/BarChart"; -import LeftArrowDouble from "../../../../assets/icons/left-arrow-double.svg?react"; -import RightArrowDouble from "../../../../assets/icons/right-arrow-double.svg?react"; -import LeftArrow from "../../../../assets/icons/left-arrow.svg?react"; -import RightArrow from "../../../../assets/icons/right-arrow.svg?react"; -import SelectorVertical from "../../../../assets/icons/selector-vertical.svg?react"; -import ActionsMenu from "../actionsMenu"; -import useUtils from "../../utils"; -import { TableBodySkeleton } from "./Skeleton"; - -/** - * Component for pagination actions (first, previous, next, last). - * - * @component - * @param {Object} props - * @param {number} props.count - Total number of items. - * @param {number} props.page - Current page number. - * @param {number} props.rowsPerPage - Number of rows per page. - * @param {function} props.onPageChange - Callback function to handle page change. - * - * @returns {JSX.Element} Pagination actions component. - */ -const TablePaginationActions = (props) => { - const { count, page, rowsPerPage, onPageChange } = props; - const handleFirstPageButtonClick = (event) => { - onPageChange(event, 0); - }; - const handleBackButtonClick = (event) => { - onPageChange(event, page - 1); - }; - const handleNextButtonClick = (event) => { - onPageChange(event, page + 1); - }; - const handleLastPageButtonClick = (event) => { - onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1)); - }; - - return ( - - - - - - - ); -}; - -TablePaginationActions.propTypes = { - count: PropTypes.number.isRequired, - page: PropTypes.number.isRequired, - rowsPerPage: PropTypes.number.isRequired, - onPageChange: PropTypes.func.isRequired, -}; +import { Pagination } from "../../../Infrastructure/components/TablePagination"; const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => { const theme = useTheme(); @@ -117,6 +40,7 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => { const { rowsPerPage } = useSelector((state) => state.ui.monitors); const authState = useSelector((state) => state.auth); + const [page, setPage] = useState(0); const [monitors, setMonitors] = useState([]); const [monitorCount, setMonitorCount] = useState(0); @@ -195,11 +119,11 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => { * Helper function to calculate the range of displayed rows. * @returns {string} */ - const getRange = () => { - let start = page * rowsPerPage + 1; - let end = Math.min(page * rowsPerPage + rowsPerPage, monitorCount); - return `${start} - ${end}`; - }; + // const getRange = () => { + // let start = page * rowsPerPage + 1; + // let end = Math.min(page * rowsPerPage + rowsPerPage, monitorCount); + // return `${start} - ${end}`; + // }; const handleSort = async (field) => { let order = ""; @@ -392,78 +316,13 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => { - - - Showing {getRange()} of {monitorCount} monitor(s) - - - `Page ${page + 1} of ${Math.max(0, Math.ceil(count / rowsPerPage))}` - } - slotProps={{ - select: { - MenuProps: { - keepMounted: true, - disableScrollLock: true, - PaperProps: { - className: "pagination-dropdown", - sx: { - mt: 0, - mb: theme.spacing(2), - }, - }, - transformOrigin: { vertical: "bottom", horizontal: "left" }, - anchorOrigin: { vertical: "top", horizontal: "left" }, - sx: { - mt: theme.spacing(-2), - }, - }, - inputProps: { id: "pagination-dropdown" }, - IconComponent: SelectorVertical, - sx: { - ml: theme.spacing(4), - mr: theme.spacing(12), - minWidth: theme.spacing(20), - textAlign: "left", - "&.Mui-focused > div": { - backgroundColor: theme.palette.background.main, - }, - }, - }, - }} - sx={{ - color: theme.palette.text.secondary, - "& svg path": { - stroke: theme.palette.text.tertiary, - strokeWidth: 1.3, - }, - "& .MuiSelect-select": { - border: 1, - borderColor: theme.palette.border.light, - borderRadius: theme.shape.borderRadius, - }, - }} - /> - + ); }; diff --git a/Client/src/Pages/Monitors/Home/index.jsx b/Client/src/Pages/Monitors/Home/index.jsx index 345a0a88e..59655af92 100644 --- a/Client/src/Pages/Monitors/Home/index.jsx +++ b/Client/src/Pages/Monitors/Home/index.jsx @@ -20,6 +20,8 @@ const Monitors = ({ isAdmin }) => { const authState = useSelector((state) => state.auth); const dispatch = useDispatch({}); + console.log({ monitorState }); + useEffect(() => { dispatch(getUptimeMonitorsByTeamId(authState.authToken)); }, [authState.authToken, dispatch]); diff --git a/Client/src/Pages/Monitors/utils.jsx b/Client/src/Pages/Monitors/utils.jsx index 4dc4260cd..1c351d4ad 100644 --- a/Client/src/Pages/Monitors/utils.jsx +++ b/Client/src/Pages/Monitors/utils.jsx @@ -1,13 +1,15 @@ import { useTheme } from "@mui/material"; const useUtils = () => { - const theme = useTheme(); const determineState = (monitor) => { if (monitor.isActive === false) return "paused"; if (monitor?.status === undefined) return "pending"; return monitor?.status == true ? "up" : "down"; }; + /* TODO Refactor: from here on shouldn't live in a custom hook, but on theme, or constants */ + const theme = useTheme(); + const statusColor = { up: theme.palette.success.main, down: theme.palette.error.main,