feat: abstracting table pagination

This commit is contained in:
Caio Cabral
2024-11-27 17:38:39 -05:00
parent 4bfe522f7d
commit fd278d3a97
8 changed files with 350 additions and 329 deletions
@@ -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;
@@ -221,6 +221,7 @@ export const addDemoMonitors = createAsyncThunk(
}
}
);
export const deleteAllMonitors = createAsyncThunk(
"monitors/deleteAllMonitors",
async (data, thunkApi) => {
@@ -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 (
<Box sx={{ flexShrink: 0, ml: "24px" }}>
<Button
variant="group"
onClick={handleFirstPageButtonClick}
disabled={page === 0}
aria-label="first page"
>
<LeftArrowDouble />
</Button>
<Button
variant="group"
onClick={handleBackButtonClick}
disabled={page === 0}
aria-label="previous page"
>
<LeftArrow />
</Button>
<Button
variant="group"
onClick={handleNextButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="next page"
>
<RightArrow />
</Button>
<Button
variant="group"
onClick={handleLastPageButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="last page"
>
<RightArrowDouble />
</Button>
</Box>
);
}
export { TablePaginationActions };
@@ -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 (
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
px={theme.spacing(4)}
marginTop={8}
>
<Typography
px={theme.spacing(2)}
variant="body2"
sx={{ opacity: 0.7 }}
>
Showing {range} of {monitorCount} monitor(s)
</Typography>
<TablePagination
component="div"
count={monitorCount}
page={page}
onPageChange={handleChangePage}
rowsPerPage={rowsPerPage}
rowsPerPageOptions={ROWS_PER_PAGE_OPTIONS}
onRowsPerPageChange={handleChangeRowsPerPage}
ActionsComponent={TablePaginationActions}
labelRowsPerPage="Rows per page"
labelDisplayedRows={({ page, count }) =>
`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,
},
}}
/>
</Stack>
);
}
export { Pagination };
+107 -151
View File
@@ -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 (
<Stack
component="main"
@@ -134,7 +151,7 @@ function Infrastructure() {
borderColor={theme.palette.border.light}
backgroundColor={theme.palette.background.accent}
>
5
{totalMonitors}
</Box>
</Stack>
<TableContainer
@@ -170,142 +187,81 @@ function Infrastructure() {
</TableRow>
</TableHead>
<TableBody>
{ROWS.map((row, index) => (
<TableRow key={index}>
{/* TODO iterate over column and get column id, applying row[column.id] */}
<TableCell>{row.ip}</TableCell>
<TableCell align="center">
<StatusLabel
status={row.status}
text={row.status}
/* Use capitalize inside of Status Label */
/* Update component so we don't need to pass text and status separately*/
customStyles={{ textTransform: "capitalize" }}
/>
</TableCell>
<TableCell align="center">
<Stack
direction={"row"}
justifyContent={"center"}
alignItems={"center"}
gap=".25rem"
>
<CPUChipIcon
width={20}
height={20}
{
/* ROWS */ monitorsAsRows.map((row, index) => (
<TableRow key={index}>
{/* TODO iterate over column and get column id, applying row[column.id] */}
<TableCell>{row.ip}</TableCell>
<TableCell align="center">
<StatusLabel
status={row.status}
text={row.status}
/* Use capitalize inside of Status Label */
/* Update component so we don't need to pass text and status separately*/
customStyles={{ textTransform: "capitalize" }}
/>
{row.processor}
</Stack>
</TableCell>
<TableCell align="center">
<Gauge
progressValue={row.cpu}
containerWidth={60}
/>
</TableCell>
<TableCell align="center">
<Gauge
progressValue={row.mem}
containerWidth={60}
/>
</TableCell>
<TableCell align="center">
<Gauge
progressValue={row.disk}
containerWidth={60}
/>
</TableCell>
<TableCell align="center">
{/* Get ActionsMenu from Monitor Table and create a component */}
<IconButton
sx={{
"& svg path": {
stroke: theme.palette.other.icon,
},
}}
>
<GearIcon
width={20}
height={20}
</TableCell>
<TableCell align="center">
<Stack
direction={"row"}
justifyContent={"center"}
alignItems={"center"}
gap=".25rem"
>
<CPUChipIcon
width={20}
height={20}
/>
{row.processor}
</Stack>
</TableCell>
<TableCell align="center">
<Gauge
progressValue={row.cpu}
containerWidth={60}
/>
</IconButton>
</TableCell>
</TableRow>
))}
</TableCell>
<TableCell align="center">
<Gauge
progressValue={row.mem}
containerWidth={60}
/>
</TableCell>
<TableCell align="center">
<Gauge
progressValue={row.disk}
containerWidth={60}
/>
</TableCell>
<TableCell align="center">
{/* Get ActionsMenu from Monitor Table and create a component */}
<IconButton
sx={{
"& svg path": {
stroke: theme.palette.other.icon,
},
}}
>
<GearIcon
width={20}
height={20}
/>
</IconButton>
</TableCell>
</TableRow>
))
}
</TableBody>
</Table>
</TableContainer>
{/* <Stack
direction="row"
alignItems="center"
justifyContent="space-between"
marginTop={8}
>
<Typography
// px={theme.spacing(2)}
variant="body2"
// sx={{ opacity: 0.7 }}
>
Showing {getRange()} of {monitorCount} monitor(s)
</Typography>
<TablePagination
component="div"
count={monitorCount}
page={page}
onPageChange={handleChangePage}
rowsPerPage={rowsPerPage}
rowsPerPageOptions={[5, 10, 15, 25]}
onRowsPerPageChange={handleChangeRowsPerPage}
ActionsComponent={TablePaginationActions}
labelRowsPerPage="Rows per page"
labelDisplayedRows={({ page, count }) =>
`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,
},
}}
/>
</Stack> */}
{/*
TODO continue creating pagination component. It should change the current page, which will trigger refetch?
*/}
{/* <Pagination
// monitorCount={totalMonitors}
page={0}
/> */}
</Stack>
</Stack>
);
@@ -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 (
<Box sx={{ flexShrink: 0, ml: "24px" }}>
<Button
variant="group"
onClick={handleFirstPageButtonClick}
disabled={page === 0}
aria-label="first page"
>
<LeftArrowDouble />
</Button>
<Button
variant="group"
onClick={handleBackButtonClick}
disabled={page === 0}
aria-label="previous page"
>
<LeftArrow />
</Button>
<Button
variant="group"
onClick={handleNextButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="next page"
>
<RightArrow />
</Button>
<Button
variant="group"
onClick={handleLastPageButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="last page"
>
<RightArrowDouble />
</Button>
</Box>
);
};
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 }) => {
</TableBody>
</Table>
</TableContainer>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
px={theme.spacing(4)}
marginTop={8}
>
<Typography
px={theme.spacing(2)}
variant="body2"
sx={{ opacity: 0.7 }}
>
Showing {getRange()} of {monitorCount} monitor(s)
</Typography>
<TablePagination
component="div"
count={monitorCount}
page={page}
onPageChange={handleChangePage}
rowsPerPage={rowsPerPage}
rowsPerPageOptions={[5, 10, 15, 25]}
onRowsPerPageChange={handleChangeRowsPerPage}
ActionsComponent={TablePaginationActions}
labelRowsPerPage="Rows per page"
labelDisplayedRows={({ page, count }) =>
`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,
},
}}
/>
</Stack>
<Pagination
page={page}
rowsPerPage={rowsPerPage}
handleChangePage={handleChangePage}
handleChangeRowsPerPage={handleChangeRowsPerPage}
monitorCount={monitorCount}
/>
</Box>
);
};
+2
View File
@@ -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]);
+3 -1
View File
@@ -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,