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": {}
+}