diff --git a/Client/src/Components/Inputs/Search/index.jsx b/Client/src/Components/Inputs/Search/index.jsx index a71732e3f..3de2cbe07 100644 --- a/Client/src/Components/Inputs/Search/index.jsx +++ b/Client/src/Components/Inputs/Search/index.jsx @@ -38,6 +38,7 @@ const SearchAdornment = () => { ); }; +//TODO keep search state inside of component const Search = ({ id, options, diff --git a/Client/src/Pages/Incidents/IncidentTable/Empty/Empty.jsx b/Client/src/Pages/Incidents/IncidentTable/Empty/Empty.jsx new file mode 100644 index 000000000..b8f6a0986 --- /dev/null +++ b/Client/src/Pages/Incidents/IncidentTable/Empty/Empty.jsx @@ -0,0 +1,32 @@ +import { useTheme } from "@emotion/react"; +import PlaceholderLight from "../../../../assets/Images/data_placeholder.svg?react"; +import PlaceholderDark from "../../../../assets/Images/data_placeholder_dark.svg?react"; +import { Box, Typography } from "@mui/material"; +import PropTypes from "prop-types"; + +const Empty = ({ styles, mode }) => { + const theme = useTheme(); + return ( + + + {mode === "light" ? : } + + + No incidents recorded yet. + + + ); +}; + +Empty.propTypes = { + styles: PropTypes.object, + mode: PropTypes.string, +}; + +export { Empty }; diff --git a/Client/src/Pages/Incidents/IncidentTable/Skeleton/Skeleton.jsx b/Client/src/Pages/Incidents/IncidentTable/Skeleton/Skeleton.jsx new file mode 100644 index 000000000..a3a23c14c --- /dev/null +++ b/Client/src/Pages/Incidents/IncidentTable/Skeleton/Skeleton.jsx @@ -0,0 +1,21 @@ +import { Skeleton /* , Stack */ } from "@mui/material"; +const IncidentSkeleton = () => { + return ( + <> + + + + ); +}; + +export { IncidentSkeleton }; diff --git a/Client/src/Pages/Incidents/IncidentTable/index.jsx b/Client/src/Pages/Incidents/IncidentTable/index.jsx index 1558084a4..da533c7ba 100644 --- a/Client/src/Pages/Incidents/IncidentTable/index.jsx +++ b/Client/src/Pages/Incidents/IncidentTable/index.jsx @@ -24,6 +24,8 @@ import { useTheme } from "@emotion/react"; import { formatDateWithTz } from "../../../Utils/timeUtils"; import PlaceholderLight from "../../../assets/Images/data_placeholder.svg?react"; import PlaceholderDark from "../../../assets/Images/data_placeholder_dark.svg?react"; +import { Empty } from "./Empty/Empty"; +import { IncidentSkeleton } from "./Skeleton/Skeleton"; const IncidentTable = ({ monitors, selectedMonitor, filter }) => { const uiTimezone = useSelector((state) => state.ui.timezone); @@ -37,6 +39,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { page: 0, rowsPerPage: 14, }); + const [isLoading, setIsLoading] = useState(true); useEffect(() => { setPaginationController((prevPaginationController) => ({ @@ -51,6 +54,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { return; } try { + setIsLoading(true); let res; if (selectedMonitor === "0") { res = await networkService.getChecksByTeam({ @@ -79,6 +83,8 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { setChecksCount(res.data.data.checksCount); } catch (error) { logger.error(error); + } finally { + setIsLoading(false); } }; fetchPage(); @@ -129,24 +135,20 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { p: theme.spacing(30), }; + const hasChecks = checks?.length === 0; + const noIncidentsRecordedYet = hasChecks && selectedMonitor === "0"; + const noIncidentsForThatMonitor = hasChecks && selectedMonitor !== "0"; + return ( <> - {checks?.length === 0 && selectedMonitor === "0" ? ( - - - {mode === "light" ? : } - - - No incidents recorded yet. - - - ) : checks?.length === 0 ? ( + {isLoading ? ( + + ) : noIncidentsRecordedYet ? ( + + ) : noIncidentsForThatMonitor ? ( { const [monitors, setMonitors] = useState({}); const [selectedMonitor, setSelectedMonitor] = useState("0"); - const [loading, setLoading] = useState(false); + const [isLoading, setIsLoading] = useState(true); // TODO do something with these filters const [filter, setFilter] = useState("all"); useEffect(() => { const fetchMonitors = async () => { - setLoading(true); - const res = await networkService.getMonitorsByTeamId({ - authToken: authState.authToken, - teamId: authState.user.teamId, - limit: -1, - types: null, - status: null, - checkOrder: null, - normalize: null, - page: null, - rowsPerPage: null, - filter: null, - field: null, - order: null, - }); - // Reduce to a lookup object for 0(1) lookup - if (res?.data?.data?.monitors?.length > 0) { - const monitorLookup = res.data.data.monitors.reduce((acc, monitor) => { - acc[monitor._id] = monitor; - return acc; - }, {}); - setMonitors(monitorLookup); - monitorId !== undefined && setSelectedMonitor(monitorId); + try { + setIsLoading(true); + const res = await networkService.getMonitorsByTeamId({ + authToken: authState.authToken, + teamId: authState.user.teamId, + limit: -1, + types: null, + status: null, + checkOrder: null, + normalize: null, + page: null, + rowsPerPage: null, + filter: null, + field: null, + order: null, + }); + // Reduce to a lookup object for 0(1) lookup + if (res?.data?.data?.monitors?.length > 0) { + const monitorLookup = res.data.data.monitors.reduce((acc, monitor) => { + acc[monitor._id] = monitor; + return acc; + }, {}); + setMonitors(monitorLookup); + monitorId !== undefined && setSelectedMonitor(monitorId); + } + } catch (error) { + console.info(error); + } finally { + setIsLoading(false); } - setLoading(false); }; - fetchMonitors(); }, [authState]); - useEffect(() => {}, []); + useEffect(() => {}, [monitors]); const handleSelect = (event) => { setSelectedMonitor(event.target.value); }; + const isActuallyLoading = isLoading && Object.keys(monitors)?.length === 0; + return ( - {loading ? ( + {isActuallyLoading ? ( ) : ( <> diff --git a/Client/src/Pages/Monitors/Home/CurrentMonitoring/index.jsx b/Client/src/Pages/Monitors/Home/CurrentMonitoring/index.jsx new file mode 100644 index 000000000..613c280a4 --- /dev/null +++ b/Client/src/Pages/Monitors/Home/CurrentMonitoring/index.jsx @@ -0,0 +1,79 @@ +import { useTheme } from "@emotion/react"; +import { Box, Stack, Typography } from "@mui/material"; +import Search from "../../../../Components/Inputs/Search"; +import MemoizedMonitorTable from "../MonitorTable"; +import { useState } from "react"; +import useDebounce from "../../../../Utils/debounce"; +import PropTypes from "prop-types"; + +const CurrentMonitoring = ({ totalMonitors, monitors, isAdmin }) => { + const theme = useTheme(); + const [search, setSearch] = useState(""); + const [isSearching, setIsSearching] = useState(false); + const debouncedFilter = useDebounce(search, 500); + const handleSearch = (value) => { + setIsSearching(true); + setSearch(value); + }; + return ( + + + + Actively monitoring + + + {totalMonitors} + + + + + + + + ); +}; + +CurrentMonitoring.propTypes = { + totalMonitors: PropTypes.number, + monitors: PropTypes.array, + isAdmin: PropTypes.bool, +}; + +export { CurrentMonitoring }; diff --git a/Client/src/Pages/Monitors/Home/MonitorTable/Skeleton/index.jsx b/Client/src/Pages/Monitors/Home/MonitorTable/Skeleton/index.jsx new file mode 100644 index 000000000..e30e0a255 --- /dev/null +++ b/Client/src/Pages/Monitors/Home/MonitorTable/Skeleton/index.jsx @@ -0,0 +1,31 @@ +import { Skeleton, TableCell, TableRow } from "@mui/material"; +const ROWS_NUMBER = 7; +const ROWS_ARRAY = Array.from({ length: ROWS_NUMBER }, (_, i) => i); + +const TableBodySkeleton = () => { + return ( + <> + {ROWS_ARRAY.map((row) => ( + + + + + + + + + + + + + + + + + + ))} + + ); +}; + +export { TableBodySkeleton }; diff --git a/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx b/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx index 4b0765b76..d9fbc34b2 100644 --- a/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx +++ b/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx @@ -12,6 +12,7 @@ import { Stack, Typography, Button, + CircularProgress, } from "@mui/material"; import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded"; import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded"; @@ -34,6 +35,7 @@ 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). @@ -107,17 +109,17 @@ TablePaginationActions.propTypes = { onPageChange: PropTypes.func.isRequired, }; -const MonitorTable = ({ isAdmin, filter, setLoading }) => { +const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => { const theme = useTheme(); const navigate = useNavigate(); const dispatch = useDispatch(); const { determineState } = useUtils(); 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); - const authState = useSelector((state) => state.auth); const [updateTrigger, setUpdateTrigger] = useState(false); const [sort, setSort] = useState({}); const prevFilter = useRef(filter); @@ -160,15 +162,25 @@ const MonitorTable = ({ isAdmin, filter, setLoading }) => { }); setMonitors(res?.data?.data?.monitors ?? []); setMonitorCount(res?.data?.data?.monitorCount ?? 0); - setLoading(false); } catch (error) { logger.error(error); + } finally { + setIsSearching(false); } - }, [authState, page, rowsPerPage, filter, sort, setLoading]); + }, [authState, page, rowsPerPage, filter, sort, setIsSearching]); useEffect(() => { fetchPage(); - }, [updateTrigger, authState, page, rowsPerPage, filter, sort, setLoading, fetchPage]); + }, [ + updateTrigger, + authState, + page, + rowsPerPage, + filter, + sort, + setIsSearching, + fetchPage, + ]); // Listen for changes in filter, if new value reset the page useEffect(() => { @@ -220,7 +232,37 @@ const MonitorTable = ({ isAdmin, filter, setLoading }) => { }; return ( - <> + + {isSearching && ( + <> + + + + + + )} @@ -271,77 +313,82 @@ const MonitorTable = ({ isAdmin, filter, setLoading }) => { - {monitors.map((monitor) => { - let uptimePercentage = ""; - let percentageColor = theme.palette.percentage.uptimeExcellent; + {/* TODO add empty state. Check if is searching, and empty => skeleton. Is empty, not searching => skeleton */} + {monitors.length > 0 ? ( + monitors.map((monitor) => { + let uptimePercentage = ""; + let percentageColor = theme.palette.percentage.uptimeExcellent; - // Determine uptime percentage and color based on the monitor's uptimePercentage value - if (monitor.uptimePercentage !== undefined) { - uptimePercentage = - monitor.uptimePercentage === 0 - ? "0" - : (monitor.uptimePercentage * 100).toFixed(2); + // Determine uptime percentage and color based on the monitor's uptimePercentage value + if (monitor.uptimePercentage !== undefined) { + uptimePercentage = + monitor.uptimePercentage === 0 + ? "0" + : (monitor.uptimePercentage * 100).toFixed(2); - percentageColor = - monitor.uptimePercentage < 0.25 - ? theme.palette.percentage.uptimePoor - : monitor.uptimePercentage < 0.5 - ? theme.palette.percentage.uptimeFair - : monitor.uptimePercentage < 0.75 - ? theme.palette.percentage.uptimeGood - : theme.palette.percentage.uptimeExcellent; - } + percentageColor = + monitor.uptimePercentage < 0.25 + ? theme.palette.percentage.uptimePoor + : monitor.uptimePercentage < 0.5 + ? theme.palette.percentage.uptimeFair + : monitor.uptimePercentage < 0.75 + ? theme.palette.percentage.uptimeGood + : theme.palette.percentage.uptimeExcellent; + } - const params = { - url: monitor.url, - title: monitor.name, - percentage: uptimePercentage, - percentageColor, - status: determineState(monitor), - }; + const params = { + url: monitor.url, + title: monitor.name, + percentage: uptimePercentage, + percentageColor, + status: determineState(monitor), + }; - return ( - { - navigate(`/monitors/${monitor._id}`); - }} - > - - - - - - - - - - - {monitor.type} - - - - - - ); - })} + return ( + { + navigate(`/monitors/${monitor._id}`); + }} + > + + + + + + + + + + + {monitor.type} + + + + + + ); + }) + ) : ( + + )}
@@ -415,14 +462,15 @@ const MonitorTable = ({ isAdmin, filter, setLoading }) => { }} />
- +
); }; MonitorTable.propTypes = { isAdmin: PropTypes.bool, filter: PropTypes.string, - setLoading: PropTypes.func, + setIsSearching: PropTypes.func, + isSearching: PropTypes.bool, }; const MemoizedMonitorTable = memo(MonitorTable); diff --git a/Client/src/Pages/Monitors/Home/index.jsx b/Client/src/Pages/Monitors/Home/index.jsx index 0a3c1c912..345a0a88e 100644 --- a/Client/src/Pages/Monitors/Home/index.jsx +++ b/Client/src/Pages/Monitors/Home/index.jsx @@ -1,79 +1,74 @@ import "./index.css"; -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import { useSelector, useDispatch } from "react-redux"; import { getUptimeMonitorsByTeamId } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; import { useNavigate } from "react-router-dom"; import { useTheme } from "@emotion/react"; -import { Box, Button, CircularProgress, Stack, Typography } from "@mui/material"; +import { Box, Button, Stack } from "@mui/material"; import PropTypes from "prop-types"; import SkeletonLayout from "./skeleton"; import Fallback from "./fallback"; import StatusBox from "./StatusBox"; import Breadcrumbs from "../../../Components/Breadcrumbs"; import Greeting from "../../../Utils/greeting"; -import MonitorTable from "./MonitorTable"; -import Search from "../../../Components/Inputs/Search"; -import useDebounce from "../../../Utils/debounce"; +import { CurrentMonitoring } from "./CurrentMonitoring"; const Monitors = ({ isAdmin }) => { const theme = useTheme(); const navigate = useNavigate(); const monitorState = useSelector((state) => state.uptimeMonitors); const authState = useSelector((state) => state.auth); - const [search, setSearch] = useState(""); - const [isSearching, setIsSearching] = useState(false); const dispatch = useDispatch({}); - const debouncedFilter = useDebounce(search, 500); - - const handleSearch = (value) => { - setIsSearching(true); - setSearch(value); - }; useEffect(() => { dispatch(getUptimeMonitorsByTeamId(authState.authToken)); }, [authState.authToken, dispatch]); - let loading = - monitorState?.isLoading && monitorState?.monitorsSummary?.monitors?.length === 0; + //TODO bring fetching to this component, like on pageSpeed + + const loading = monitorState?.isLoading; + + const totalMonitors = monitorState?.monitorsSummary?.monitorCounts?.total; + + const hasMonitors = totalMonitors > 0; + const noMonitors = !hasMonitors; + const canAddMonitor = isAdmin && hasMonitors; return ( + + + + + {canAddMonitor && ( + + )} + + + {noMonitors && } {loading ? ( ) : ( <> - - - - - {isAdmin && monitorState?.monitorsSummary?.monitors?.length !== 0 && ( - - )} - - - {isAdmin && monitorState?.monitorsSummary?.monitors?.length === 0 && ( - - )} - - {monitorState?.monitorsSummary?.monitors?.length !== 0 && ( + {hasMonitors && ( <> { value={monitorState?.monitorsSummary?.monitorCounts?.paused ?? 0} /> - - - - Actively monitoring - - - {monitorState?.monitorsSummary?.monitorCounts?.total || 0} - - - - - - - {isSearching && ( - <> - - - - - - )} - - - + )} diff --git a/Client/src/Pages/PageSpeed/index.jsx b/Client/src/Pages/PageSpeed/index.jsx index 8ecc4392f..30e11b2d7 100644 --- a/Client/src/Pages/PageSpeed/index.jsx +++ b/Client/src/Pages/PageSpeed/index.jsx @@ -19,7 +19,7 @@ const PageSpeed = ({ isAdmin }) => { const navigate = useNavigate(); const { user, authToken } = useSelector((state) => state.auth); - const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(true); const [monitors, setMonitors] = useState([]); useEffect(() => { dispatch(getPageSpeedByTeamId(authToken)); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..f71a78c1f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "bluewave-uptime", + "lockfileVersion": 3, + "requires": true, + "packages": {} +}