From a011d8302055161dd9832e3b525ab8967adb95c0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 24 Jan 2025 13:08:33 -0800 Subject: [PATCH 1/5] Create circular count component --- Client/src/Components/CircularCount/index.jsx | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 Client/src/Components/CircularCount/index.jsx diff --git a/Client/src/Components/CircularCount/index.jsx b/Client/src/Components/CircularCount/index.jsx new file mode 100644 index 000000000..5c09ad162 --- /dev/null +++ b/Client/src/Components/CircularCount/index.jsx @@ -0,0 +1,29 @@ +import { Box } from "@mui/material"; +import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; +const CircularCount = ({ count }) => { + const theme = useTheme(); + return ( + + {count} + + ); +}; + +CircularCount.propTypes = { + count: PropTypes.number, +}; + +export default CircularCount; From a18b8172635805e7cb42901d5c069c095cdd6e07 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 24 Jan 2025 13:09:05 -0800 Subject: [PATCH 2/5] refactor components out to Components dir --- .../Home/Components/ActionsMenu/index.jsx | 240 ++++++++++++++++ .../Uptime/Home/Components/Host/index.jsx | 64 +++++ .../Home/Components/LoadingSpinner/index.jsx | 46 +++ .../Home/Components/SearchComponent/index.jsx | 43 +++ .../Uptime/Home/Components/Skeleton/index.jsx | 61 ++++ .../Home/Components/StatusBoxes/index.jsx | 36 +++ .../Home/Components/StatusBoxes/skeleton.jsx | 31 +++ .../Home/Components/StatusBoxes/statusBox.jsx | 107 +++++++ .../Home/Components/UptimeDataTable/index.jsx | 263 ++++++++++++++++++ .../Components/UptimeDataTable/skeleton.jsx | 21 ++ 10 files changed, 912 insertions(+) create mode 100644 Client/src/Pages/Uptime/Home/Components/ActionsMenu/index.jsx create mode 100644 Client/src/Pages/Uptime/Home/Components/Host/index.jsx create mode 100644 Client/src/Pages/Uptime/Home/Components/LoadingSpinner/index.jsx create mode 100644 Client/src/Pages/Uptime/Home/Components/SearchComponent/index.jsx create mode 100644 Client/src/Pages/Uptime/Home/Components/Skeleton/index.jsx create mode 100644 Client/src/Pages/Uptime/Home/Components/StatusBoxes/index.jsx create mode 100644 Client/src/Pages/Uptime/Home/Components/StatusBoxes/skeleton.jsx create mode 100644 Client/src/Pages/Uptime/Home/Components/StatusBoxes/statusBox.jsx create mode 100644 Client/src/Pages/Uptime/Home/Components/UptimeDataTable/index.jsx create mode 100644 Client/src/Pages/Uptime/Home/Components/UptimeDataTable/skeleton.jsx diff --git a/Client/src/Pages/Uptime/Home/Components/ActionsMenu/index.jsx b/Client/src/Pages/Uptime/Home/Components/ActionsMenu/index.jsx new file mode 100644 index 000000000..f0561d6e6 --- /dev/null +++ b/Client/src/Pages/Uptime/Home/Components/ActionsMenu/index.jsx @@ -0,0 +1,240 @@ +import { useState } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { useTheme } from "@emotion/react"; +import { useNavigate } from "react-router-dom"; +import { createToast } from "../../../../../Utils/toastUtils"; +import { logger } from "../../../../../Utils/Logger"; +import { IconButton, Menu, MenuItem } from "@mui/material"; +import { + deleteUptimeMonitor, + pauseUptimeMonitor, +} from "../../../../../Features/UptimeMonitors/uptimeMonitorsSlice"; +import Settings from "../../../../../assets/icons/settings-bold.svg?react"; +import PropTypes from "prop-types"; +import Dialog from "../../../../../Components/Dialog"; + +const ActionsMenu = ({ + monitor, + isAdmin, + updateRowCallback, + pauseCallback, + setIsLoading, +}) => { + const [anchorEl, setAnchorEl] = useState(null); + const [actions, setActions] = useState({}); + const [isOpen, setIsOpen] = useState(false); + const dispatch = useDispatch(); + const theme = useTheme(); + const authState = useSelector((state) => state.auth); + const authToken = authState.authToken; + const { isLoading } = useSelector((state) => state.uptimeMonitors); + + const handleRemove = async (event) => { + event.preventDefault(); + event.stopPropagation(); + let monitor = { _id: actions.id }; + const action = await dispatch( + deleteUptimeMonitor({ authToken: authState.authToken, monitor }) + ); + if (action.meta.requestStatus === "fulfilled") { + setIsOpen(false); // close modal + updateRowCallback(); + createToast({ body: "Monitor deleted successfully." }); + } else { + createToast({ body: "Failed to delete monitor." }); + } + }; + + const handlePause = async () => { + try { + setIsLoading(true); + const action = await dispatch( + pauseUptimeMonitor({ authToken, monitorId: monitor._id }) + ); + if (pauseUptimeMonitor.fulfilled.match(action)) { + const state = action?.payload?.data.isActive === false ? "paused" : "resumed"; + createToast({ body: `Monitor ${state} successfully.` }); + pauseCallback(); + } else { + throw new Error(action?.error?.message ?? "Failed to pause monitor."); + } + } catch (error) { + logger.error("Error pausing monitor:", monitor._id, error); + createToast({ body: "Failed to pause monitor." }); + } + }; + + const openMenu = (event, id, url) => { + event.preventDefault(); + event.stopPropagation(); + setAnchorEl(event.currentTarget); + setActions({ id: id, url: url }); + }; + + const openRemove = (e) => { + closeMenu(e); + setIsOpen(true); + }; + + const closeMenu = (e) => { + e.stopPropagation(); + setAnchorEl(null); + }; + + const navigate = useNavigate(); + return ( + <> + { + event.stopPropagation(); + openMenu(event, monitor._id, monitor.type === "ping" ? null : monitor.url); + }} + sx={{ + "&:focus": { + outline: "none", + }, + "& svg path": { + stroke: theme.palette.primary.contrastTextTertiary, + }, + }} + > + + + + closeMenu(e)} + disableScrollLock + slotProps={{ + paper: { + sx: { + "& ul": { + p: theme.spacing(2.5), + backgroundColor: theme.palette.primary.main, + }, + "& li": { m: 0, color: theme.palette.primary.contrastTextSecondary }, + /* + This should not be set automatically on the last of type + "& li:last-of-type": { + color: theme.palette.error.main, + }, */ + }, + }, + }} + > + {actions.url !== null ? ( + { + closeMenu(e); + e.stopPropagation(); + window.open(actions.url, "_blank", "noreferrer"); + }} + > + Open site + + ) : ( + "" + )} + { + e.stopPropagation(); + navigate(`/uptime/${actions.id}`); + }} + > + Details + + {/* TODO - pass monitor id to Incidents page */} + { + e.stopPropagation(); + navigate(`/incidents/${actions.id}`); + }} + > + Incidents + + {isAdmin && ( + { + e.stopPropagation(); + + navigate(`/uptime/configure/${actions.id}`); + }} + > + Configure + + )} + {isAdmin && ( + { + e.stopPropagation(); + navigate(`/uptime/create/${actions.id}`); + }} + > + Clone + + )} + {isAdmin && ( + { + closeMenu(e); + + e.stopPropagation(); + handlePause(e); + }} + > + {monitor?.isActive === true ? "Pause" : "Resume"} + + )} + {isAdmin && ( + { + e.stopPropagation(); + openRemove(e); + }} + sx={{ "&.MuiButtonBase-root": { color: theme.palette.error.main } }} + > + Remove + + )} + + { + e.stopPropagation(); + setIsOpen(false); + }} + confirmationButtonLabel="Delete" + /* Do we need stop propagation? */ + onConfirm={(e) => { + e.stopPropagation(); + handleRemove(e); + }} + isLoading={isLoading} + modelTitle="modal-delete-monitor" + modelDescription="delete-monitor-confirmation" + /> + + ); +}; + +ActionsMenu.propTypes = { + monitor: PropTypes.shape({ + _id: PropTypes.string, + url: PropTypes.string, + type: PropTypes.string, + isActive: PropTypes.bool, + }).isRequired, + isAdmin: PropTypes.bool, + updateRowCallback: PropTypes.func, + pauseCallback: PropTypes.func, + setIsLoading: PropTypes.func, +}; + +export default ActionsMenu; diff --git a/Client/src/Pages/Uptime/Home/Components/Host/index.jsx b/Client/src/Pages/Uptime/Home/Components/Host/index.jsx new file mode 100644 index 000000000..ec78875f4 --- /dev/null +++ b/Client/src/Pages/Uptime/Home/Components/Host/index.jsx @@ -0,0 +1,64 @@ +import { Box, Typography } from "@mui/material"; +import PropTypes from "prop-types"; +/** + * Host component. + * This subcomponent receives a params object and displays the host details. + * + * @component + * @param {Object} params - An object containing the following properties: + * @param {string} params.url - The URL of the host. + * @param {string} params.title - The name of the host. + * @param {string} params.percentageColor - The color of the percentage text. + * @param {number} params.percentage - The percentage to display. + * @returns {React.ElementType} Returns a div element with the host details. + */ +const Host = ({ url, title, percentageColor, percentage }) => { + const noTitle = title === undefined || title === url; + return ( + + + {title} + + {percentageColor && percentage && ( + + {percentage}% + + )} + {!noTitle && {url}} + + ); +}; + +Host.propTypes = { + title: PropTypes.string, + percentageColor: PropTypes.string, + percentage: PropTypes.string, + url: PropTypes.string, +}; + +export default Host; diff --git a/Client/src/Pages/Uptime/Home/Components/LoadingSpinner/index.jsx b/Client/src/Pages/Uptime/Home/Components/LoadingSpinner/index.jsx new file mode 100644 index 000000000..a649473af --- /dev/null +++ b/Client/src/Pages/Uptime/Home/Components/LoadingSpinner/index.jsx @@ -0,0 +1,46 @@ +import { CircularProgress, Box } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import PropTypes from "prop-types"; +const LoadingSpinner = ({ shouldRender }) => { + const theme = useTheme(); + if (shouldRender === false) { + return; + } + + return ( + <> + + + + + + ); +}; + +LoadingSpinner.propTypes = { + shouldRender: PropTypes.bool, +}; + +export default LoadingSpinner; diff --git a/Client/src/Pages/Uptime/Home/Components/SearchComponent/index.jsx b/Client/src/Pages/Uptime/Home/Components/SearchComponent/index.jsx new file mode 100644 index 000000000..d93e6672b --- /dev/null +++ b/Client/src/Pages/Uptime/Home/Components/SearchComponent/index.jsx @@ -0,0 +1,43 @@ +import { useState } from "react"; +import Search from "../../../../../Components/Inputs/Search"; +import { Box } from "@mui/material"; +import useDebounce from "../../Hooks/useDebounce"; +import { useEffect } from "react"; +import PropTypes from "prop-types"; + +const SearchComponent = ({ monitors, onSearchChange, setIsSearching }) => { + const [localSearch, setLocalSearch] = useState(""); + const debouncedSearch = useDebounce(localSearch, 500); + useEffect(() => { + onSearchChange(debouncedSearch); + setIsSearching(false); + }, [debouncedSearch, onSearchChange, setIsSearching]); + + const handleSearch = (value) => { + setLocalSearch(value); + setIsSearching(true); + }; + + return ( + + + + ); +}; + +SearchComponent.propTypes = { + monitors: PropTypes.array, + onSearchChange: PropTypes.func, + setIsSearching: PropTypes.func, +}; + +export default SearchComponent; diff --git a/Client/src/Pages/Uptime/Home/Components/Skeleton/index.jsx b/Client/src/Pages/Uptime/Home/Components/Skeleton/index.jsx new file mode 100644 index 000000000..4e55be536 --- /dev/null +++ b/Client/src/Pages/Uptime/Home/Components/Skeleton/index.jsx @@ -0,0 +1,61 @@ +import { Skeleton, Stack } from "@mui/material"; +import { useTheme } from "@emotion/react"; + +/** + * Renders a skeleton layout. + * + * @returns {JSX.Element} + */ +const SkeletonLayout = () => { + const theme = useTheme(); + + return ( + <> + + + + + + + + + + + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Pages/Uptime/Home/Components/StatusBoxes/index.jsx b/Client/src/Pages/Uptime/Home/Components/StatusBoxes/index.jsx new file mode 100644 index 000000000..ac9eca47f --- /dev/null +++ b/Client/src/Pages/Uptime/Home/Components/StatusBoxes/index.jsx @@ -0,0 +1,36 @@ +import PropTypes from "prop-types"; +import { Stack } from "@mui/material"; +import StatusBox from "./statusBox"; +import { useTheme } from "@emotion/react"; +import SkeletonLayout from "./skeleton"; + +const StatusBoxes = ({ shouldRender, monitorsSummary }) => { + const theme = useTheme(); + if (!shouldRender) return ; + return ( + + + + + + ); +}; + +StatusBoxes.propTypes = { + monitorsSummary: PropTypes.object.isRequired, +}; + +export default StatusBoxes; diff --git a/Client/src/Pages/Uptime/Home/Components/StatusBoxes/skeleton.jsx b/Client/src/Pages/Uptime/Home/Components/StatusBoxes/skeleton.jsx new file mode 100644 index 000000000..df6e08ccd --- /dev/null +++ b/Client/src/Pages/Uptime/Home/Components/StatusBoxes/skeleton.jsx @@ -0,0 +1,31 @@ +import { Skeleton, Stack } from "@mui/material"; +import { useTheme } from "@emotion/react"; + +const SkeletonLayout = () => { + const theme = useTheme(); + return ( + + + + + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Pages/Uptime/Home/Components/StatusBoxes/statusBox.jsx b/Client/src/Pages/Uptime/Home/Components/StatusBoxes/statusBox.jsx new file mode 100644 index 000000000..b157857ea --- /dev/null +++ b/Client/src/Pages/Uptime/Home/Components/StatusBoxes/statusBox.jsx @@ -0,0 +1,107 @@ +import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; +import { Box, Stack, Typography } from "@mui/material"; +import Arrow from "../../../../../assets/icons/top-right-arrow.svg?react"; +import Background from "../../../../../assets/Images/background-grid.svg?react"; +import ClockSnooze from "../../../../../assets/icons/clock-snooze.svg?react"; + +const StatusBox = ({ title, value }) => { + const theme = useTheme(); + + let sharedStyles = { + position: "absolute", + right: 8, + opacity: 0.5, + "& svg path": { stroke: theme.palette.primary.contrastTextTertiary }, + }; + + let color; + let icon; + if (title === "up") { + color = theme.palette.success.lowContrast; + icon = ( + + + + ); + } else if (title === "down") { + color = theme.palette.error.lowContrast; + icon = ( + + + + ); + } else if (title === "paused") { + color = theme.palette.warning.lowContrast; + icon = ( + + + + ); + } + + return ( + + + + + + + + {title} + + {icon} + + + {value} + + + # + + + + + ); +}; + +StatusBox.propTypes = { + title: PropTypes.oneOf(["up", "down", "paused"]).isRequired, + value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, +}; + +export default StatusBox; diff --git a/Client/src/Pages/Uptime/Home/Components/UptimeDataTable/index.jsx b/Client/src/Pages/Uptime/Home/Components/UptimeDataTable/index.jsx new file mode 100644 index 000000000..eaf08046d --- /dev/null +++ b/Client/src/Pages/Uptime/Home/Components/UptimeDataTable/index.jsx @@ -0,0 +1,263 @@ +// Components +import { Box, Stack, CircularProgress } from "@mui/material"; +import { Heading } from "../../../../../Components/Heading"; +import DataTable from "../../../../../Components/Table"; +import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded"; +import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded"; +import Host from "../Host"; +import { StatusLabel } from "../../../../../Components/Label"; +import BarChart from "../../../../../Components/Charts/BarChart"; +import ActionsMenu from "../ActionsMenu"; +import { useState } from "react"; +import SearchComponent from "../SearchComponent"; +import CircularCount from "../../../../../Components/CircularCount"; +import LoadingSpinner from "../LoadingSpinner"; +import UptimeDataTableSkeleton from "./skeleton"; +// Utils +import { useTheme } from "@emotion/react"; +import useUtils from "../../Hooks/useUtils"; +import { useNavigate } from "react-router-dom"; +import PropTypes from "prop-types"; + +const MonitorDataTable = ({ shouldRender, isSearching, headers, filteredMonitors }) => { + const theme = useTheme(); + const navigate = useNavigate(); + + if (!shouldRender) return null; + return ( + + + { + navigate(`/uptime/${row.id}`); + }, + emptyView: "No monitors found", + }} + /> + + ); +}; + +/** + * UptimeDataTable displays a table of uptime monitors with sorting, searching, and action capabilities + * @param {Object} props - Component props + * @param {boolean} props.isAdmin - Whether the current user has admin privileges + * @param {boolean} props.isLoading - Loading state of the table + * @param {Array<{ + * _id: string, + * url: string, + * title: string, + * percentage: number, + * percentageColor: string, + * monitor: { + * _id: string, + * type: string, + * checks: Array + * } + * }>} props.monitors - Array of monitor objects to display + * @param {number} props.monitorCount - Total count of monitors + * @param {Object} props.sort - Current sort configuration + * @param {string} props.sort.field - Field to sort by + * @param {'asc'|'desc'} props.sort.order - Sort direction + * @param {Function} props.setSort - Callback to update sort configuration + * @param {string} props.search - Current search query + * @param {Function} props.setSearch - Callback to update search query + * @param {boolean} props.isSearching - Whether a search is in progress + * @param {Function} props.setIsLoading - Callback to update loading state + * @param {Function} props.triggerUpdate - Callback to trigger a data refresh + * @returns {JSX.Element} Rendered component + */ +const UptimeDataTable = (props) => { + // Utils + + const { + isAdmin, + setIsLoading, + monitors, + filteredMonitors, + monitorCount, + sort, + setSort, + setSearch, + triggerUpdate, + monitorsAreLoading, + } = props; + const { determineState } = useUtils(); + const theme = useTheme(); + const navigate = useNavigate(); + + // Local state + const [isSearching, setIsSearching] = useState(false); + // Handlers + const handleSort = (field) => { + let order = ""; + if (sort.field !== field) { + order = "desc"; + } else { + order = sort.order === "asc" ? "desc" : "asc"; + } + setSort({ field, order }); + }; + + const headers = [ + { + id: "name", + content: ( + handleSort("name")} + > + Host + + {sort.order === "asc" ? ( + + ) : ( + + )} + + + ), + render: (row) => ( + + ), + }, + { + id: "status", + content: ( + handleSort("status")} + > + {" "} + Status + + {sort.order === "asc" ? ( + + ) : ( + + )} + + + ), + render: (row) => { + const status = determineState(row.monitor); + return ( + + ); + }, + }, + { + id: "responseTime", + content: "Response Time", + render: (row) => , + }, + { + id: "type", + content: "Type", + render: (row) => ( + {row.monitor.type} + ), + }, + { + id: "actions", + content: "Actions", + render: (row) => ( + + ), + }, + ]; + console.log("rendering"); + return ( + + + Uptime monitors + + + + + + + + ); +}; + +UptimeDataTable.propTypes = { + isSearching: PropTypes.bool, + setIsSearching: PropTypes.func, + setSort: PropTypes.func, + setSearch: PropTypes.func, + setIsLoading: PropTypes.func, + triggerUpdate: PropTypes.func, + debouncedSearch: PropTypes.string, + onSearchChange: PropTypes.func, + isAdmin: PropTypes.bool, + isLoading: PropTypes.bool, + monitors: PropTypes.array, + filteredMonitors: PropTypes.array, + monitorCount: PropTypes.number, + sort: PropTypes.shape({ + field: PropTypes.string, + order: PropTypes.oneOf(["asc", "desc"]), + }), +}; + +export default UptimeDataTable; diff --git a/Client/src/Pages/Uptime/Home/Components/UptimeDataTable/skeleton.jsx b/Client/src/Pages/Uptime/Home/Components/UptimeDataTable/skeleton.jsx new file mode 100644 index 000000000..303eb48c8 --- /dev/null +++ b/Client/src/Pages/Uptime/Home/Components/UptimeDataTable/skeleton.jsx @@ -0,0 +1,21 @@ +import { Skeleton } from "@mui/material"; +import PropTypes from "prop-types"; + +const UptimeDataTableSkeleton = ({ shouldRender }) => { + if (!shouldRender) return null; + + return ( + + ); +}; + +UptimeDataTableSkeleton.propTypes = { + shouldRender: PropTypes.bool.isRequired, +}; + +export default UptimeDataTableSkeleton; From 0234cab8b576a1e801db182bc04380238b936049 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 24 Jan 2025 13:09:57 -0800 Subject: [PATCH 3/5] refactor uptime home page --- Client/src/Components/StatBox/index.jsx | 2 +- .../Pages/Infrastructure/Details/index.jsx | 2 +- Client/src/Pages/Infrastructure/index.jsx | 4 +- .../src/Pages/PageSpeed/Configure/index.jsx | 2 +- Client/src/Pages/PageSpeed/Details/index.jsx | 2 +- Client/src/Pages/PageSpeed/card.jsx | 2 +- Client/src/Pages/Uptime/Details/index.jsx | 2 +- .../Pages/Uptime/Home/Hooks/useDebounce.jsx | 18 + .../Uptime/Home/Hooks/useMonitorFetch.jsx | 98 ++++++ .../{utils.jsx => Home/Hooks/useUtils.jsx} | 0 Client/src/Pages/Uptime/Home/StatusBox.jsx | 106 ------ .../Home/UptimeDataTable/Skeleton/index.jsx | 47 --- .../Uptime/Home/UptimeDataTable/index.jsx | 325 ------------------ Client/src/Pages/Uptime/Home/actionsMenu.jsx | 240 ------------- Client/src/Pages/Uptime/Home/fallback.jsx | 59 ---- Client/src/Pages/Uptime/Home/host.jsx | 64 ---- Client/src/Pages/Uptime/Home/index.css | 6 - Client/src/Pages/Uptime/Home/index.jsx | 279 +++++---------- Client/src/Pages/Uptime/Home/skeleton.jsx | 61 ---- Server/controllers/monitorController.js | 2 +- Server/db/mongo/modules/monitorModule.js | 32 +- 21 files changed, 243 insertions(+), 1110 deletions(-) create mode 100644 Client/src/Pages/Uptime/Home/Hooks/useDebounce.jsx create mode 100644 Client/src/Pages/Uptime/Home/Hooks/useMonitorFetch.jsx rename Client/src/Pages/Uptime/{utils.jsx => Home/Hooks/useUtils.jsx} (100%) delete mode 100644 Client/src/Pages/Uptime/Home/StatusBox.jsx delete mode 100644 Client/src/Pages/Uptime/Home/UptimeDataTable/Skeleton/index.jsx delete mode 100644 Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx delete mode 100644 Client/src/Pages/Uptime/Home/actionsMenu.jsx delete mode 100644 Client/src/Pages/Uptime/Home/fallback.jsx delete mode 100644 Client/src/Pages/Uptime/Home/host.jsx delete mode 100644 Client/src/Pages/Uptime/Home/index.css delete mode 100644 Client/src/Pages/Uptime/Home/skeleton.jsx diff --git a/Client/src/Components/StatBox/index.jsx b/Client/src/Components/StatBox/index.jsx index 953d378ec..b476a1c33 100644 --- a/Client/src/Components/StatBox/index.jsx +++ b/Client/src/Components/StatBox/index.jsx @@ -1,7 +1,7 @@ import { Box, Typography } from "@mui/material"; import { useTheme } from "@mui/material/styles"; import PropTypes from "prop-types"; -import useUtils from "../../Pages/Uptime/utils"; +import useUtils from "../../Pages/Uptime/Home/Hooks/useUtils"; /** * StatBox Component diff --git a/Client/src/Pages/Infrastructure/Details/index.jsx b/Client/src/Pages/Infrastructure/Details/index.jsx index acd1bafb1..5da921e30 100644 --- a/Client/src/Pages/Infrastructure/Details/index.jsx +++ b/Client/src/Pages/Infrastructure/Details/index.jsx @@ -8,7 +8,7 @@ import AreaChart from "../../../Components/Charts/AreaChart"; import { useSelector } from "react-redux"; import { networkService } from "../../../main"; import PulseDot from "../../../Components/Animated/PulseDot"; -import useUtils from "../../Uptime/utils"; +import useUtils from "../../Uptime/Home/Hooks/useUtils"; import { useNavigate } from "react-router-dom"; import Empty from "./empty"; import { logger } from "../../../Utils/Logger"; diff --git a/Client/src/Pages/Infrastructure/index.jsx b/Client/src/Pages/Infrastructure/index.jsx index 0bf367b99..43768132b 100644 --- a/Client/src/Pages/Infrastructure/index.jsx +++ b/Client/src/Pages/Infrastructure/index.jsx @@ -2,7 +2,7 @@ import { useEffect, useState, useCallback } from "react"; import { useNavigate } from "react-router-dom"; import { /* useDispatch, */ useSelector } from "react-redux"; import { useTheme } from "@emotion/react"; -import useUtils from "../Uptime/utils.jsx"; +import useUtils from "../Uptime/Home/Hooks/useUtils.jsx"; import { jwtDecode } from "jwt-decode"; import SkeletonLayout from "./skeleton"; import Fallback from "../../Components/Fallback"; @@ -17,7 +17,7 @@ import Pagination from "../../Components/Table/TablePagination/index.jsx"; // import { getInfrastructureMonitorsByTeamId } from "../../Features/InfrastructureMonitors/infrastructureMonitorsSlice"; import { networkService } from "../../Utils/NetworkService.js"; import CustomGauge from "../../Components/Charts/CustomGauge/index.jsx"; -import Host from "../Uptime/Home/host.jsx"; +import Host from "../Uptime/Home/Components/Host"; import { useIsAdmin } from "../../Hooks/useIsAdmin.js"; import { InfrastructureMenu } from "./components/Menu"; diff --git a/Client/src/Pages/PageSpeed/Configure/index.jsx b/Client/src/Pages/PageSpeed/Configure/index.jsx index f7829ef35..eee6dfcc9 100644 --- a/Client/src/Pages/PageSpeed/Configure/index.jsx +++ b/Client/src/Pages/PageSpeed/Configure/index.jsx @@ -23,7 +23,7 @@ import PulseDot from "../../../Components/Animated/PulseDot"; import LoadingButton from "@mui/lab/LoadingButton"; import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded"; import SkeletonLayout from "./skeleton"; -import useUtils from "../../Uptime/utils"; +import useUtils from "../../Uptime/Home/Hooks/useUtils"; import "./index.css"; import Dialog from "../../../Components/Dialog"; diff --git a/Client/src/Pages/PageSpeed/Details/index.jsx b/Client/src/Pages/PageSpeed/Details/index.jsx index c5badbab8..9192faaa9 100644 --- a/Client/src/Pages/PageSpeed/Details/index.jsx +++ b/Client/src/Pages/PageSpeed/Details/index.jsx @@ -19,7 +19,7 @@ import PulseDot from "../../../Components/Animated/PulseDot"; import PagespeedDetailsAreaChart from "./Charts/AreaChart"; import Checkbox from "../../../Components/Inputs/Checkbox"; import PieChart from "./Charts/PieChart"; -import useUtils from "../../Uptime/utils"; +import useUtils from "../../Uptime/Home/Hooks/useUtils"; import "./index.css"; import { useIsAdmin } from "../../../Hooks/useIsAdmin"; import StatBox from "../../../Components/StatBox"; diff --git a/Client/src/Pages/PageSpeed/card.jsx b/Client/src/Pages/PageSpeed/card.jsx index f5b0d7110..f7f6c4249 100644 --- a/Client/src/Pages/PageSpeed/card.jsx +++ b/Client/src/Pages/PageSpeed/card.jsx @@ -7,7 +7,7 @@ import { useTheme } from "@emotion/react"; import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts"; import { useSelector } from "react-redux"; import { formatDateWithTz, formatDurationSplit } from "../../Utils/timeUtils"; -import useUtils from "../Uptime/utils"; +import useUtils from "../Uptime/Home/Hooks/useUtils"; import { useState } from "react"; import IconBox from "../../Components/IconBox"; /** diff --git a/Client/src/Pages/Uptime/Details/index.jsx b/Client/src/Pages/Uptime/Details/index.jsx index d5be671d3..be589c1ba 100644 --- a/Client/src/Pages/Uptime/Details/index.jsx +++ b/Client/src/Pages/Uptime/Details/index.jsx @@ -19,7 +19,7 @@ import PulseDot from "../../../Components/Animated/PulseDot"; import { ChartBox } from "./styled"; import SkeletonLayout from "./skeleton"; import "./index.css"; -import useUtils from "../utils"; +import useUtils from "../Home/Hooks/useUtils"; import { formatDateWithTz, formatDurationSplit } from "../../../Utils/timeUtils"; import { useIsAdmin } from "../../../Hooks/useIsAdmin"; import IconBox from "../../../Components/IconBox"; diff --git a/Client/src/Pages/Uptime/Home/Hooks/useDebounce.jsx b/Client/src/Pages/Uptime/Home/Hooks/useDebounce.jsx new file mode 100644 index 000000000..11d639f14 --- /dev/null +++ b/Client/src/Pages/Uptime/Home/Hooks/useDebounce.jsx @@ -0,0 +1,18 @@ +import { useState, useEffect } from "react"; + +const useDebounce = (value, delay) => { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + return debouncedValue; +}; + +export default useDebounce; diff --git a/Client/src/Pages/Uptime/Home/Hooks/useMonitorFetch.jsx b/Client/src/Pages/Uptime/Home/Hooks/useMonitorFetch.jsx new file mode 100644 index 000000000..d4b7e5ba7 --- /dev/null +++ b/Client/src/Pages/Uptime/Home/Hooks/useMonitorFetch.jsx @@ -0,0 +1,98 @@ +import { useEffect, useState } from "react"; +import { networkService } from "../../../../main"; +import { createToast } from "../../../../Utils/toastUtils"; +import { useTheme } from "@emotion/react"; +const getMonitorWithPercentage = (monitor, theme) => { + let uptimePercentage = ""; + let percentageColor = ""; + + if (monitor.uptimePercentage !== undefined) { + uptimePercentage = + monitor.uptimePercentage === 0 ? "0" : (monitor.uptimePercentage * 100).toFixed(2); + + percentageColor = + monitor.uptimePercentage < 0.25 + ? theme.palette.error.main + : monitor.uptimePercentage < 0.5 + ? theme.palette.warning.main + : monitor.uptimePercentage < 0.75 + ? theme.palette.success.main + : theme.palette.success.main; + } + + return { + id: monitor._id, + name: monitor.name, + url: monitor.url, + title: monitor.name, + percentage: uptimePercentage, + percentageColor, + monitor: monitor, + }; +}; + +export const useMonitorFetch = ({ + authToken, + teamId, + limit, + page, + rowsPerPage, + filter, + field, + order, + triggerUpdate, +}) => { + const [monitorsAreLoading, setMonitorsAreLoading] = useState(false); + const [monitors, setMonitors] = useState([]); + const [filteredMonitors, setFilteredMonitors] = useState([]); + const [monitorsSummary, setMonitorsSummary] = useState({}); + + const theme = useTheme(); + + useEffect(() => { + const fetchMonitors = async () => { + try { + setMonitorsAreLoading(true); + const res = await networkService.getMonitorsByTeamId({ + authToken, + teamId, + limit, + types: ["http", "ping", "docker", "port"], + page, + rowsPerPage, + filter, + field, + order, + }); + const { monitors, filteredMonitors, summary } = res.data.data; + const mappedMonitors = filteredMonitors.map((monitor) => + getMonitorWithPercentage(monitor, theme) + ); + setMonitors(monitors); + setFilteredMonitors(mappedMonitors); + setMonitorsSummary(summary); + } catch (error) { + createToast({ + body: error.message, + }); + } finally { + setMonitorsAreLoading(false); + } + }; + fetchMonitors(); + }, [ + authToken, + teamId, + limit, + field, + filter, + order, + page, + rowsPerPage, + theme, + triggerUpdate, + ]); + return { monitors, filteredMonitors, monitorsSummary, monitorsAreLoading }; +}; + +export default useMonitorFetch; diff --git a/Client/src/Pages/Uptime/utils.jsx b/Client/src/Pages/Uptime/Home/Hooks/useUtils.jsx similarity index 100% rename from Client/src/Pages/Uptime/utils.jsx rename to Client/src/Pages/Uptime/Home/Hooks/useUtils.jsx diff --git a/Client/src/Pages/Uptime/Home/StatusBox.jsx b/Client/src/Pages/Uptime/Home/StatusBox.jsx deleted file mode 100644 index d04ebdf5f..000000000 --- a/Client/src/Pages/Uptime/Home/StatusBox.jsx +++ /dev/null @@ -1,106 +0,0 @@ -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import { Box, Stack, Typography } from "@mui/material"; -import Arrow from "../../../assets/icons/top-right-arrow.svg?react"; -import Background from "../../../assets/Images/background-grid.svg?react"; -import ClockSnooze from "../../../assets/icons/clock-snooze.svg?react"; - -const StatusBox = ({ title, value }) => { - const theme = useTheme(); - - let sharedStyles = { - position: "absolute", - right: 8, - opacity: 0.5, - "& svg path": { stroke: theme.palette.primary.contrastTextTertiary }, - }; - - let color; - let icon; - if (title === "up") { - color = theme.palette.success.lowContrast; - icon = ( - - - - ); - } else if (title === "down") { - color = theme.palette.error.lowContrast; - icon = ( - - - - ); - } else if (title === "paused") { - color = theme.palette.warning.lowContrast; - icon = ( - - - - ); - } - - return ( - - - - - - {title} - - {icon} - - {value} - - - # - - - - ); -}; - -StatusBox.propTypes = { - title: PropTypes.oneOf(["up", "down", "paused"]).isRequired, - value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, -}; - -export default StatusBox; diff --git a/Client/src/Pages/Uptime/Home/UptimeDataTable/Skeleton/index.jsx b/Client/src/Pages/Uptime/Home/UptimeDataTable/Skeleton/index.jsx deleted file mode 100644 index c17f6d74b..000000000 --- a/Client/src/Pages/Uptime/Home/UptimeDataTable/Skeleton/index.jsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Skeleton } from "@mui/material"; -import DataTable from "../../../../../Components/Table"; -const ROWS_NUMBER = 7; -const ROWS_ARRAY = Array.from({ length: ROWS_NUMBER }, (_, i) => i); - -const TableSkeleton = () => { - /* TODO Skeleton does not follow light and dark theme */ - - const headers = [ - { - id: "name", - - content: "Host", - - render: () => , - }, - { - id: "status", - content: "Status", - render: () => , - }, - { - id: "responseTime", - content: "Response Time", - render: () => , - }, - { - id: "type", - content: "Type", - render: () => , - }, - { - id: "actions", - content: "Actions", - render: () => , - }, - ]; - - return ( - - ); -}; - -export { TableSkeleton }; diff --git a/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx b/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx deleted file mode 100644 index 6961c6d5c..000000000 --- a/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx +++ /dev/null @@ -1,325 +0,0 @@ -// Components -import { Box, Stack, CircularProgress } from "@mui/material"; -import Search from "../../../../Components/Inputs/Search"; -import { Heading } from "../../../../Components/Heading"; -import DataTable from "../../../../Components/Table"; -import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded"; -import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded"; -import Host from "../host"; -import { StatusLabel } from "../../../../Components/Label"; -import BarChart from "../../../../Components/Charts/BarChart"; -import ActionsMenu from "../actionsMenu"; - -// Utils -import { useTheme } from "@emotion/react"; -import useUtils from "../../utils"; -import { useState, memo, useCallback } from "react"; -import { useNavigate } from "react-router-dom"; -import "../index.css"; -import PropTypes from "prop-types"; - -const SearchComponent = memo( - ({ monitors, debouncedSearch, onSearchChange, setIsSearching }) => { - const [localSearch, setLocalSearch] = useState(debouncedSearch); - const handleSearch = useCallback( - (value) => { - setIsSearching(true); - setLocalSearch(value); - onSearchChange(value); - }, - [onSearchChange, setIsSearching] - ); - - return ( - - - - ); - } -); -SearchComponent.displayName = "SearchComponent"; -SearchComponent.propTypes = { - monitors: PropTypes.array, - debouncedSearch: PropTypes.string, - onSearchChange: PropTypes.func, - setIsSearching: PropTypes.func, -}; - -/** - * UptimeDataTable displays a table of uptime monitors with sorting, searching, and action capabilities - * @param {Object} props - Component props - * @param {boolean} props.isAdmin - Whether the current user has admin privileges - * @param {boolean} props.isLoading - Loading state of the table - * @param {Array<{ - * _id: string, - * url: string, - * title: string, - * percentage: number, - * percentageColor: string, - * monitor: { - * _id: string, - * type: string, - * checks: Array - * } - * }>} props.monitors - Array of monitor objects to display - * @param {number} props.monitorCount - Total count of monitors - * @param {Object} props.sort - Current sort configuration - * @param {string} props.sort.field - Field to sort by - * @param {'asc'|'desc'} props.sort.order - Sort direction - * @param {Function} props.setSort - Callback to update sort configuration - * @param {string} props.search - Current search query - * @param {Function} props.setSearch - Callback to update search query - * @param {boolean} props.isSearching - Whether a search is in progress - * @param {Function} props.setIsSearching - Callback to update search state - * @param {Function} props.setIsLoading - Callback to update loading state - * @param {Function} props.triggerUpdate - Callback to trigger a data refresh - * @returns {JSX.Element} Rendered component - */ -const UptimeDataTable = ({ - isAdmin, - isLoading, - monitors, - filteredMonitors, - monitorCount, - sort, - setSort, - debouncedSearch, - setSearch, - isSearching, - setIsSearching, - setIsLoading, - triggerUpdate, -}) => { - const { determineState } = useUtils(); - - const theme = useTheme(); - const navigate = useNavigate(); - - const handleSort = (field) => { - let order = ""; - if (sort.field !== field) { - order = "desc"; - } else { - order = sort.order === "asc" ? "desc" : "asc"; - } - setSort({ field, order }); - }; - - const headers = [ - { - id: "name", - content: ( - handleSort("name")} - > - Host - - {sort.order === "asc" ? ( - - ) : ( - - )} - - - ), - render: (row) => ( - - ), - }, - { - id: "status", - content: ( - handleSort("status")} - > - {" "} - Status - - {sort.order === "asc" ? ( - - ) : ( - - )} - - - ), - render: (row) => { - const status = determineState(row.monitor); - return ( - - ); - }, - }, - { - id: "responseTime", - content: "Response Time", - render: (row) => , - }, - { - id: "type", - content: "Type", - render: (row) => ( - {row.monitor.type} - ), - }, - { - id: "actions", - content: "Actions", - render: (row) => ( - - ), - }, - ]; - - return ( - - - Uptime monitors - {/* TODO Same as the one in Infrastructure. Create component */} - - {monitorCount} - - - - - - {(isSearching || isLoading) && ( - <> - - - - - - )} - { - navigate(`/uptime/${row.id}`); - }, - emptyView: "No monitors found", - }} - /> - - - ); -}; - -const MemoizedUptimeDataTable = memo(UptimeDataTable); -export default MemoizedUptimeDataTable; - -UptimeDataTable.propTypes = { - isAdmin: PropTypes.bool, - isLoading: PropTypes.bool, - monitors: PropTypes.array, - filteredMonitors: PropTypes.array, - monitorCount: PropTypes.number, - sort: PropTypes.shape({ - field: PropTypes.string, - order: PropTypes.oneOf(["asc", "desc"]), - }), - setSort: PropTypes.func, - debouncedSearch: PropTypes.string, - setSearch: PropTypes.func, - isSearching: PropTypes.bool, - setIsSearching: PropTypes.func, - setIsLoading: PropTypes.func, - triggerUpdate: PropTypes.func, -}; diff --git a/Client/src/Pages/Uptime/Home/actionsMenu.jsx b/Client/src/Pages/Uptime/Home/actionsMenu.jsx deleted file mode 100644 index 493c5f25e..000000000 --- a/Client/src/Pages/Uptime/Home/actionsMenu.jsx +++ /dev/null @@ -1,240 +0,0 @@ -import { useState } from "react"; -import { useSelector, useDispatch } from "react-redux"; -import { useTheme } from "@emotion/react"; -import { useNavigate } from "react-router-dom"; -import { createToast } from "../../../Utils/toastUtils"; -import { logger } from "../../../Utils/Logger"; -import { IconButton, Menu, MenuItem } from "@mui/material"; -import { - deleteUptimeMonitor, - pauseUptimeMonitor, -} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; -import Settings from "../../../assets/icons/settings-bold.svg?react"; -import PropTypes from "prop-types"; -import Dialog from "../../../Components/Dialog"; - -const ActionsMenu = ({ - monitor, - isAdmin, - updateRowCallback, - pauseCallback, - setIsLoading, -}) => { - const [anchorEl, setAnchorEl] = useState(null); - const [actions, setActions] = useState({}); - const [isOpen, setIsOpen] = useState(false); - const dispatch = useDispatch(); - const theme = useTheme(); - const authState = useSelector((state) => state.auth); - const authToken = authState.authToken; - const { isLoading } = useSelector((state) => state.uptimeMonitors); - - const handleRemove = async (event) => { - event.preventDefault(); - event.stopPropagation(); - let monitor = { _id: actions.id }; - const action = await dispatch( - deleteUptimeMonitor({ authToken: authState.authToken, monitor }) - ); - if (action.meta.requestStatus === "fulfilled") { - setIsOpen(false); // close modal - updateRowCallback(); - createToast({ body: "Monitor deleted successfully." }); - } else { - createToast({ body: "Failed to delete monitor." }); - } - }; - - const handlePause = async () => { - try { - setIsLoading(true); - const action = await dispatch( - pauseUptimeMonitor({ authToken, monitorId: monitor._id }) - ); - if (pauseUptimeMonitor.fulfilled.match(action)) { - const state = action?.payload?.data.isActive === false ? "paused" : "resumed"; - createToast({ body: `Monitor ${state} successfully.` }); - pauseCallback(); - } else { - throw new Error(action?.error?.message ?? "Failed to pause monitor."); - } - } catch (error) { - logger.error("Error pausing monitor:", monitor._id, error); - createToast({ body: "Failed to pause monitor." }); - } - }; - - const openMenu = (event, id, url) => { - event.preventDefault(); - event.stopPropagation(); - setAnchorEl(event.currentTarget); - setActions({ id: id, url: url }); - }; - - const openRemove = (e) => { - closeMenu(e); - setIsOpen(true); - }; - - const closeMenu = (e) => { - e.stopPropagation(); - setAnchorEl(null); - }; - - const navigate = useNavigate(); - return ( - <> - { - event.stopPropagation(); - openMenu(event, monitor._id, monitor.type === "ping" ? null : monitor.url); - }} - sx={{ - "&:focus": { - outline: "none", - }, - "& svg path": { - stroke: theme.palette.primary.contrastTextTertiary, - }, - }} - > - - - - closeMenu(e)} - disableScrollLock - slotProps={{ - paper: { - sx: { - "& ul": { - p: theme.spacing(2.5), - backgroundColor: theme.palette.primary.main, - }, - "& li": { m: 0, color: theme.palette.primary.contrastTextSecondary }, - /* - This should not be set automatically on the last of type - "& li:last-of-type": { - color: theme.palette.error.main, - }, */ - }, - }, - }} - > - {actions.url !== null ? ( - { - closeMenu(e); - e.stopPropagation(); - window.open(actions.url, "_blank", "noreferrer"); - }} - > - Open site - - ) : ( - "" - )} - { - e.stopPropagation(); - navigate(`/uptime/${actions.id}`); - }} - > - Details - - {/* TODO - pass monitor id to Incidents page */} - { - e.stopPropagation(); - navigate(`/incidents/${actions.id}`); - }} - > - Incidents - - {isAdmin && ( - { - e.stopPropagation(); - - navigate(`/uptime/configure/${actions.id}`); - }} - > - Configure - - )} - {isAdmin && ( - { - e.stopPropagation(); - navigate(`/uptime/create/${actions.id}`); - }} - > - Clone - - )} - {isAdmin && ( - { - closeMenu(e); - - e.stopPropagation(); - handlePause(e); - }} - > - {monitor?.isActive === true ? "Pause" : "Resume"} - - )} - {isAdmin && ( - { - e.stopPropagation(); - openRemove(e); - }} - sx={{ "&.MuiButtonBase-root": { color: theme.palette.error.main } }} - > - Remove - - )} - - { - e.stopPropagation(); - setIsOpen(false); - }} - confirmationButtonLabel="Delete" - /* Do we need stop propagation? */ - onConfirm={(e) => { - e.stopPropagation(); - handleRemove(e); - }} - isLoading={isLoading} - modelTitle="modal-delete-monitor" - modelDescription="delete-monitor-confirmation" - /> - - ); -}; - -ActionsMenu.propTypes = { - monitor: PropTypes.shape({ - _id: PropTypes.string, - url: PropTypes.string, - type: PropTypes.string, - isActive: PropTypes.bool, - }).isRequired, - isAdmin: PropTypes.bool, - updateRowCallback: PropTypes.func, - pauseCallback: PropTypes.func, - setIsLoading: PropTypes.func, -}; - -export default ActionsMenu; diff --git a/Client/src/Pages/Uptime/Home/fallback.jsx b/Client/src/Pages/Uptime/Home/fallback.jsx deleted file mode 100644 index 2606ca95f..000000000 --- a/Client/src/Pages/Uptime/Home/fallback.jsx +++ /dev/null @@ -1,59 +0,0 @@ -import { Box, Button, Stack, Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import { useNavigate } from "react-router-dom"; -import { useSelector } from "react-redux"; -import PlaceholderLight from "../../../assets/Images/data_placeholder.svg?react"; -import PlaceholderDark from "../../../assets/Images/data_placeholder_dark.svg?react"; -import PropTypes from "prop-types"; - -const Fallback = ({ isAdmin }) => { - const theme = useTheme(); - const navigate = useNavigate(); - const mode = useSelector((state) => state.ui.mode); - - return ( - - - {mode === "light" ? : } - - - No monitors found to display - - - It looks like you don’t have any monitors set up yet. - - {isAdmin && ( - - )} - - ); -}; - -Fallback.propTypes = { - isAdmin: PropTypes.bool, -}; - -export default Fallback; diff --git a/Client/src/Pages/Uptime/Home/host.jsx b/Client/src/Pages/Uptime/Home/host.jsx deleted file mode 100644 index ec78875f4..000000000 --- a/Client/src/Pages/Uptime/Home/host.jsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Box, Typography } from "@mui/material"; -import PropTypes from "prop-types"; -/** - * Host component. - * This subcomponent receives a params object and displays the host details. - * - * @component - * @param {Object} params - An object containing the following properties: - * @param {string} params.url - The URL of the host. - * @param {string} params.title - The name of the host. - * @param {string} params.percentageColor - The color of the percentage text. - * @param {number} params.percentage - The percentage to display. - * @returns {React.ElementType} Returns a div element with the host details. - */ -const Host = ({ url, title, percentageColor, percentage }) => { - const noTitle = title === undefined || title === url; - return ( - - - {title} - - {percentageColor && percentage && ( - - {percentage}% - - )} - {!noTitle && {url}} - - ); -}; - -Host.propTypes = { - title: PropTypes.string, - percentageColor: PropTypes.string, - percentage: PropTypes.string, - url: PropTypes.string, -}; - -export default Host; diff --git a/Client/src/Pages/Uptime/Home/index.css b/Client/src/Pages/Uptime/Home/index.css deleted file mode 100644 index adb1d8674..000000000 --- a/Client/src/Pages/Uptime/Home/index.css +++ /dev/null @@ -1,6 +0,0 @@ -/* TODO take these from here and declare using emotion. Plus, the values should live in the theme */ -.monitors .MuiStack-root > button:not(.MuiIconButton-root) { - font-size: var(--env-var-font-size-medium); - height: var(--env-var-height-2); - min-width: fit-content; -} diff --git a/Client/src/Pages/Uptime/Home/index.jsx b/Client/src/Pages/Uptime/Home/index.jsx index 446702ebb..c300118e0 100644 --- a/Client/src/Pages/Uptime/Home/index.jsx +++ b/Client/src/Pages/Uptime/Home/index.jsx @@ -1,128 +1,70 @@ // Components -import { Box, Stack, Button } from "@mui/material"; +import Breadcrumbs from "../../../Components/Breadcrumbs"; import Greeting from "../../../Utils/greeting"; -import SkeletonLayout from "./skeleton"; -import Fallback from "./fallback"; -import StatusBox from "./StatusBox"; -import UptimeDataTable from "./UptimeDataTable"; +import StatusBoxes from "./Components/StatusBoxes"; +import UptimeDataTable from "./Components/UptimeDataTable"; import Pagination from "../../../Components/Table/TablePagination"; +// MUI Components +import { Stack, Box, Button } from "@mui/material"; + // Utils -import { useTheme } from "@emotion/react"; -import { useEffect, useState, useCallback, useMemo } from "react"; -import { setRowsPerPage } from "../../../Features/UI/uiSlice"; +import { useState, useCallback } from "react"; import { useIsAdmin } from "../../../Hooks/useIsAdmin"; -import { useSelector, useDispatch } from "react-redux"; +import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; -import { createToast } from "../../../Utils/toastUtils"; -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import useDebounce from "../../../Utils/debounce"; -import { networkService } from "../../../main"; +import useMonitorFetch from "./Hooks/useMonitorFetch"; +import { useSelector, useDispatch } from "react-redux"; +import { setRowsPerPage } from "../../../Features/UI/uiSlice"; +import PropTypes from "prop-types"; const BREADCRUMBS = [{ name: `Uptime`, path: "/uptime" }]; +const CreateMonitorButton = ({ shouldRender }) => { + // Utils + const navigate = useNavigate(); + if (shouldRender === false) { + return; + } + + return ( + + + + ); +}; + +CreateMonitorButton.propTypes = { + shouldRender: PropTypes.bool, +}; + const UptimeMonitors = () => { // Redux state + const { authToken, user } = useSelector((state) => state.auth); const rowsPerPage = useSelector((state) => state.ui.monitors.rowsPerPage); + // Local state - const [sort, setSort] = useState({}); const [search, setSearch] = useState(""); const [page, setPage] = useState(0); + const [sort, setSort] = useState({}); const [isSearching, setIsSearching] = useState(false); const [isLoading, setIsLoading] = useState(false); const [monitorUpdateTrigger, setMonitorUpdateTrigger] = useState(false); - const [monitors, setMonitors] = useState([]); - const [filteredMonitors, setFilteredMonitors] = useState([]); - const [monitorsSummary, setMonitorsSummary] = useState({}); // Utils - const debouncedFilter = useDebounce(search, 500); - const dispatch = useDispatch(); const theme = useTheme(); - const navigate = useNavigate(); const isAdmin = useIsAdmin(); + const dispatch = useDispatch(); - const authState = useSelector((state) => state.auth); - - const fetchParams = useMemo( - () => ({ - authToken: authState.authToken, - teamId: authState.user.teamId, - sort: { field: sort.field, order: sort.order }, - filter: debouncedFilter, - page, - rowsPerPage, - }), - [authState.authToken, authState.user.teamId, sort, debouncedFilter, page, rowsPerPage] - ); - - const getMonitorWithPercentage = useCallback((monitor, theme) => { - let uptimePercentage = ""; - let percentageColor = ""; - - if (monitor.uptimePercentage !== undefined) { - uptimePercentage = - monitor.uptimePercentage === 0 - ? "0" - : (monitor.uptimePercentage * 100).toFixed(2); - - percentageColor = - monitor.uptimePercentage < 0.25 - ? theme.palette.error.main - : monitor.uptimePercentage < 0.5 - ? theme.palette.warning.main - : monitor.uptimePercentage < 0.75 - ? theme.palette.success.main - : theme.palette.success.main; - } - - return { - id: monitor._id, - name: monitor.name, - url: monitor.url, - title: monitor.name, - percentage: uptimePercentage, - percentageColor, - monitor: monitor, - }; - }, []); - - const fetchMonitors = useCallback(async () => { - try { - setIsLoading(true); - const config = fetchParams; - const res = await networkService.getMonitorsByTeamId({ - authToken: config.authToken, - teamId: config.teamId, - limit: 25, - types: ["http", "ping", "docker", "port"], - page: config.page, - rowsPerPage: config.rowsPerPage, - filter: config.filter, - field: config.sort.field, - order: config.sort.order, - }); - const { monitors, filteredMonitors, summary } = res.data.data; - const mappedMonitors = filteredMonitors.map((monitor) => - getMonitorWithPercentage(monitor, theme) - ); - setMonitors(monitors); - setFilteredMonitors(mappedMonitors); - setMonitorsSummary(summary); - } catch (error) { - createToast({ - body: error.message, - }); - } finally { - setIsLoading(false); - setIsSearching(false); - } - }, [fetchParams, getMonitorWithPercentage, theme]); - - useEffect(() => { - fetchMonitors(); - }, [fetchMonitors, monitorUpdateTrigger]); - + // Handlers const handleChangePage = (event, newPage) => { setPage(newPage); }; @@ -140,103 +82,58 @@ const UptimeMonitors = () => { const triggerUpdate = useCallback(() => { setMonitorUpdateTrigger((prev) => !prev); }, []); + + const teamId = user.teamId; + + const { monitorsAreLoading, monitors, filteredMonitors, monitorsSummary } = + useMonitorFetch({ + authToken, + teamId, + limit: 25, + page, + rowsPerPage: rowsPerPage, + filter: search, + field: sort.field, + order: sort.order, + triggerUpdate: monitorUpdateTrigger, + }); const totalMonitors = monitorsSummary?.totalMonitors ?? 0; - const hasMonitors = totalMonitors > 0; - const canAddMonitor = isAdmin && hasMonitors; return ( - - - - {canAddMonitor && ( - <> - - - )} - - - - { - <> - {!isLoading && !hasMonitors && } - {isLoading ? ( - - ) : ( - hasMonitors && ( - <> - - - - - - - - - ) - )} - - } + + + + + + ); }; diff --git a/Client/src/Pages/Uptime/Home/skeleton.jsx b/Client/src/Pages/Uptime/Home/skeleton.jsx deleted file mode 100644 index 4e55be536..000000000 --- a/Client/src/Pages/Uptime/Home/skeleton.jsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Skeleton, Stack } from "@mui/material"; -import { useTheme } from "@emotion/react"; - -/** - * Renders a skeleton layout. - * - * @returns {JSX.Element} - */ -const SkeletonLayout = () => { - const theme = useTheme(); - - return ( - <> - - - - - - - - - - - - ); -}; - -export default SkeletonLayout; diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index 86c0fb840..ccd8ec26f 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -436,7 +436,7 @@ class MonitorController { monitor.isActive = !monitor.isActive; monitor.status = undefined; monitor.save(); - return res.ssuccess({ + return res.success({ msg: monitor.isActive ? successMessages.MONITOR_RESUME : successMessages.MONITOR_PAUSE, diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 4c60e0f22..cad9bd68f 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -497,7 +497,7 @@ const getMonitorById = async (monitorId) => { const getMonitorsByTeamId = async (req) => { let { limit, type, page, rowsPerPage, filter, field, order } = req.query; - + console.log("req.query", req.query); limit = parseInt(limit); page = parseInt(page); rowsPerPage = parseInt(rowsPerPage); @@ -512,8 +512,16 @@ const getMonitorsByTeamId = async (req) => { } const skip = page && rowsPerPage ? page * rowsPerPage : 0; - const sort = { [field]: order === "asc" ? 1 : -1 }; + + console.log("limit", limit); + console.log("page", page); + console.log("rowsPerPage", rowsPerPage); + console.log("filter", filter); + console.log("field", field); + console.log("order", order); + console.log("skip", skip); + console.log("sort", sort); const results = await Monitor.aggregate([ { $match: matchStage }, { @@ -675,6 +683,7 @@ const getMonitorsByTeamId = async (req) => { ]); let { monitors, filteredMonitors, summary } = results[0]; + console.log("filteredMonitors", filteredMonitors); filteredMonitors = filteredMonitors.map((monitor) => { if (!monitor.checks) { return monitor; @@ -839,3 +848,22 @@ export { groupChecksByTime, calculateGroupStats, }; + +// limit 25 +// page 1 +// rowsPerPage 25 +// filter undefined +// field name +// order asc +// skip 25 +// sort { name: 1 } +// filteredMonitors [] + +// limit 25 +// page NaN +// rowsPerPage 25 +// filter undefined +// field name +// order asc +// skip 0 +// sort { name: 1 } From be6d2f25ea29e984d43088ec33cc865a68333448 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 24 Jan 2025 16:22:14 -0800 Subject: [PATCH 4/5] rationalize host component --- .../Uptime/Home/Components/Host/index.jsx | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/Client/src/Pages/Uptime/Home/Components/Host/index.jsx b/Client/src/Pages/Uptime/Home/Components/Host/index.jsx index ec78875f4..9238f9889 100644 --- a/Client/src/Pages/Uptime/Home/Components/Host/index.jsx +++ b/Client/src/Pages/Uptime/Home/Components/Host/index.jsx @@ -1,5 +1,6 @@ -import { Box, Typography } from "@mui/material"; +import { Stack, Box, Typography } from "@mui/material"; import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; /** * Host component. * This subcomponent receives a params object and displays the host details. @@ -13,44 +14,43 @@ import PropTypes from "prop-types"; * @returns {React.ElementType} Returns a div element with the host details. */ const Host = ({ url, title, percentageColor, percentage }) => { - const noTitle = title === undefined || title === url; + const theme = useTheme(); + console.log(url, title); return ( - - + {title} - - {percentageColor && percentage && ( - - {percentage}% - - )} - {!noTitle && {url}} - + {percentageColor && percentage && ( + <> + + + {percentage}% + + + )} + + {url} + ); }; From f19ae024b944b4490115d72739d9639b2d9da34a Mon Sep 17 00:00:00 2001 From: Aryaman Kumar Sharma Date: Mon, 27 Jan 2025 16:59:54 +0530 Subject: [PATCH 5/5] Fix: uptime-http issues based on fix/fe/uptime-refactor --- .../Pages/Uptime/Home/Components/UptimeDataTable/index.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Client/src/Pages/Uptime/Home/Components/UptimeDataTable/index.jsx b/Client/src/Pages/Uptime/Home/Components/UptimeDataTable/index.jsx index eaf08046d..a13b54613 100644 --- a/Client/src/Pages/Uptime/Home/Components/UptimeDataTable/index.jsx +++ b/Client/src/Pages/Uptime/Home/Components/UptimeDataTable/index.jsx @@ -190,7 +190,9 @@ const UptimeDataTable = (props) => { id: "type", content: "Type", render: (row) => ( - {row.monitor.type} + + {row.monitor.type === "http" ? "HTTP(s)" : row.monitor.type} + ), }, {