diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
new file mode 100644
index 000000000..c884b2e12
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
@@ -0,0 +1,9 @@
+## Describe your changes
+
+## Issue Number if applicable
+
+## Checklist before requesting a review
+
+- [ ] I have performed a self-review of my code
+- [ ] I have inlcuded the issue # in the PR if applicable
+- [ ] I have labelled the PR correctly
diff --git a/Client/src/Components/BasicTable/index.css b/Client/src/Components/BasicTable/index.css
index 14adc440c..604dc412e 100644
--- a/Client/src/Components/BasicTable/index.css
+++ b/Client/src/Components/BasicTable/index.css
@@ -10,7 +10,6 @@
}
.MuiTable-root .host div:nth-child(2) {
width: fit-content;
- font-weight: 700;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
@@ -125,3 +124,68 @@
.MuiPaper-root + .MuiPagination-root div.MuiPaginationItem-root {
user-select: none;
}
+
+.MuiTablePagination-root p {
+ color: var(--env-var-color-2);
+ font-weight: 500;
+}
+.MuiTablePagination-root button,
+.MuiTablePagination-root .MuiSelect-select {
+ border: solid 1px #ebebeb;
+ border-radius: var(--env-var-radius-1);
+}
+.MuiTablePagination-root .MuiTablePagination-select.MuiSelect-select {
+ text-align: left;
+ text-align-last: left;
+}
+.MuiTablePagination-root button {
+ min-width: 0;
+ padding: 4px;
+ margin-left: 5px;
+}
+.MuiTablePagination-root svg {
+ width: 22px;
+ height: 22px;
+}
+.MuiTablePagination-root svg path {
+ stroke: var(--env-var-color-2);
+ stroke-width: 1.3;
+}
+.MuiTablePagination-root .MuiSelect-icon {
+ width: 16px;
+ height: 16px;
+ top: 50%;
+ right: 8%;
+ transform: translateY(-50%);
+}
+.MuiTablePagination-root button.Mui-disabled {
+ opacity: 0.4;
+}
+.MuiTablePagination-root button:not(.Mui-disabled):hover {
+ background-color: #f4f4f4;
+ border-color: #f4f4f4;
+}
+
+.pagination-dropdown.MuiPaper-root {
+ box-shadow: var(--env-var-shadow-1);
+ border: solid 1px var(--env-var-color-6);
+ border-radius: var(--env-var-radius-1);
+}
+.pagination-dropdown ul {
+ padding: 4px;
+ min-width: 100px;
+}
+.pagination-dropdown li,
+body:has(.pagination-dropdown) p:has(+ .MuiTablePagination-root) {
+ font-size: var(--env-var-font-size-medium);
+ color: var(--env-var-color-2);
+ padding: 4px;
+}
+.pagination-dropdown li.Mui-selected,
+.pagination-dropdown li.Mui-selected.Mui-focusVisible,
+.pagination-dropdown li.Mui-selected:hover {
+ background-color: #f4f4f4;
+}
+body:has(.pagination-dropdown) p:has(+ .MuiTablePagination-root) {
+ font-size: var(--env-var-font-size-small-plus);
+}
diff --git a/Client/src/Components/BasicTable/index.jsx b/Client/src/Components/BasicTable/index.jsx
index 76f1c61c7..8b6cafd91 100644
--- a/Client/src/Components/BasicTable/index.jsx
+++ b/Client/src/Components/BasicTable/index.jsx
@@ -1,3 +1,6 @@
+import PropTypes from "prop-types";
+import { useState, useEffect } from "react";
+import { useTheme } from "@emotion/react";
import {
TableContainer,
Paper,
@@ -6,14 +9,86 @@ import {
TableRow,
TableCell,
TableBody,
- Pagination,
- PaginationItem,
+ TablePagination,
+ Box,
+ Typography,
+ Stack,
} from "@mui/material";
-import React, { useState, useEffect } from "react";
-import PropTypes from "prop-types";
+import { useDispatch, useSelector } from "react-redux";
+import { useLocation, useParams } from "react-router-dom";
+import { setRowsPerPage } from "../../Features/UI/uiSlice";
+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 Button from "../Button";
import "./index.css";
-import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
-import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded";
+/**
+ * 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 (
+
+ }
+ onClick={handleFirstPageButtonClick}
+ disabled={page === 0}
+ aria-label="first page"
+ />
+ }
+ onClick={handleBackButtonClick}
+ disabled={page === 0}
+ aria-label="previous page"
+ />
+ }
+ onClick={handleNextButtonClick}
+ disabled={page >= Math.ceil(count / rowsPerPage) - 1}
+ aria-label="next page"
+ />
+ }
+ onClick={handleLastPageButtonClick}
+ disabled={page >= Math.ceil(count / rowsPerPage) - 1}
+ aria-label="last page"
+ />
+
+ );
+};
/**
* BasicTable Component
@@ -68,9 +143,13 @@ import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded";
*
*/
-const BasicTable = ({ data, paginated, reversed, rowsPerPage = 5 }) => {
- // Add headers to props validation
-
+const BasicTable = ({ data, paginated, reversed }) => {
+ const theme = useTheme();
+ const location = useLocation();
+ const dispatch = useDispatch();
+ const uiState = useSelector((state) => state.ui);
+ let table = location.pathname.split("/").pop();
+ let rowsPerPage = uiState[table].rowsPerPage;
const [page, setPage] = useState(0);
useEffect(() => {
@@ -81,6 +160,16 @@ const BasicTable = ({ data, paginated, reversed, rowsPerPage = 5 }) => {
setPage(newPage);
};
+ const handleChangeRowsPerPage = (event) => {
+ dispatch(
+ setRowsPerPage({
+ value: parseInt(event.target.value, 10),
+ table: table,
+ })
+ );
+ setPage(0);
+ };
+
let displayData = [];
if (data && data.rows) {
@@ -94,35 +183,15 @@ const BasicTable = ({ data, paginated, reversed, rowsPerPage = 5 }) => {
return
No data
;
}
- let paginationComponent = <>>;
- if (paginated === true && data.rows.length > rowsPerPage) {
- paginationComponent = (
- handleChangePage(event, value - 1)}
- shape="rounded"
- renderItem={(item) => (
-
- )}
- />
- );
- }
+ /**
+ * 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, data.rows.length);
+ return `${start} - ${end}`;
+ };
return (
<>
@@ -154,8 +223,59 @@ const BasicTable = ({ data, paginated, reversed, rowsPerPage = 5 }) => {
-
- {paginationComponent}
+ {paginated && (
+
+
+ Showing {getRange()} of {data.rows.length} monitor(s)
+
+
+ `Page ${page + 1} of ${Math.max(
+ 0,
+ Math.ceil(count / rowsPerPage)
+ )}`
+ }
+ slotProps={{
+ select: {
+ MenuProps: {
+ keepMounted: true,
+ PaperProps: {
+ className: "pagination-dropdown",
+ },
+ transformOrigin: { vertical: "bottom", horizontal: "left" },
+ anchorOrigin: { vertical: "top", horizontal: "left" },
+ sx: { mt: "-4px" },
+ },
+ inputProps: { id: "pagination-dropdown" },
+ IconComponent: SelectorVertical,
+ sx: {
+ ml: theme.gap.small,
+ mr: theme.gap.large,
+ minWidth: theme.gap.xl,
+ textAlign: "left",
+ "&.Mui-focused > div": {
+ backgroundColor: theme.palette.otherColors.white,
+ },
+ },
+ },
+ }}
+ sx={{ mt: theme.gap.medium }}
+ />
+
+ )}
>
);
};
diff --git a/Client/src/Components/Breadcrumbs/index.css b/Client/src/Components/Breadcrumbs/index.css
new file mode 100644
index 000000000..b7d190de1
--- /dev/null
+++ b/Client/src/Components/Breadcrumbs/index.css
@@ -0,0 +1,31 @@
+.MuiBreadcrumbs-root {
+ height: 36px;
+}
+.MuiBreadcrumbs-root svg {
+ width: 16px;
+ height: 16px;
+}
+.MuiBreadcrumbs-root .MuiBreadcrumbs-li a {
+ font-size: var(--env-var-font-size-medium);
+ color: var(--env-var-color-2);
+ opacity: 0.8;
+ font-weight: 400;
+}
+.MuiBreadcrumbs-root .MuiBreadcrumbs-li:hover a {
+ color: var(--env-var-color-2);
+}
+.MuiBreadcrumbs-root .MuiBreadcrumbs-li:not(:last-child):hover a {
+ opacity: 1;
+ background-color: #e3e3e3;
+}
+.MuiBreadcrumbs-root .MuiBreadcrumbs-li:not(:last-child) {
+ cursor: pointer;
+}
+.MuiBreadcrumbs-root .MuiBreadcrumbs-li:last-child a {
+ font-weight: 500;
+ opacity: 1;
+ cursor: default;
+}
+.MuiBreadcrumbs-root .MuiBreadcrumbs-separator {
+ margin: 0;
+}
diff --git a/Client/src/Components/Breadcrumbs/index.jsx b/Client/src/Components/Breadcrumbs/index.jsx
new file mode 100644
index 000000000..506263fce
--- /dev/null
+++ b/Client/src/Components/Breadcrumbs/index.jsx
@@ -0,0 +1,63 @@
+import PropTypes from "prop-types";
+import { Box, Breadcrumbs as MUIBreadcrumbs } from "@mui/material";
+import { useTheme } from "@emotion/react";
+import { useNavigate } from "react-router";
+import ArrowRight from "../../assets/icons/right-arrow.svg?react";
+
+import "./index.css";
+
+/**
+ * Breadcrumbs component that displays a list of breadcrumb items.
+ *
+ * @param {Object} props
+ * @param {Array} props.list - Array of breadcrumb items. Each item should have `name` and `path` properties.
+ * @param {string} props.list.name - The name to display for the breadcrumb.
+ * @param {string} props.list.path - The path to navigate to when the breadcrumb is clicked.
+ *
+ * @returns {JSX.Element} The rendered Breadcrumbs component.
+ */
+
+const Breadcrumbs = ({ list }) => {
+ const theme = useTheme();
+ const navigate = useNavigate();
+
+ return (
+ }
+ aria-label="breadcrumb"
+ p={theme.gap.small}
+ width="fit-content"
+ backgroundColor={theme.palette.otherColors.fillGray}
+ borderRadius={`${theme.shape.borderRadius}px`}
+ lineHeight="18px"
+ >
+ {list.map((item, index) => {
+ return (
+ navigate(item.path)}
+ >
+ {item.name}
+
+ );
+ })}
+
+ );
+};
+
+Breadcrumbs.propTypes = {
+ list: PropTypes.arrayOf(
+ PropTypes.shape({
+ name: PropTypes.string.isRequired,
+ path: PropTypes.string.isRequired,
+ }).isRequired
+ ).isRequired,
+};
+
+export default Breadcrumbs;
diff --git a/Client/src/Components/Charts/Servers/serverStatus.css b/Client/src/Components/Charts/Servers/serverStatus.css
index dd15e3e9b..fcbf470aa 100644
--- a/Client/src/Components/Charts/Servers/serverStatus.css
+++ b/Client/src/Components/Charts/Servers/serverStatus.css
@@ -22,9 +22,10 @@
.server-status-tile-title {
font-size: var(--font-size-0);
- font-weight: bold;
+ font-weight: 500;
margin-bottom: calc(var(--spacing-general-1) / 2);
- color: var(--env-var-color-1);
+ color: var(--env-var-color-5);
+ opacity: 0.6;
}
.server-status-tile-value {
diff --git a/Client/src/Components/Inputs/Select/index.css b/Client/src/Components/Inputs/Select/index.css
index 1952feb43..be1a92503 100644
--- a/Client/src/Components/Inputs/Select/index.css
+++ b/Client/src/Components/Inputs/Select/index.css
@@ -15,7 +15,7 @@
font-size: var(--env-var-font-size-medium);
color: var(--env-var-color-5);
}
-.select-wrapper h3.MuiTypography-root{
+.select-wrapper h3.MuiTypography-root {
font-weight: 500;
}
.MuiInputBase-root:has(> .MuiSelect-select) fieldset {
@@ -24,3 +24,23 @@
.MuiInputBase-root:not(.Mui-focused):has(> .MuiSelect-select):hover fieldset {
border-color: var(--env-var-color-29);
}
+
+.select-dropdown.MuiPaper-root {
+ box-shadow: var(--env-var-shadow-1);
+ border: solid 1px var(--env-var-color-6);
+ border-radius: var(--env-var-radius-1);
+}
+.select-dropdown .MuiList-root {
+ min-width: 100px;
+}
+.select-dropdown li {
+ font-size: var(--env-var-font-size-medium);
+ color: var(--env-var-color-5);
+ padding: 5px 8px;
+ border-radius: var(--env-var-radius-1);
+}
+.select-dropdown li.Mui-selected,
+.select-dropdown li.Mui-selected.Mui-focusVisible,
+.select-dropdown li.Mui-selected:hover {
+ background-color: #f4f4f4;
+}
diff --git a/Client/src/Components/Sidebar/index.css b/Client/src/Components/Sidebar/index.css
index 63c365742..10cdfc77d 100644
--- a/Client/src/Components/Sidebar/index.css
+++ b/Client/src/Components/Sidebar/index.css
@@ -1,4 +1,5 @@
aside .selected-path,
+.sidebar-popup .selected-path,
aside .MuiListItemButton-root:hover {
background-color: var(--env-var-color-13);
}
@@ -52,12 +53,12 @@ aside
> .MuiListItemButton-root::before {
content: "";
position: absolute;
- left: -8px;
+ left: -7px;
top: 0;
height: 100%;
width: 100%;
border-left: solid 1px var(--env-var-color-5);
- opacity: 0.3;
+ opacity: 0.2;
}
aside
.MuiCollapse-wrapperInner
@@ -65,15 +66,59 @@ aside
> .MuiListItemButton-root:last-child::before {
height: 50%;
}
+aside .MuiCollapse-wrapperInner .MuiList-root svg,
+aside .MuiList-root .MuiListItemText-root + svg {
+ width: 18px;
+ height: 18px;
+}
aside .MuiCollapse-wrapperInner .MuiList-root > .MuiListItemButton-root::after {
content: "";
position: absolute;
left: -8px;
top: 50%;
- height: 1px;
- width: 7px;
+ transform: translateY(-50%);
+ height: 3px;
+ width: 3px;
border-radius: 50%;
- background-color: var(--env-var-color-5);
- opacity: 0.3;
+ background-color: #d6d9dd;
+}
+
+aside {
+ transition: flex 300ms ease-out;
+}
+
+.sidebar-popup .MuiPaper-root {
+ box-shadow: var(--env-var-shadow-1);
+ border: solid 1px var(--env-var-color-6);
+ border-radius: var(--env-var-radius-1);
+ gap: 1px;
+}
+.sidebar-popup .MuiList-root {
+ padding: 6px 6px;
+ min-width: 100px;
+}
+.sidebar-popup li.MuiButtonBase-root {
+ font-size: var(--env-var-font-size-medium);
+ color: var(--env-var-color-5);
+ padding: 5px 8px;
+ border-radius: var(--env-var-radius-1);
+}
+.sidebar-popup li.MuiButtonBase-root:hover{
+ background-color: var(--env-var-color-13);
+}
+.sidebar-popup li.MuiButtonBase-root:has(.MuiBox-root):hover{
+ background-color: white;
+}
+.sidebar-popup li.MuiButtonBase-root:has(.MuiBox-root){
+ padding-bottom: 0;
+}
+.sidebar-popup .MuiModal-root p.MuiTypography-root {
+ font-size: var(--env-var-font-size-medium);
+ color: var(--env-var-color-2);
+}
+.sidebar-popup svg{
+ width: 16px;
+ height: 16px;
+ opacity: 0.9;
}
diff --git a/Client/src/Components/Sidebar/index.jsx b/Client/src/Components/Sidebar/index.jsx
index 1ab2171cb..69b44c125 100644
--- a/Client/src/Components/Sidebar/index.jsx
+++ b/Client/src/Components/Sidebar/index.jsx
@@ -9,6 +9,8 @@ import {
ListItemIcon,
ListItemText,
ListSubheader,
+ Menu,
+ MenuItem,
Stack,
Tooltip,
Typography,
@@ -17,6 +19,7 @@ import { useLocation, useNavigate } from "react-router";
import { useTheme } from "@emotion/react";
import { useDispatch, useSelector } from "react-redux";
import { clearAuthState } from "../../Features/Auth/authSlice";
+import { toggleSidebar } from "../../Features/UI/uiSlice";
import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
import Avatar from "../Avatar";
import LockSvg from "../../assets/icons/lock.svg?react";
@@ -24,6 +27,7 @@ import UserSvg from "../../assets/icons/user.svg?react";
import TeamSvg from "../../assets/icons/user-two.svg?react";
import LogoutSvg from "../../assets/icons/logout.svg?react";
import BWULogo from "../../assets/Images/bwl-logo.svg?react";
+import BWULogoAbbreviated from "../../assets/icons/bwu-abbreviated.svg?react";
import Support from "../../assets/icons/support.svg?react";
import Dashboard from "../../assets/icons/dashboard.svg?react";
import Account from "../../assets/icons/user-edit.svg?react";
@@ -36,6 +40,8 @@ import PageSpeed from "../../assets/icons/page-speed.svg?react";
import Settings from "../../assets/icons/settings.svg?react";
import ArrowDown from "../../assets/icons/down-arrow.svg?react";
import ArrowUp from "../../assets/icons/up-arrow.svg?react";
+import ArrowRight from "../../assets/icons/right-arrow.svg?react";
+import ArrowLeft from "../../assets/icons/left-arrow.svg?react";
import "./index.css";
@@ -81,7 +87,19 @@ function Sidebar() {
const location = useLocation();
const dispatch = useDispatch();
const authState = useSelector((state) => state.auth);
+ const { sidebar } = useSelector((state) => state.ui);
+ let collapsed = sidebar.collapsed;
const [open, setOpen] = useState({ Dashboard: false, Account: false });
+ const [anchorEl, setAnchorEl] = useState(null);
+ const [popup, setPopup] = useState();
+
+ const openPopup = (event, id) => {
+ setAnchorEl(event.currentTarget);
+ setPopup(id);
+ };
+ const closePopup = () => {
+ setAnchorEl(null);
+ };
/**
* Handles logging out the user
@@ -105,10 +123,47 @@ function Sidebar() {
}, []);
return (
-
-
-
-
+
+
+ {collapsed ? (
+
+ ) : (
+
+ )}
+ dispatch(toggleSidebar())}
+ >
+ {collapsed ? : }
+
+
{/* menu */}
+
-
-
-
- {authState.user?.firstName} {authState.user?.lastName}
-
-
- {authState.user?.role}
-
-
-
-
-
-
-
+ {collapsed ? (
+ <>
+
+ openPopup(event, "logout")}
+ sx={{ p: 0, "&:focus": { outline: "none" } }}
+ >
+
+
+
+
+
+
+
+ {authState.user?.firstName} {authState.user?.lastName}
+
+
+ {authState.user?.role}
+
+
+
+
+
+
+ Log out
+
+
+ >
+ ) : (
+ <>
+
+
+
+ {authState.user?.firstName} {authState.user?.lastName}
+
+
+ {authState.user?.role}
+
+
+
+
+
+
+
+ >
+ )}
);
diff --git a/Client/src/Components/TabPanels/Account/PasswordPanel.jsx b/Client/src/Components/TabPanels/Account/PasswordPanel.jsx
index 6247a57a9..602816e6c 100644
--- a/Client/src/Components/TabPanels/Account/PasswordPanel.jsx
+++ b/Client/src/Components/TabPanels/Account/PasswordPanel.jsx
@@ -98,16 +98,17 @@ const PasswordPanel = () => {
return (
-
+
+
+
+
);
};
diff --git a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx
index 355b09ca6..7d18b48c5 100644
--- a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx
+++ b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx
@@ -19,6 +19,7 @@ import ProgressUpload from "../../ProgressBars";
import { formatBytes } from "../../../Utils/fileUtils";
import { clearUptimeMonitorState } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import { createToast } from "../../../Utils/toastUtils";
+import { logger } from "../../../Utils/Logger";
/**
* ProfilePanel component displays a form for editing user profile information
@@ -216,11 +217,17 @@ const ProfilePanel = () => {
return (
-
-
-
+
+
+
+
+ Delete account
+
+ Note that deleting your account will remove all data from our
+ system. This is permanent and non-recoverable.
+
+
+ setIsOpen("delete")}
+ sx={{ width: "fit-content", mt: theme.gap.small }}
+ />
+
{/* TODO - Update ModalPopup Component with @mui for reusability */}
{
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
- width: 400,
+ width: 450,
bgcolor: "white",
border: "solid 1px #f2f2f2",
borderRadius: `${theme.shape.borderRadius}px`,
diff --git a/Client/src/Components/TabPanels/Account/TeamPanel.jsx b/Client/src/Components/TabPanels/Account/TeamPanel.jsx
index 07bbf6e68..a6c378a60 100644
--- a/Client/src/Components/TabPanels/Account/TeamPanel.jsx
+++ b/Client/src/Components/TabPanels/Account/TeamPanel.jsx
@@ -16,7 +16,7 @@ import { useEffect, useState } from "react";
import EditSvg from "../../../assets/icons/edit.svg?react";
import Field from "../../Inputs/Field";
import { credentials } from "../../../Validation/validation";
-import axiosInstance from "../../../Utils/axiosConfig";
+import { networkService } from "../../../main";
import { createToast } from "../../../Utils/toastUtils";
import { useSelector } from "react-redux";
import BasicTable from "../../BasicTable";
@@ -52,10 +52,7 @@ const TeamPanel = () => {
useEffect(() => {
const fetchTeam = async () => {
try {
- const response = await axiosInstance.get("/auth/users", {
- headers: { Authorization: `Bearer ${authToken}` },
- });
-
+ const response = await networkService.getAllUsers(authToken);
setMembers(response.data.data);
} catch (error) {
createToast({
@@ -134,9 +131,6 @@ const TeamPanel = () => {
};
const handleRename = () => {};
- //TODO - implement save team function
- const handleSaveTeam = () => {};
-
// INVITE MEMBER
const [isOpen, setIsOpen] = useState(false);
@@ -179,13 +173,10 @@ const TeamPanel = () => {
setErrors((prev) => ({ ...prev, email: error.details[0].message }));
} else
try {
- await axiosInstance.post(
- "/auth/invite",
- {
- email: toInvite.email,
- role: toInvite.role,
- },
- { headers: { Authorization: `Bearer ${authToken}` } }
+ await networkService.requestInvitationToken(
+ authToken,
+ toInvite.email,
+ toInvite.role
);
closeInviteModal();
@@ -206,12 +197,11 @@ const TeamPanel = () => {
return (
-
-
-
+
{
try {
- const res = await axiosInstance.post("/auth/register", form);
+ const res = await networkService.registerUser(form);
return res.data;
} catch (error) {
if (error.response.data) {
@@ -31,7 +31,7 @@ export const register = createAsyncThunk(
export const login = createAsyncThunk("auth/login", async (form, thunkApi) => {
try {
- const res = await axiosInstance.post(`/auth/login`, form);
+ const res = await networkService.loginUser(form);
return res.data;
} catch (error) {
if (error.response && error.response.data) {
@@ -51,16 +51,13 @@ export const update = createAsyncThunk(
const { authToken: token, localData: form } = data;
const user = jwtDecode(token);
try {
- //1.5s delay to show loading spinner
- await new Promise((resolve) => setTimeout(resolve, 1500));
-
const fd = new FormData();
form.firstName && fd.append("firstName", form.firstName);
form.lastName && fd.append("lastName", form.lastName);
form.password && fd.append("password", form.password);
form.newPassword && fd.append("newPassword", form.newPassword);
if (form.file && form.file !== "") {
- const imageResult = await axiosInstance.get(form.file, {
+ const imageResult = await networkService.get(form.file, {
responseType: "blob",
baseURL: "",
});
@@ -69,12 +66,8 @@ export const update = createAsyncThunk(
form.deleteProfileImage &&
fd.append("deleteProfileImage", form.deleteProfileImage);
- const res = await axiosInstance.put(`/auth/user/${user._id}`, fd, {
- headers: {
- Authorization: `Bearer ${token}`,
- "Content-Type": "multipart/form-data",
- },
- });
+ const res = await networkService.updateUser(token, user._id, fd);
+
return res.data;
} catch (error) {
if (error.response && error.response.data) {
@@ -95,9 +88,7 @@ export const deleteUser = createAsyncThunk(
const user = jwtDecode(data);
try {
- const res = await axiosInstance.delete(`/auth/user/${user._id}`, {
- headers: { Authorization: `Bearer ${data}` },
- });
+ const res = await networkService.deleteUser(data, user._id);
return res.data;
} catch (error) {
if (error.response && error.response.data) {
@@ -116,7 +107,7 @@ export const forgotPassword = createAsyncThunk(
"auth/forgotPassword",
async (form, thunkApi) => {
try {
- const res = await axiosInstance.post("/auth/recovery/request", form);
+ const res = await networkService.forgotPassword(form);
return res.data;
} catch (error) {
if (error.response.data) {
@@ -136,13 +127,8 @@ export const setNewPassword = createAsyncThunk(
async (data, thunkApi) => {
const { token, form } = data;
try {
- await axiosInstance.post("/auth/recovery/validate", {
- recoveryToken: token,
- });
- const res = await axiosInstance.post("/auth/recovery/reset", {
- ...form,
- recoveryToken: token,
- });
+ await networkService.validateRecoveryToken(token);
+ const res = await networkService.setNewPassword(token, form);
return res.data;
} catch (error) {
if (error.response.data) {
diff --git a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js
index 6af825a37..22e2b3324 100644
--- a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js
+++ b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js
@@ -1,6 +1,6 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { jwtDecode } from "jwt-decode";
-import axiosInstance from "../../Utils/axiosConfig";
+import { networkService } from "../../main";
const initialState = {
isLoading: false,
monitors: [],
@@ -13,32 +13,7 @@ export const createPageSpeed = createAsyncThunk(
async (data, thunkApi) => {
try {
const { authToken, monitor } = data;
-
- const res = await axiosInstance.post(`/monitors`, monitor, {
- headers: {
- Authorization: `Bearer ${authToken}`,
- "Content-Type": "application/json",
- },
- });
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-
-export const getPageSpeedMonitors = createAsyncThunk(
- "pageSpeedMonitors/getPageSpeedMonitors",
- async (token, thunkApi) => {
- try {
- const res = await axiosInstance.get("/monitors");
+ const res = await networkService.createMonitor(authToken, monitor);
return res.data;
} catch (error) {
if (error.response && error.response.data) {
@@ -58,14 +33,16 @@ export const getPageSpeedByUserId = createAsyncThunk(
async (token, thunkApi) => {
const user = jwtDecode(token);
try {
- const res = await axiosInstance.get(
- `/monitors/user/${user._id}?limit=25&type=pagespeed&sortOrder=desc`,
- {
- headers: {
- Authorization: `Bearer ${token}`,
- },
- }
+ const res = await networkService.getMonitorsByUserId(
+ token,
+ user._id,
+ 25,
+ ["pagespeed"],
+ null,
+ "desc",
+ false
);
+
return res.data;
} catch (error) {
if (error.response && error.response.data) {
@@ -91,15 +68,10 @@ export const updatePageSpeed = createAsyncThunk(
interval: monitor.interval,
// notifications: monitor.notifications,
};
- const res = await axiosInstance.put(
- `/monitors/${monitor._id}`,
- updatedFields,
- {
- headers: {
- Authorization: `Bearer ${authToken}`,
- "Content-Type": "application/json",
- },
- }
+ const res = await networkService.updateMonitor(
+ authToken,
+ monitor._id,
+ updatedFields
);
return res.data;
} catch (error) {
@@ -120,12 +92,10 @@ export const deletePageSpeed = createAsyncThunk(
async (data, thunkApi) => {
try {
const { authToken, monitor } = data;
- const res = await axiosInstance.delete(`/monitors/${monitor._id}`, {
- headers: {
- Authorization: `Bearer ${authToken}`,
- "Content-Type": "application/json",
- },
- });
+ const res = await networkService.deleteMonitorById(
+ authToken,
+ monitor._id
+ );
return res.data;
} catch (error) {
if (error.response && error.response.data) {
@@ -153,25 +123,6 @@ const pageSpeedMonitorSlice = createSlice({
},
extraReducers: (builder) => {
builder
- // *****************************************************
- // All Monitors
- // *****************************************************
- .addCase(getPageSpeedMonitors.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(getPageSpeedMonitors.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.success;
- state.msg = action.payload.msg;
- state.monitors = action.payload.data;
- })
- .addCase(getPageSpeedMonitors.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload
- ? action.payload.msg
- : "Getting montiors failed";
- })
// *****************************************************
// Monitors by userId
// *****************************************************
diff --git a/Client/src/Features/UI/uiSlice.js b/Client/src/Features/UI/uiSlice.js
new file mode 100644
index 000000000..ebea20bf1
--- /dev/null
+++ b/Client/src/Features/UI/uiSlice.js
@@ -0,0 +1,31 @@
+import { createSlice } from "@reduxjs/toolkit";
+
+// Initial state for UI settings.
+// Add more settings as needed (e.g., theme preferences, user settings)
+const initialState = {
+ monitors: {
+ rowsPerPage: 5,
+ },
+ team: {
+ rowsPerPage: 5,
+ },
+ sidebar: {
+ collapsed: false,
+ },
+};
+
+const uiSlice = createSlice({
+ name: "ui",
+ initialState,
+ reducers: {
+ setRowsPerPage: (state, action) => {
+ state[action.payload.table].rowsPerPage = action.payload.value;
+ },
+ toggleSidebar: (state) => {
+ state.sidebar.collapsed = !state.sidebar.collapsed;
+ },
+ },
+});
+
+export default uiSlice.reducer;
+export const { setRowsPerPage, toggleSidebar } = uiSlice.actions;
diff --git a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
index 27d616e1e..9ab7d385b 100644
--- a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
+++ b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
@@ -1,6 +1,6 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { jwtDecode } from "jwt-decode";
-import axiosInstance from "../../Utils/axiosConfig";
+import { networkService } from "../../main";
const initialState = {
isLoading: false,
monitors: [],
@@ -13,32 +13,7 @@ export const createUptimeMonitor = createAsyncThunk(
async (data, thunkApi) => {
try {
const { authToken, monitor } = data;
-
- const res = await axiosInstance.post(`/monitors`, monitor, {
- headers: {
- Authorization: `Bearer ${authToken}`,
- "Content-Type": "application/json",
- },
- });
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-
-export const getUptimeMonitors = createAsyncThunk(
- "monitors/getMonitors",
- async (token, thunkApi) => {
- try {
- const res = await axiosInstance.get("/monitors");
+ const res = await networkService.createMonitor(authToken, monitor);
return res.data;
} catch (error) {
if (error.response && error.response.data) {
@@ -58,13 +33,14 @@ export const getUptimeMonitorsByUserId = createAsyncThunk(
async (token, thunkApi) => {
const user = jwtDecode(token);
try {
- const res = await axiosInstance.get(
- `/monitors/user/${user._id}?limit=25&type=http&type=ping&sortOrder=desc&normalize=true`,
- {
- headers: {
- Authorization: `Bearer ${token}`,
- },
- }
+ const res = await networkService.getMonitorsByUserId(
+ token,
+ user._id,
+ 25,
+ ["http", "ping"],
+ null,
+ "desc",
+ true
);
return res.data;
} catch (error) {
@@ -91,15 +67,10 @@ export const updateUptimeMonitor = createAsyncThunk(
interval: monitor.interval,
notifications: monitor.notifications,
};
- const res = await axiosInstance.put(
- `/monitors/${monitor._id}`,
- updatedFields,
- {
- headers: {
- Authorization: `Bearer ${authToken}`,
- "Content-Type": "application/json",
- },
- }
+ const res = await networkService.updateMonitor(
+ authToken,
+ monitor._id,
+ updatedFields
);
return res.data;
} catch (error) {
@@ -120,12 +91,10 @@ export const deleteUptimeMonitor = createAsyncThunk(
async (data, thunkApi) => {
try {
const { authToken, monitor } = data;
- const res = await axiosInstance.delete(`/monitors/${monitor._id}`, {
- headers: {
- Authorization: `Bearer ${authToken}`,
- "Content-Type": "application/json",
- },
- });
+ const res = await networkService.deleteMonitorById(
+ authToken,
+ monitor._id
+ );
return res.data;
} catch (error) {
if (error.response && error.response.data) {
@@ -153,25 +122,6 @@ const uptimeMonitorsSlice = createSlice({
},
extraReducers: (builder) => {
builder
- // *****************************************************
- // All Monitors
- // *****************************************************
- .addCase(getUptimeMonitors.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(getUptimeMonitors.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.success;
- state.msg = action.payload.msg;
- state.monitors = action.payload.data;
- })
- .addCase(getUptimeMonitors.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload
- ? action.payload.msg
- : "Getting uptime monitors failed";
- })
// *****************************************************
// Monitors by userId
// *****************************************************
diff --git a/Client/src/HOC/withAdminCheck.jsx b/Client/src/HOC/withAdminCheck.jsx
index 23ee5dc1f..30a767f36 100644
--- a/Client/src/HOC/withAdminCheck.jsx
+++ b/Client/src/HOC/withAdminCheck.jsx
@@ -1,21 +1,23 @@
import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom";
-import axiosInstance from "../Utils/axiosConfig";
+
+import { logger } from "../Utils/Logger";
+import { networkService } from "../main";
const withAdminCheck = (WrappedComponent) => {
const WithAdminCheck = (props) => {
const navigate = useNavigate();
useEffect(() => {
- axiosInstance
- .get("/auth/users/admin")
+ networkService
+ .doesAdminExist()
.then((response) => {
if (response.data.data === true) {
navigate("/login");
}
})
.catch((error) => {
- console.log(error);
+ logger.error(error);
});
}, [navigate]);
return ;
diff --git a/Client/src/Layouts/HomeLayout/index.css b/Client/src/Layouts/HomeLayout/index.css
index f143b3fd9..dcdcf0535 100644
--- a/Client/src/Layouts/HomeLayout/index.css
+++ b/Client/src/Layouts/HomeLayout/index.css
@@ -19,7 +19,6 @@
height: calc(100vh - var(--env-var-spacing-2) * 2);
max-width: var(--env-var-side-bar-width);
- flex: 1;
border: 1px solid var(--color-border-0);
border-radius: var(--env-var-radius-1);
@@ -27,10 +26,6 @@
padding: var(--env-var-spacing-1) 0;
}
-.home-layout aside > *:not(:first-child) {
- padding-left: var(--env-var-spacing-1-plus);
- padding-right: var(--env-var-spacing-1-plus);
-}
.home-layout > div {
min-height: calc(100vh - var(--env-var-spacing-2) * 2);
diff --git a/Client/src/Pages/Account/index.css b/Client/src/Pages/Account/index.css
index c05cc01e7..594b7b839 100644
--- a/Client/src/Pages/Account/index.css
+++ b/Client/src/Pages/Account/index.css
@@ -15,18 +15,10 @@
.account h1.MuiTypography-root {
font-weight: 600;
}
-.account .MuiStack-root:not(.table-stack) {
- flex: 1;
- -webkit-flex: 1;
- -ms-flex: 1;
-}
.account .MuiTabPanel-root {
padding: 0;
margin-top: 50px;
}
-.account .MuiDivider-root:not(.short-divider) {
- margin: 50px 0;
-}
.account .MuiDivider-root,
.account .MuiButtonGroup-root button {
border-color: var(--env-var-color-16);
@@ -34,50 +26,14 @@
.account button:not(.MuiIconButton-root) {
height: 34px;
}
-.account [class$="-form"] {
- width: inherit;
-}
-.account [class$="__wrapper"],
-.account [class$="__wrapper compact"] {
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- align-items: flex-start;
-}
-.edit-profile-form__wrapper > .MuiStack-root:not(.row-stack),
-.edit-password-form__wrapper > .MuiStack-root:not(.row-stack) {
- flex-direction: column;
- gap: 8px;
- margin-right: 10px;
-}
-.edit-profile-form__wrapper p.MuiTypography-root,
-.delete-profile-form__wrapper p.MuiTypography-root {
- opacity: 0.6;
+.account .field {
+ flex: 1;
}
#modal-delete-account,
#modal-edit-org-name,
#modal-invite-member {
font-size: var(--env-var-font-size-large);
}
-.edit-profile-form__wrapper:not(:first-of-type),
-.edit-password-form__wrapper:not(:first-of-type),
-.edit-team-form__wrapper:not(:first-of-type) {
- margin-top: var(--env-var-spacing-4);
-}
-.edit-team-form__wrapper.compact {
- margin-top: var(--env-var-spacing-3);
- justify-content: flex-start;
- align-items: center;
-}
-
-/* for testing, will remove later */
-@media only screen and (max-width: 1600px) {
- .edit-profile-form__wrapper .MuiStack-root:not(.row-stack),
- .edit-password-form__wrapper .MuiStack-root:not(.row-stack),
- .edit-organization-form__wrapper .MuiStack-root:not(.row-stack) {
- margin-right: 30px;
- }
-}
.account .MuiStack-root:has(span.MuiTypography-root.input-error) {
position: relative;
@@ -87,22 +43,9 @@
top: 100%;
}
-.edit-profile-form__wrapper .MuiFormControl-root,
-.edit-password-form__wrapper .MuiFormControl-root {
- width: 100%;
+.account .MuiTable-root .MuiTableBody-root .MuiTableCell-root {
+ color: var(--env-var-color-25);
}
-.edit-profile-form__wrapper .MuiStack-root:has(.MuiFormControl-root),
-.edit-password-form__wrapper .MuiStack-root:has(.MuiFormControl-root) {
- margin: 0;
-}
-
-.edit-organization-form {
- display: flex;
- align-items: center;
-}
-.edit-team-form .MuiTableCell-root {
- color: var(--env-var-color-25) !important;
-}
-.edit-team-form .MuiTableBody-root .MuiTableCell-root {
- padding: var(--env-var-spacing-1-plus) var(--env-var-spacing-2) !important;
+.account .MuiTableBody-root .MuiTableCell-root {
+ padding: var(--env-var-spacing-1-plus) var(--env-var-spacing-2);
}
diff --git a/Client/src/Pages/Account/index.jsx b/Client/src/Pages/Account/index.jsx
index 700e8c6e8..2fed926b8 100644
--- a/Client/src/Pages/Account/index.jsx
+++ b/Client/src/Pages/Account/index.jsx
@@ -1,14 +1,13 @@
import PropTypes from "prop-types";
-import { useState } from "react";
+import { useNavigate } from "react-router";
+import { useSelector } from "react-redux";
import { Box, Tab, useTheme } from "@mui/material";
import TabContext from "@mui/lab/TabContext";
import TabList from "@mui/lab/TabList";
-import "./index.css";
import ProfilePanel from "../../Components/TabPanels/Account/ProfilePanel";
import PasswordPanel from "../../Components/TabPanels/Account/PasswordPanel";
import TeamPanel from "../../Components/TabPanels/Account/TeamPanel";
-import { useNavigate } from "react-router";
-import { useSelector } from "react-redux";
+import "./index.css";
/**
* Account component renders a settings page with tabs for Profile, Password, and Team settings.
diff --git a/Client/src/Pages/Auth/CheckEmail.jsx b/Client/src/Pages/Auth/CheckEmail.jsx
index 0ede66831..4a0daf68a 100644
--- a/Client/src/Pages/Auth/CheckEmail.jsx
+++ b/Client/src/Pages/Auth/CheckEmail.jsx
@@ -145,7 +145,7 @@ const CheckEmail = () => {
}}
/>
- Didn't receive the email?{" "}
+ Didn't receive the email?{" "}
{
Forgot password?
- No worries, we'll send you reset instructions.
+ No worries, we'll send you reset instructions.
diff --git a/Client/src/Pages/Auth/Login.jsx b/Client/src/Pages/Auth/Login.jsx
index 84f6c518e..00de688f6 100644
--- a/Client/src/Pages/Auth/Login.jsx
+++ b/Client/src/Pages/Auth/Login.jsx
@@ -4,16 +4,17 @@ import { Box, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { credentials } from "../../Validation/validation";
import { login } from "../../Features/Auth/authSlice";
-import { useDispatch } from "react-redux";
+import { useDispatch, useSelector } from "react-redux";
import { createToast } from "../../Utils/toastUtils";
import Button from "../../Components/Button";
-import axiosInstance from "../../Utils/axiosConfig";
+import { networkService } from "../../main";
import Field from "../../Components/Inputs/Field";
import background from "../../assets/Images/background_pattern_decorative.png";
import Logo from "../../assets/icons/bwu-icon.svg?react";
import Mail from "../../assets/icons/mail.svg?react";
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
-
+import PropTypes from "prop-types";
+import { logger } from "../../Utils/Logger";
import "./index.css";
/**
@@ -63,6 +64,10 @@ const LandingPage = ({ onContinue }) => {
);
};
+LandingPage.propTypes = {
+ onContinue: PropTypes.func.isRequired,
+};
+
/**
* Renders the first step of the login process.
*
@@ -136,6 +141,14 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
);
};
+StepOne.propTypes = {
+ form: PropTypes.object.isRequired,
+ errors: PropTypes.object.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+ onChange: PropTypes.func.isRequired,
+ onBack: PropTypes.func.isRequired,
+};
+
/**
* Renders the second step of the login process, including a password input field.
*
@@ -230,11 +243,22 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
);
};
+StepTwo.propTypes = {
+ form: PropTypes.object.isRequired,
+ errors: PropTypes.object.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+ onChange: PropTypes.func.isRequired,
+ onBack: PropTypes.func.isRequired,
+};
+
const Login = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const theme = useTheme();
+ const authState = useSelector((state) => state.auth);
+ const { authToken } = authState;
+
const idMap = {
"login-email-input": "email",
"login-password-input": "password",
@@ -248,17 +272,21 @@ const Login = () => {
const [step, setStep] = useState(0);
useEffect(() => {
- axiosInstance
- .get("/auth/users/admin")
+ if (authToken) {
+ navigate("/monitors");
+ return;
+ }
+ networkService
+ .doesAdminExist()
.then((response) => {
if (response.data.data === false) {
navigate("/register");
}
})
.catch((error) => {
- console.log(error);
+ logger.error(error);
});
- }, [navigate]);
+ }, [authToken, navigate]);
const handleChange = (event) => {
const { value, id } = event.target;
@@ -399,7 +427,9 @@ const Login = () => {
)}
- Don't have an account? —
+
+ Don't have an account? —
+
{
);
};
+LandingPage.propTypes = {
+ isAdmin: PropTypes.bool,
+ onSignup: PropTypes.func,
+};
+
/**
* Renders the first step of the sign up process.
*
@@ -151,6 +157,14 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
);
};
+StepOne.propTypes = {
+ form: PropTypes.object,
+ errors: PropTypes.object,
+ onSubmit: PropTypes.func,
+ onChange: PropTypes.func,
+ onBack: PropTypes.func,
+};
+
/**
* Renders the second step of the sign up process.
*
@@ -223,6 +237,14 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
);
};
+StepTwo.propTypes = {
+ form: PropTypes.object,
+ errors: PropTypes.object,
+ onSubmit: PropTypes.func,
+ onChange: PropTypes.func,
+ onBack: PropTypes.func,
+};
+
/**
* Renders the third step of the sign up process.
*
@@ -368,6 +390,14 @@ const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => {
);
};
+StepThree.propTypes = {
+ form: PropTypes.object,
+ errors: PropTypes.object,
+ onSubmit: PropTypes.func,
+ onChange: PropTypes.func,
+ onBack: PropTypes.func,
+};
+
const Register = ({ isAdmin }) => {
const dispatch = useDispatch();
const navigate = useNavigate();
@@ -397,19 +427,16 @@ const Register = ({ isAdmin }) => {
const fetchInvite = async () => {
if (token !== undefined) {
try {
- const res = await axiosInstance.post(`/auth/invite/verify`, {
- token,
- });
+ const res = await networkService.verifyInvitationToken(token);
const { role, email } = res.data.data;
- console.log(role);
setForm({ ...form, email, role });
} catch (error) {
- console.log(error);
+ logger.error(error);
}
}
};
fetchInvite();
- }, [token]);
+ }, [token, form]);
/**
* Validates the form data against the validation schema.
diff --git a/Client/src/Pages/Home/index.css b/Client/src/Pages/Home/index.css
deleted file mode 100644
index fdb4ddd1b..000000000
--- a/Client/src/Pages/Home/index.css
+++ /dev/null
@@ -1 +0,0 @@
-/* Home Page Styles*/
\ No newline at end of file
diff --git a/Client/src/Pages/Home/index.jsx b/Client/src/Pages/Home/index.jsx
deleted file mode 100644
index 161650512..000000000
--- a/Client/src/Pages/Home/index.jsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import DropdownTeamMember from "../../Components/DropdownTeamMember";
-import Search from "../../Components/Search";
-import "./index.css";
-import DashboardMenu from "../../Components/DashboardMenu";
-import Integrations from "../../Components/Integrations";
-
-const Home = () => {
- const token = localStorage.getItem("token");
- return (
- <>
- Home
- >
- );
-};
-
-export default Home;
diff --git a/Client/src/Pages/Incidents/IncidentTable/index.jsx b/Client/src/Pages/Incidents/IncidentTable/index.jsx
index 44a30decc..7a77208f8 100644
--- a/Client/src/Pages/Incidents/IncidentTable/index.jsx
+++ b/Client/src/Pages/Incidents/IncidentTable/index.jsx
@@ -18,8 +18,9 @@ import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded";
import { useState, useEffect } from "react";
import { useSelector } from "react-redux";
-import axiosInstance from "../../../Utils/axiosConfig";
+import { networkService } from "../../../main";
import { StatusLabel } from "../../../Components/Label";
+import { logger } from "../../../Utils/Logger";
const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
const { authToken, user } = useSelector((state) => state.auth);
@@ -31,10 +32,10 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
});
useEffect(() => {
- setPaginationController({
- ...paginationController,
+ setPaginationController((prevPaginationController) => ({
+ ...prevPaginationController,
page: 0,
- });
+ }));
}, [filter, selectedMonitor]);
useEffect(() => {
@@ -43,21 +44,34 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
return;
}
try {
- let url = `/checks/${selectedMonitor}?sortOrder=desc&filter=${filter}&page=${paginationController.page}&rowsPerPage=${paginationController.rowsPerPage}`;
-
+ let res;
if (selectedMonitor === "0") {
- url = `/checks/user/${user._id}?sortOrder=desc&filter=${filter}&page=${paginationController.page}&rowsPerPage=${paginationController.rowsPerPage}`;
+ res = await networkService.getChecksByUser(
+ authToken,
+ user._id,
+ "desc",
+ null,
+ null,
+ filter,
+ paginationController.page,
+ paginationController.rowsPerPage
+ );
+ } else {
+ res = await networkService.getChecksByMonitor(
+ authToken,
+ selectedMonitor,
+ "desc",
+ null,
+ null,
+ filter,
+ paginationController.page,
+ paginationController.rowsPerPage
+ );
}
-
- const res = await axiosInstance.get(url, {
- headers: {
- Authorization: `Bearer ${authToken}`,
- },
- });
setChecks(res.data.data.checks);
setChecksCount(res.data.data.checksCount);
} catch (error) {
- console.log(error);
+ logger.error(error);
}
};
fetchPage();
@@ -72,7 +86,6 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
]);
const handlePageChange = (_, newPage) => {
- console.log(newPage);
setPaginationController({
...paginationController,
page: newPage - 1, // 0-indexed
diff --git a/Client/src/Pages/Incidents/index.jsx b/Client/src/Pages/Incidents/index.jsx
index f9d53f7c0..34768f7f9 100644
--- a/Client/src/Pages/Incidents/index.jsx
+++ b/Client/src/Pages/Incidents/index.jsx
@@ -2,7 +2,7 @@ import { useState, useEffect } from "react";
import { useSelector } from "react-redux";
import { ButtonGroup, Stack, Skeleton, Typography } from "@mui/material";
import Button from "../../Components/Button";
-import axiosInstance from "../../Utils/axiosConfig";
+import { networkService } from "../../main";
import { useTheme } from "@emotion/react";
import Select from "../../Components/Inputs/Select";
import IncidentTable from "./IncidentTable";
@@ -48,13 +48,14 @@ const Incidents = () => {
useEffect(() => {
const fetchMonitors = async () => {
setLoading(true);
- const res = await axiosInstance.get(
- `/monitors/user/${authState.user._id}?status=false&limit=1`,
- {
- headers: {
- Authorization: `Bearer ${authState.authToken}`,
- },
- }
+ const res = await networkService.getMonitorsByUserId(
+ authState.authToken,
+ authState.user._id,
+ 1,
+ null,
+ null,
+ null,
+ null
);
// Reduce to a lookup object for 0(1) lookup
diff --git a/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx
index 187d9c63d..d207bfc98 100644
--- a/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx
+++ b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx
@@ -1,8 +1,8 @@
import { Box, Stack, Typography } from "@mui/material";
import "./index.css";
-import React, { useState } from "react";
+import { useState } from "react";
import Button from "../../../Components/Button";
-import Back from "../../../assets/icons/left-arrow.svg?react";
+import Back from "../../../assets/icons/left-arrow-long.svg?react";
import Select from "../../../Components/Inputs/Select";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
diff --git a/Client/src/Pages/Maintenance/index.jsx b/Client/src/Pages/Maintenance/index.jsx
index c589552f7..e9c28d905 100644
--- a/Client/src/Pages/Maintenance/index.jsx
+++ b/Client/src/Pages/Maintenance/index.jsx
@@ -1,10 +1,7 @@
-import { useTheme } from "@emotion/react";
import Fallback from "../../Components/Fallback";
import "./index.css";
const Maintenance = () => {
- const theme = useTheme();
-
return (
.MuiBox-root,
+.configure-monitor .config-box > .MuiStack-root {
flex: 1;
+ padding: 24px 30px;
+ padding-bottom: 40px;
+}
+.configure-monitor .config-box > *:first-child {
+ flex: 0.7;
+ border-right: solid 1px var(--env-var-color-16);
}
.configure-monitor .MuiStack-root:has(span.MuiTypography-root.input-error) {
position: relative;
@@ -72,12 +72,6 @@
border-color: var(--env-var-color-29);
}
-.configure-monitor-form {
- display: flex;
- flex-direction: column;
- gap: var(--env-var-spacing-2);
-}
-
body:has(.configure-monitor) .select-dropdown .MuiMenuItem-root,
.configure-monitor .select-wrapper .select-component > .MuiSelect-select {
text-transform: lowercase;
diff --git a/Client/src/Pages/Monitors/Configure/index.jsx b/Client/src/Pages/Monitors/Configure/index.jsx
index 08c8cbe39..6b809433d 100644
--- a/Client/src/Pages/Monitors/Configure/index.jsx
+++ b/Client/src/Pages/Monitors/Configure/index.jsx
@@ -5,15 +5,11 @@ import { useEffect, useState } from "react";
import Button from "../../../Components/Button";
import Field from "../../../Components/Inputs/Field";
import { Box, Modal, Skeleton, Stack, Typography } from "@mui/material";
-import WestRoundedIcon from "@mui/icons-material/WestRounded";
import GreenCheck from "../../../assets/icons/checkbox-green.svg?react";
import RedCheck from "../../../assets/icons/checkbox-red.svg?react";
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
-import { getLastChecked } from "../../../Utils/monitorUtils";
-import "./index.css";
import { monitorValidation } from "../../../Validation/validation";
import Select from "../../../Components/Inputs/Select";
-import { formatDurationRounded } from "../../../Utils/timeUtils";
import { createToast } from "../../../Utils/toastUtils";
import {
updateUptimeMonitor,
@@ -21,6 +17,9 @@ import {
deleteUptimeMonitor,
} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import Checkbox from "../../../Components/Inputs/Checkbox";
+import Breadcrumbs from "../../../Components/Breadcrumbs";
+import "./index.css";
+import { logger } from "../../../Utils/Logger";
/**
* Parses a URL string and returns a URL object.
@@ -96,7 +95,6 @@ const Configure = () => {
const { user, authToken } = useSelector((state) => state.auth);
const { monitors } = useSelector((state) => state.uptimeMonitors);
const [monitor, setMonitor] = useState({});
- const [duration, setDuration] = useState(0);
const [errors, setErrors] = useState({});
const { monitorId } = useParams();
@@ -111,14 +109,13 @@ const Configure = () => {
useEffect(() => {
const data = monitors.find((monitor) => monitor._id === monitorId);
if (!data) {
- console.error("Error fetching monitor of id: " + monitorId);
- navigate("/not-found");
+ logger.error("Error fetching monitor of id: " + monitorId);
+ navigate("/not-found", { replace: true });
}
setMonitor({
...data,
});
- setDuration(formatDurationRounded(data?.interval));
- }, [monitorId, authToken, monitors]);
+ }, [monitorId, authToken, monitors, navigate]);
const handleChange = (event, name) => {
let { value, id } = event.target;
@@ -210,74 +207,54 @@ const Configure = () => {
const parsedUrl = parseUrl(monitor?.url);
const protocol = parsedUrl?.protocol?.replace(":", "") || "";
- var lastChecked = formatDurationRounded(getLastChecked(monitor?.checks));
-
let loading = Object.keys(monitor).length === 0;
return (
-
+
{loading ? (
) : (
<>
- }
- onClick={() => navigate(-1)}
- sx={{
- backgroundColor: "#f4f4f4",
- px: theme.gap.ml,
- "& svg.MuiSvgIcon-root": {
- mr: theme.gap.small,
- fill: theme.palette.otherColors.slateGray,
- },
- }}
+
-
General settings
-
+
Here you can select the URL of the host, together with the
type of monitor.
@@ -345,11 +322,10 @@ const Configure = () => {
className="config-box"
direction="row"
justifyContent="space-between"
- gap={theme.gap.xxl}
>
Incident notifications
-
+
When there is an incident, notify users.
@@ -362,7 +338,7 @@ const Configure = () => {
label="Notify via SMS (coming soon)"
isChecked={false}
value=""
- onChange={() => console.log("disabled")}
+ onChange={() => logger.warn("disabled")}
isDisabled={true}
/>
{
label="Also notify via email to multiple addresses (coming soon)"
isChecked={false}
value=""
- onChange={() => console.log("disabled")}
+ onChange={() => logger.warn("disabled")}
isDisabled={true}
/>
{monitor?.notifications?.some(
@@ -393,7 +369,7 @@ const Configure = () => {
type="text"
placeholder="name@gmail.com"
value=""
- onChange={() => console.log("disabled")}
+ onChange={() => logger.warn("disabled")}
/>
You can separate multiple emails with a comma
@@ -408,7 +384,6 @@ const Configure = () => {
className="config-box"
direction="row"
justifyContent="space-between"
- gap={theme.gap.xxl}
>
Advanced settings
@@ -423,7 +398,7 @@ const Configure = () => {
/>
-
+
{
onClick={handleSubmit}
/>
-
+
>
)}
{
}}
>
- Do you really want to delete this monitor?
+ Do you really want to delete this monitor?
Once deleted, this monitor cannot be retrieved.
@@ -480,7 +455,7 @@ const Configure = () => {
-
+
);
};
diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.css b/Client/src/Pages/Monitors/CreateMonitor/index.css
index 3fa38f14a..ecb27969c 100644
--- a/Client/src/Pages/Monitors/CreateMonitor/index.css
+++ b/Client/src/Pages/Monitors/CreateMonitor/index.css
@@ -13,7 +13,9 @@
.create-monitor button.MuiButtonBase-root {
font-size: var(--env-var-font-size-medium);
}
-.create-monitor h2.MuiTypography-root,
+.create-monitor h2.MuiTypography-root {
+ color: var(--env-var-color-5);
+}
.create-monitor h6.MuiTypography-root,
.create-monitor p.MuiTypography-root {
color: var(--env-var-color-2);
@@ -43,23 +45,20 @@
top: 100%;
}
.create-monitor .config-box {
- padding: var(--env-var-spacing-4) 50px;
- padding-bottom: 60px;
+ flex-direction: row;
border: 1px solid var(--env-var-color-16);
border-radius: var(--env-var-radius-1);
- flex-direction: row;
- gap: var(--env-var-spacing-4);
background-color: var(--env-var-color-8);
}
-.create-monitor .config-box .MuiBox-root,
-.create-monitor .config-box .MuiStack-root {
+.create-monitor .config-box > .MuiBox-root,
+.create-monitor .config-box > .MuiStack-root {
flex: 1;
+ padding: 24px 30px;
+ padding-bottom: 40px;
}
-
-.create-monitor-form {
- display: flex;
- flex-direction: column;
- gap: var(--env-var-spacing-2);
+.create-monitor .config-box > *:first-child {
+ flex: 0.7;
+ border-right: solid 1px var(--env-var-color-16);
}
.create-monitor .MuiStack-root .MuiButtonGroup-root button {
diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.jsx b/Client/src/Pages/Monitors/CreateMonitor/index.jsx
index e4e3f4d1c..28493080a 100644
--- a/Client/src/Pages/Monitors/CreateMonitor/index.jsx
+++ b/Client/src/Pages/Monitors/CreateMonitor/index.jsx
@@ -1,5 +1,5 @@
import "./index.css";
-import React, { useState } from "react";
+import { useState } from "react";
import RadioButton from "../../../Components/RadioButton";
import Button from "../../../Components/Button";
import { Box, ButtonGroup, Stack, Typography } from "@mui/material";
@@ -8,11 +8,12 @@ import { monitorValidation } from "../../../Validation/validation";
import { createUptimeMonitor } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import { useNavigate } from "react-router-dom";
import { useTheme } from "@emotion/react";
-import WestRoundedIcon from "@mui/icons-material/WestRounded";
import Field from "../../../Components/Inputs/Field";
import Select from "../../../Components/Inputs/Select";
import Checkbox from "../../../Components/Inputs/Checkbox";
import { createToast } from "../../../Utils/toastUtils";
+import Breadcrumbs from "../../../Components/Breadcrumbs";
+import { logger } from "../../../Utils/Logger";
const CreateMonitor = () => {
const MS_PER_MINUTE = 60000;
@@ -143,33 +144,26 @@ const CreateMonitor = () => {
return (
- }
- onClick={() => navigate("/monitors")}
- sx={{
- backgroundColor: theme.palette.otherColors.fillGray,
- mb: theme.gap.medium,
- px: theme.gap.ml,
- "& svg.MuiSvgIcon-root": {
- mr: theme.gap.small,
- fill: theme.palette.otherColors.slateGray,
- },
- }}
+
-
+
);
};
diff --git a/Client/src/Pages/Monitors/Details/PaginationTable/index.jsx b/Client/src/Pages/Monitors/Details/PaginationTable/index.jsx
index 28a668977..ad8248ab0 100644
--- a/Client/src/Pages/Monitors/Details/PaginationTable/index.jsx
+++ b/Client/src/Pages/Monitors/Details/PaginationTable/index.jsx
@@ -13,10 +13,11 @@ import {
import { useState, useEffect } from "react";
import { useSelector } from "react-redux";
-import axiosInstance from "../../../../Utils/axiosConfig";
+import { networkService } from "../../../../main";
import { StatusLabel } from "../../../../Components/Label";
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded";
+import { logger } from "../../../../Utils/Logger";
const PaginationTable = ({ monitorId, dateRange }) => {
const { authToken } = useSelector((state) => state.auth);
@@ -28,27 +29,29 @@ const PaginationTable = ({ monitorId, dateRange }) => {
});
useEffect(() => {
- setPaginationController({
- ...paginationController,
+ setPaginationController((prevPaginationController) => ({
+ ...prevPaginationController,
page: 0,
- });
+ }));
}, [dateRange]);
useEffect(() => {
const fetchPage = async () => {
try {
- const res = await axiosInstance.get(
- `/checks/${monitorId}?sortOrder=desc&dateRange=${dateRange}&page=${paginationController.page}&rowsPerPage=${paginationController.rowsPerPage}`,
- {
- headers: {
- Authorization: `Bearer ${authToken}`,
- },
- }
+ const res = await networkService.getChecksByMonitor(
+ authToken,
+ monitorId,
+ "desc",
+ null,
+ dateRange,
+ null,
+ paginationController.page,
+ paginationController.rowsPerPage
);
setChecks(res.data.data.checks);
setChecksCount(res.data.data.checksCount);
} catch (error) {
- console.log(error);
+ logger.error(error);
}
};
fetchPage();
diff --git a/Client/src/Pages/Monitors/Details/index.jsx b/Client/src/Pages/Monitors/Details/index.jsx
index 15cfd2c82..b636444bd 100644
--- a/Client/src/Pages/Monitors/Details/index.jsx
+++ b/Client/src/Pages/Monitors/Details/index.jsx
@@ -1,13 +1,12 @@
-import React, { useEffect, useState, useCallback } from "react";
+import { useEffect, useState, useCallback } from "react";
import PropTypes from "prop-types";
import { Box, Skeleton, Stack, Typography, useTheme } from "@mui/material";
import { useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
-import axiosInstance from "../../../Utils/axiosConfig";
+import { networkService } from "../../../main";
import MonitorDetailsAreaChart from "../../../Components/Charts/MonitorDetailsAreaChart";
import ButtonGroup from "@mui/material/ButtonGroup";
import Button from "../../../Components/Button";
-import WestRoundedIcon from "@mui/icons-material/WestRounded";
import GreenCheck from "../../../assets/icons/checkbox-green.svg?react";
import RedCheck from "../../../assets/icons/checkbox-red.svg?react";
import SettingsIcon from "../../../assets/icons/settings-bold.svg?react";
@@ -17,7 +16,8 @@ import {
formatDurationRounded,
} from "../../../Utils/timeUtils";
import "./index.css";
-import MonitorDetails60MinChart from "../../../Components/Charts/MonitorDetails60MinChart";
+import Breadcrumbs from "../../../Components/Breadcrumbs";
+import { logger } from "../../../Utils/Logger";
const StatBox = ({ title, value }) => {
return (
@@ -121,18 +121,19 @@ const DetailsPage = () => {
const fetchMonitor = useCallback(async () => {
try {
- const res = await axiosInstance.get(
- `/monitors/stats/${monitorId}?dateRange=${dateRange}&numToDisplay=50&normalize=true`,
- {
- headers: {
- Authorization: `Bearer ${authToken}`,
- },
- }
+ const res = await networkService.getStatsByMonitorId(
+ authToken,
+ monitorId,
+ null,
+ null,
+ dateRange,
+ 50,
+ true
);
- setMonitor(res.data.data);
+ setMonitor(res?.data?.data ?? {});
} catch (error) {
- console.error("Error fetching monitor of id: " + monitorId);
- navigate("/not-found");
+ logger.error(error);
+ navigate("/not-found", { replace: true });
}
}, [authToken, monitorId, navigate, dateRange]);
@@ -142,15 +143,15 @@ const DetailsPage = () => {
useEffect(() => {
const fetchCertificate = async () => {
- const res = await axiosInstance.get(
- `/monitors/certificate/${monitorId}`,
- {
- headers: {
- Authorization: `Bearer ${authToken}`,
- },
- }
- );
- setCertificateExpiry(res.data.data.certificateDate);
+ try {
+ const res = await networkService.getCertificateExpiry(
+ authToken,
+ monitorId
+ );
+ setCertificateExpiry(res?.data?.data?.certificateDate ?? "N/A");
+ } catch (error) {
+ console.error(error);
+ }
};
fetchCertificate();
}, [authToken, monitorId]);
@@ -163,39 +164,19 @@ const DetailsPage = () => {
) : (
<>
- }
- onClick={() => navigate("/monitors")}
- sx={{
- backgroundColor: theme.palette.otherColors.fillGray,
- px: theme.gap.ml,
- "& svg.MuiSvgIcon-root": {
- mr: theme.gap.small,
- fill: theme.palette.otherColors.slateGray,
- },
- }}
+
-
-
+
+
{monitor?.status ? : }
-
-
- {monitor.url?.replace(/^https?:\/\//, "") || "..."}
-
-
-
+
+ {monitor.url?.replace(/^https?:\/\//, "") || "..."}
+
button:not(.MuiIconButton-root) {
diff --git a/Client/src/Pages/Monitors/index.jsx b/Client/src/Pages/Monitors/index.jsx
index f8efd7fa3..dc293ef49 100644
--- a/Client/src/Pages/Monitors/index.jsx
+++ b/Client/src/Pages/Monitors/index.jsx
@@ -11,7 +11,6 @@ import ServerStatus from "../../Components/Charts/Servers/ServerStatus";
import { useTheme } from "@emotion/react";
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
import OpenInNewPage from "../../assets/icons/open-in-new-page.svg?react";
-import Settings from "../../assets/icons/settings-bold.svg?react";
import BasicTable from "../../Components/BasicTable";
import { StatusLabel } from "../../Components/Label";
import { createToast } from "../../Utils/toastUtils";
@@ -27,6 +26,180 @@ import {
Typography,
} from "@mui/material";
+import Settings from "../../assets/icons/settings-bold.svg?react";
+import PropTypes from "prop-types";
+
+const ActionsMenu = ({ monitor }) => {
+ 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 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
+ dispatch(getUptimeMonitorsByUserId(authState.authToken));
+ createToast({ body: "Monitor deleted successfully." });
+ } else {
+ createToast({ body: "Failed to delete 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",
+ },
+ }}
+ >
+
+
+
+ closeMenu(e)}
+ >
+ {actions.url !== null ? (
+ {
+ closeMenu(e);
+ e.stopPropagation();
+ window.open(actions.url, "_blank", "noreferrer");
+ }}
+ >
+ Open site
+
+ ) : (
+ ""
+ )}
+ {
+ e.stopPropagation();
+ navigate(`/monitors/${actions.id}`);
+ }}
+ >
+ Details
+
+ {/* TODO - pass monitor id to Incidents page */}
+ Incidents
+ {
+ e.stopPropagation();
+
+ navigate(`/monitors/configure/${actions.id}`);
+ }}
+ >
+ Configure
+
+ {
+ e.stopPropagation();
+ openRemove(e);
+ }}
+ >
+ Remove
+
+
+ setIsOpen(false)}
+ disablePortal
+ >
+
+
+ Do you really want to delete this monitor?
+
+
+ Once deleted, this monitor cannot be retrieved.
+
+
+ setIsOpen(false)}
+ />
+ handleRemove(e)}
+ />
+
+
+
+ >
+ );
+};
+
+ActionsMenu.propTypes = {
+ monitor: PropTypes.shape({
+ _id: PropTypes.string,
+ url: PropTypes.string,
+ type: PropTypes.string,
+ }).isRequired,
+};
+
/**
* Host component.
* This subcomponent receives a params object and displays the host details.
@@ -52,7 +225,7 @@ const Host = ({ params }) => {
"&:focus": {
outline: "none",
},
- mr: "3px",
+ mr: "5px",
}}
>
{
);
};
+Host.propTypes = {
+ params: PropTypes.shape({
+ url: PropTypes.string,
+ title: PropTypes.string,
+ percentageColor: PropTypes.string,
+ percentage: PropTypes.number,
+ }).isRequired,
+};
+
/**
* Renders a skeleton layout.
*
@@ -111,37 +293,6 @@ const Monitors = () => {
const authState = useSelector((state) => state.auth);
const dispatch = useDispatch({});
- const [anchorEl, setAnchorEl] = useState(null);
- const [actions, setActions] = useState({});
- const openMenu = (event, id, url) => {
- event.preventDefault();
- setAnchorEl(event.currentTarget);
- setActions({ id: id, url: url });
- };
- const closeMenu = () => {
- setAnchorEl(null);
- };
-
- const [isOpen, setIsOpen] = useState(false);
- const openRemove = () => {
- closeMenu();
- setIsOpen(true);
- };
- const handleRemove = async (event) => {
- event.preventDefault();
- let monitor = { _id: actions.id };
- const action = await dispatch(
- deleteUptimeMonitor({ authToken: authState.authToken, monitor })
- );
- if (action.meta.requestStatus === "fulfilled") {
- setIsOpen(false); // close modal
- dispatch(getUptimeMonitorsByUserId(authState.authToken));
- createToast({ body: "Monitor deleted successfully." });
- } else {
- createToast({ body: "Failed to delete monitor." });
- }
- };
-
useEffect(() => {
dispatch(getUptimeMonitorsByUserId(authState.authToken));
}, [authState.authToken, dispatch]);
@@ -191,7 +342,9 @@ const Monitors = () => {
return {
id: monitor._id,
// disabled for now
- handleClick: () => navigate(`/monitors/${monitor._id}`),
+ handleClick: () => {
+ navigate(`/monitors/${monitor._id}`);
+ },
data: [
{ id: idx, data: },
{
@@ -213,28 +366,7 @@ const Monitors = () => {
},
{
id: idx + 4,
- data: (
- <>
- {
- event.stopPropagation();
- openMenu(
- event,
- monitor._id,
- monitor.type === "ping" ? null : monitor.url
- );
- }}
- sx={{
- "&:focus": {
- outline: "none",
- },
- }}
- >
-
-
- >
- ),
+ data: ,
},
],
};
@@ -243,7 +375,7 @@ const Monitors = () => {
let loading = monitorState.isLoading && monitorState.monitors.length === 0;
return (
-
+
{loading ? (
) : (
@@ -262,10 +394,11 @@ const Monitors = () => {
{monitorState.monitors?.length !== 0 && (
{
navigate("/monitors/create");
}}
+ sx={{ fontWeight: 500 }}
/>
)}
@@ -303,10 +436,8 @@ const Monitors = () => {
- {
borderRadius: `${theme.shape.borderRadius}px`,
}}
>
-
+
Current monitors
{monitorState.monitors.length}
@@ -322,84 +453,11 @@ const Monitors = () => {
{/* TODO - add search bar */}
- {" "}
+
>
)}
>
)}
-
- {actions.url !== null ? (
- {
- window.open(actions.url, "_blank", "noreferrer");
- }}
- >
- Open site
-
- ) : (
- ""
- )}
- navigate(`/monitors/${actions.id}`)}>
- Details
-
- {/* TODO - pass monitor id to Incidents page */}
- Incidents
- navigate(`/monitors/configure/${actions.id}`)}>
- Configure
-
- Remove
-
- setIsOpen(false)}
- disablePortal
- >
-
-
- Do you really want to delete this monitor?
-
-
- Once deleted, this monitor cannot be retrieved.
-
-
- setIsOpen(false)}
- />
-
-
-
-
);
};
diff --git a/Client/src/Pages/NotFound/index.jsx b/Client/src/Pages/NotFound/index.jsx
index 113f3fd34..ef4b37839 100644
--- a/Client/src/Pages/NotFound/index.jsx
+++ b/Client/src/Pages/NotFound/index.jsx
@@ -11,7 +11,7 @@ import NotFoundSvg from "../../../src/assets/Images/sushi_404.svg";
* So That why we're using JavaScript default parameters instead.
*/
const DefaultValue = {
- title: "We cannot find this page",
+ title: "Oh no! You dropped your sushi!",
desc: "Either the URL doesn’t exist, or you don’t have access to it.",
};
@@ -27,20 +27,18 @@ const DefaultValue = {
* />
*
* @param {Object} props - The component props.
- * @param {string} [props.title="We cannot find this page"] - The title of the error page.
+ * @param {string} [props.title="Oh no! You dropped your sushi!"] - The title of the error page.
* @param {string} [props.desc="Either the URL doesn’t exist, or you don’t have access to it."] - The description of the error page.
* @returns {JSX.Element} The rendered error page component.
*/
const NotFound = ({ title = DefaultValue.title, desc = DefaultValue.desc }) => {
const navigate = useNavigate();
- console.log("NOT FOUND");
return (
+
{title}
{desc}
-
- Oh no! You dropped your sushi!
{
useEffect(() => {
const data = monitors.find((monitor) => monitor._id === monitorId);
if (!data) {
- console.error("Error fetching pagespeed monitor of id: " + monitorId);
- navigate("/not-found");
+ logger.error("Error fetching pagespeed monitor of id: " + monitorId);
+ navigate("/not-found", { replace: true });
}
setMonitor({
...data,
});
- }, [monitorId]);
+ }, [monitorId, monitors, navigate]);
const handleChange = (event, id) => {
let { value } = event.target;
@@ -147,37 +148,25 @@ const PageSpeedConfigure = () => {
let loading = Object.keys(monitor).length === 0;
return (
-
+
{loading ? (
) : (
<>
- }
- onClick={() => navigate(-1)}
- sx={{
- backgroundColor: theme.palette.otherColors.fillGray,
- mb: theme.gap.large,
- px: theme.gap.ml,
- "& svg.MuiSvgIcon-root": {
- mr: theme.gap.small,
- fill: theme.palette.otherColors.slateGray,
- },
- }}
+
-
-
+
{
sx={{ px: theme.gap.large, mt: theme.gap.large }}
/>
-
+
>
)}
{
-
+
);
};
diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.css b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.css
index f1f10621f..42ca0ffa8 100644
--- a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.css
+++ b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.css
@@ -2,10 +2,10 @@
background-color: var(--env-var-color-8);
border: solid 1px var(--env-var-color-6);
border-radius: var(--env-var-radius-1);
- padding: var(--env-var-default-2);
+ padding: var(--env-var-font-size-large-plus) var(--env-var-font-size-xlarge);
}
.create-page-speed h1.MuiTypography-root {
- font-size: var(--env-var-font-size-large);
+ font-size: 22px;
color: var(--env-var-color-1);
}
.create-page-speed p.MuiTypography-root,
@@ -50,9 +50,12 @@ body:has(.create-page-speed) .select-dropdown .MuiMenuItem-root,
text-transform: lowercase;
}
+.create-page-speed h3.MuiTypography-root {
+ flex: 0.7;
+}
+
.create-page-speed .field,
.create-page-speed .section-disabled,
-.create-page-speed .select-wrapper,
-.create-page-speed h3.MuiTypography-root {
+.create-page-speed .select-wrapper {
flex: 1;
}
diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx
index e9b271326..c707db319 100644
--- a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx
+++ b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx
@@ -10,8 +10,9 @@ import Checkbox from "../../../Components/Inputs/Checkbox";
import { monitorValidation } from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
import { createPageSpeed } from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
-import WestRoundedIcon from "@mui/icons-material/WestRounded";
+import Breadcrumbs from "../../../Components/Breadcrumbs";
import "./index.css";
+import { logger } from "../../../Utils/Logger";
const CreatePageSpeed = () => {
const theme = useTheme();
@@ -97,34 +98,22 @@ const CreatePageSpeed = () => {
};
return (
-
- }
- onClick={() => navigate("/pagespeed")}
- sx={{
- backgroundColor: theme.palette.otherColors.fillGray,
- mb: theme.gap.medium,
- px: theme.gap.ml,
- "& svg.MuiSvgIcon-root": {
- mr: theme.gap.small,
- fill: theme.palette.otherColors.slateGray,
- },
- }}
+
+
-
-
+
{
sx={{ px: theme.gap.large, mt: theme.gap.large }}
/>
-
-
+
+
);
};
diff --git a/Client/src/Pages/PageSpeed/Details/index.css b/Client/src/Pages/PageSpeed/Details/index.css
index 5d4d037b7..357177c78 100644
--- a/Client/src/Pages/PageSpeed/Details/index.css
+++ b/Client/src/Pages/PageSpeed/Details/index.css
@@ -37,7 +37,7 @@
.page-speed-details > .MuiStack-root:last-child,
.page-speed-details > .MuiBox-root:nth-last-child(3) {
border: solid 1px var(--env-var-color-6);
- border-radius: var(--env-var-radius-2);
+ border-radius: var(--env-var-radius-1);
background-color: var(--env-var-color-8);
min-width: 200px;
}
diff --git a/Client/src/Pages/PageSpeed/Details/index.jsx b/Client/src/Pages/PageSpeed/Details/index.jsx
index 006f2acf3..41f5892f2 100644
--- a/Client/src/Pages/PageSpeed/Details/index.jsx
+++ b/Client/src/Pages/PageSpeed/Details/index.jsx
@@ -9,28 +9,20 @@ import {
formatDuration,
formatDurationRounded,
} from "../../../Utils/timeUtils";
-import axiosInstance from "../../../Utils/axiosConfig";
+import { networkService } from "../../../main";
import Button from "../../../Components/Button";
-import WestRoundedIcon from "@mui/icons-material/WestRounded";
import SettingsIcon from "../../../assets/icons/settings-bold.svg?react";
import LastCheckedIcon from "../../../assets/icons/calendar-check.svg?react";
import ClockIcon from "../../../assets/icons/maintenance.svg?react";
import IntervalCheckIcon from "../../../assets/icons/interval-check.svg?react";
import GreenCheck from "../../../assets/icons/checkbox-green.svg?react";
import RedCheck from "../../../assets/icons/checkbox-red.svg?react";
-
-import "./index.css";
import PageSpeedLineChart from "../../../Components/Charts/PagespeedLineChart";
+import Breadcrumbs from "../../../Components/Breadcrumbs";
+import "./index.css";
+import PropTypes from "prop-types";
+import { logger } from "../../../Utils/Logger";
-/**
- * Displays a box with an icon, title, and value.
- *
- * @param {Object} props
- * @param {ReactNode} props.icon - The icon to display in the box.
- * @param {string} props.title - The title text to display above the value.
- * @param {string | number} props.value - The value text to display in the box.
- * @returns {JSX.Element}
- */
const StatBox = ({ icon, title, value }) => {
const theme = useTheme();
@@ -39,7 +31,8 @@ const StatBox = ({ icon, title, value }) => {
className="stat-box"
direction="row"
gap={theme.gap.small}
- p={theme.gap.ml}
+ pt={theme.gap.ml}
+ px={theme.gap.ml}
pb={theme.gap.mlplus}
>
{icon}
@@ -53,6 +46,12 @@ const StatBox = ({ icon, title, value }) => {
);
};
+StatBox.propTypes = {
+ icon: PropTypes.element,
+ title: PropTypes.string,
+ value: PropTypes.node,
+};
+
/**
* Renders a centered label within a pie chart.
*
@@ -85,6 +84,12 @@ const PieCenterLabel = ({ value, color, setExpand }) => {
);
};
+PieCenterLabel.propTypes = {
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ color: PropTypes.string,
+ setExpand: PropTypes.func,
+};
+
/**
* A component that renders a label on a pie chart slice.
* The label is positioned relative to the center of the pie chart and is optionally highlighted.
@@ -128,6 +133,14 @@ const PieValueLabel = ({ value, startAngle, endAngle, color, highlighted }) => {
);
};
+PieValueLabel.propTypes = {
+ value: PropTypes.number,
+ startAngle: PropTypes.number,
+ endAngle: PropTypes.number,
+ color: PropTypes.string,
+ highlighted: PropTypes.bool,
+};
+
/**
* Renders a skeleton layout.
*
@@ -186,25 +199,25 @@ const PageSpeedDetails = () => {
useEffect(() => {
const fetchMonitor = async () => {
try {
- const res = await axiosInstance.get(
- `/monitors/stats/${monitorId}?sortOrder=desc&limit=50`,
- {
- headers: {
- Authorization: `Bearer ${authToken}`,
- },
- }
+ const res = await networkService.getStatsByMonitorId(
+ authToken,
+ monitorId,
+ "desc",
+ 50,
+ null,
+ null,
+ null
);
-
- setMonitor(res.data.data);
- setAudits(res.data.data.checks[0].audits);
+ setMonitor(res?.data?.data ?? {});
+ setAudits(res?.data?.data?.checks?.[0]?.audits ?? []);
} catch (error) {
- console.error("Error fetching pagespeed monitor of id: " + monitorId);
- navigate("/not-found");
+ logger.error(logger);
+ navigate("/not-found", { replace: true });
}
};
fetchMonitor();
- }, [monitorId]);
+ }, [monitorId, authToken, navigate]);
/**
* Weight constants for different performance metrics.
@@ -306,21 +319,11 @@ const PageSpeedDetails = () => {
) : (
<>
- }
- onClick={() => navigate("/pagespeed")}
- sx={{
- width: "fit-content",
- backgroundColor: theme.palette.otherColors.fillGray,
- px: theme.gap.ml,
- "& svg.MuiSvgIcon-root": {
- mr: theme.gap.small,
- fill: theme.palette.otherColors.slateGray,
- },
- }}
+
{monitor?.status ? : }
@@ -662,4 +665,8 @@ const PageSpeedDetails = () => {
);
};
+PageSpeedDetails.propTypes = {
+ push: PropTypes.func,
+};
+
export default PageSpeedDetails;
diff --git a/Client/src/Pages/PageSpeed/index.jsx b/Client/src/Pages/PageSpeed/index.jsx
index 771a692c1..368ae2062 100644
--- a/Client/src/Pages/PageSpeed/index.jsx
+++ b/Client/src/Pages/PageSpeed/index.jsx
@@ -11,6 +11,7 @@ import "./index.css";
import Button from "../../Components/Button";
import { useNavigate } from "react-router";
import { getLastChecked } from "../../Utils/monitorUtils";
+import PropTypes from "prop-types";
const Card = ({ data }) => {
const theme = useTheme();
@@ -57,6 +58,10 @@ const Card = ({ data }) => {
);
};
+Card.propTypes = {
+ data: PropTypes.object.isRequired,
+};
+
/**
* Renders a skeleton layout.
*
@@ -129,7 +134,7 @@ const PageSpeed = () => {
);
useEffect(() => {
dispatch(getPageSpeedByUserId(authToken));
- }, []);
+ }, [authToken, dispatch]);
// will show skeletons only on initial load
// since monitor state is being added to redux persist, there's no reason to display skeletons on every render
diff --git a/Client/src/Pages/Settings/index.jsx b/Client/src/Pages/Settings/index.jsx
index 3ee029675..2b6f40682 100644
--- a/Client/src/Pages/Settings/index.jsx
+++ b/Client/src/Pages/Settings/index.jsx
@@ -4,7 +4,7 @@ import Button from "../../Components/Button";
import Field from "../../Components/Inputs/Field";
import Link from "../../Components/Link";
import Select from "../../Components/Inputs/Select";
-
+import { logger } from "../../Utils/Logger";
import "./index.css";
const Settings = () => {
@@ -40,14 +40,14 @@ const Settings = () => {
id="display-timezone"
label="Display timezone"
value="est"
- onChange={() => console.log("disabled")}
+ onChange={() => logger.warn("disabled")}
items={[{ _id: "est", name: "America / Toronto" }]}
/>
console.log("disabled")}
+ onChange={() => logger.warn("disabled")}
items={[{ _id: "est", name: "America / Toronto" }]}
/>
@@ -74,7 +74,7 @@ const Settings = () => {
optionalLabel="0 for infinite"
placeholder="90"
value=""
- onChange={() => console.log("Disabled")}
+ onChange={() => logger.warn("Disabled")}
/>
Clear all stats. This is irreversible.
diff --git a/Client/src/Pages/Status/index.jsx b/Client/src/Pages/Status/index.jsx
index 474375ede..d280132c4 100644
--- a/Client/src/Pages/Status/index.jsx
+++ b/Client/src/Pages/Status/index.jsx
@@ -1,9 +1,6 @@
import Fallback from "../../Components/Fallback";
-import { useTheme } from "@emotion/react";
const Status = () => {
- const theme = useTheme();
-
return (
{};
+
+ if (logLevel === "none") {
+ this.error = NO_OP;
+ this.warn = NO_OP;
+ this.log = NO_OP;
+ return;
+ }
+
+ this.error = console.error.bind(console);
+
+ if (logLevel === "error") {
+ this.warn = NO_OP;
+ this.log = NO_OP;
+ return;
+ }
+ this.warn = console.warn.bind(console);
+
+ if (logLevel === "warn") {
+ this.log = NO_OP;
+ return;
+ }
+ this.log = console.log.bind(console);
+ }
+}
+
+export const logger = new Logger(LOG_LEVEL);
diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js
new file mode 100644
index 000000000..32fcb6aa8
--- /dev/null
+++ b/Client/src/Utils/NetworkService.js
@@ -0,0 +1,432 @@
+import axios from "axios";
+const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL;
+import { logger } from "./Logger";
+class NetworkService {
+ constructor(store) {
+ this.store = store;
+ this.axiosInstance = axios.create({ baseURL: BASE_URL });
+ this.axiosInstance.interceptors.response.use(
+ (response) => response,
+ (error) => {
+ logger.error(error);
+ if (error.response && error.response.status === 401) {
+ logger.error("Invalid token received");
+ }
+ return Promise.reject(error);
+ }
+ );
+ }
+
+ /**
+ *
+ * ************************************
+ * Create a new monitor
+ * ************************************
+ *
+ * @async
+ * @param {string} authToken - The authorization token to be used in the request header.
+ * @param {Object} monitor - The monitor object to be sent in the request body.
+ * @returns {Promise} The response from the axios POST request.
+ */
+ async createMonitor(authToken, monitor) {
+ return this.axiosInstance.post(`/monitors`, monitor, {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ "Content-Type": "application/json",
+ },
+ });
+ }
+
+ /**
+ * ************************************
+ * Get all uptime monitors for a user
+ * ************************************
+ *
+ * @async
+ * @param {string} authToken - The authorization token to be used in the request header.
+ * @param {string} userId - The ID of the user whose monitors are to be retrieved.
+ * @param {number} [limit] - The maximum number of monitors to retrieve.
+ * @param {Array} [types] - The types of monitors to retrieve.
+ * @param {string} [status] - The status of the monitors to retrieve.
+ * @param {string} [sortOrder] - The order in which to sort the retrieved monitors.
+ * @param {boolean} [normalize] - Whether to normalize the retrieved monitors.
+ * @returns {Promise} The response from the axios GET request.
+ */
+ async getMonitorsByUserId(
+ authToken,
+ userId,
+ limit,
+ types,
+ status,
+ sortOrder,
+ normalize
+ ) {
+ const params = new URLSearchParams();
+
+ if (limit) params.append("limit", limit);
+ if (types) {
+ types.forEach((type) => {
+ params.append("type", type);
+ });
+ }
+ if (status) params.append("status", status);
+ if (sortOrder) params.append("sortOrder", sortOrder);
+ if (normalize) params.append("normalize", normalize);
+
+ return this.axiosInstance.get(
+ `/monitors/user/${userId}?${params.toString()}`,
+ {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ }
+
+ /**
+ * ************************************
+ * Get stats for a monitor
+ * ************************************
+ *
+ * @async
+ * @param {string} authToken - The authorization token to be used in the request header.
+ * @param {string} monitorId - The ID of the monitor whose statistics are to be retrieved.
+ * @param {string} [sortOrder] - The order in which to sort the retrieved statistics.
+ * @param {number} [limit] - The maximum number of statistics to retrieve.
+ * @param {string} [dateRange] - The date range for which to retrieve statistics.
+ * @param {number} [numToDisplay] - The number of statistics to display.
+ * @param {boolean} [normalize] - Whether to normalize the retrieved statistics.
+ * @returns {Promise} The response from the axios GET request.
+ */
+ async getStatsByMonitorId(
+ authToken,
+ monitorId,
+ sortOrder,
+ limit,
+ dateRange,
+ numToDisplay,
+ normalize
+ ) {
+ const params = new URLSearchParams();
+ if (sortOrder) params.append("sortOrder", sortOrder);
+ if (limit) params.append("limit", limit);
+ if (dateRange) params.append("dateRange", dateRange);
+ if (numToDisplay) params.append("numToDisplay", numToDisplay);
+ if (normalize) params.append("normalize", normalize);
+
+ return this.axiosInstance.get(
+ `/monitors/stats/${monitorId}?${params.toString()}`,
+ {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ },
+ }
+ );
+ }
+
+ /**
+ * ************************************
+ * Updates a single monitor
+ * ************************************
+ *
+ * @async
+ * @param {string} authToken - The authorization token to be used in the request header.
+ * @param {string} monitorId - The ID of the monitor to be updated.
+ * @param {Object} updatedFields - The fields to be updated for the monitor.
+ * @returns {Promise} The response from the axios PUT request.
+ */
+ async updateMonitor(authToken, monitorId, updatedFields) {
+ return this.axiosInstance.put(`/monitors/${monitorId}`, updatedFields, {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ "Content-Type": "application/json",
+ },
+ });
+ }
+
+ /**
+ * ************************************
+ * Deletes a single monitor by its ID
+ * ************************************
+ *
+ * @async
+ * @param {string} authToken - The authorization token to be used in the request header.
+ * @param {string} monitorId - The ID of the monitor to be deleted.
+ * @returns {Promise} The response from the axios DELETE request.
+ */
+ async deleteMonitorById(authToken, monitorId) {
+ return this.axiosInstance.delete(`/monitors/${monitorId}`, {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ "Content-Type": "application/json",
+ },
+ });
+ }
+
+ /**
+ * ************************************
+ * Gets the certificate expiry for a monitor
+ * ************************************
+ *
+ * @async
+ * @param {string} authToken - The authorization token to be used in the request header.
+ * @param {string} monitorId - The ID of the monitor whose certificate expiry is to be retrieved.
+ * @returns {Promise} The response from the axios GET request.
+ *
+ */
+ async getCertificateExpiry(authToken, monitorId) {
+ return this.axiosInstance.get(`/monitors/certificate/${monitorId}`, {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ },
+ });
+ }
+
+ /**
+ * ************************************
+ * Registers a new user
+ * ************************************
+ *
+ * @async
+ * @param {Object} form - The form data for the new user to be registered.
+ * @returns {Promise} The response from the axios POST request.
+ */
+ async registerUser(form) {
+ return this.axiosInstance.post(`/auth/register`, form);
+ }
+
+ /**
+ * ************************************
+ * Logs in a user
+ * ************************************
+ *
+ * @async
+ * @param {Object} form - The form data for the user to be logged in.
+ * @returns {Promise} The response from the axios POST request.
+ *
+ */
+ async loginUser(form) {
+ return this.axiosInstance.post(`/auth/login`, form);
+ }
+
+ /**
+ * ************************************
+ * Updates a user
+ * ************************************
+ *
+ * @async
+ * @param {string} authToken - The authorization token to be used in the request header.
+ * @param {string} userId - The ID of the user to be updated.
+ * @param {Object} form - The form data for the user to be updated.
+ * @returns {Promise} The response from the axios PUT request.
+ *
+ */
+ async updateUser(authToken, userId, form) {
+ return this.axiosInstance.put(`/auth/user/${userId}`, form, {
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ "Content-Type": "application/json",
+ },
+ });
+ }
+
+ /**
+ * ************************************
+ * Forgot password request
+ * ************************************
+ *
+ * @async
+ * @param {Object} form - The form data for the password recovery request.
+ * @returns {Promise} The response from the axios POST request.
+ *
+ */
+ async forgotPassword(form) {
+ return this.axiosInstance.post(`/auth/recovery/request`, form);
+ }
+
+ /**
+ * ************************************
+ * Validates a recovery token
+ * ************************************
+ *
+ * @async
+ * @param {string} recoveryToken - The recovery token to be validated.
+ * @returns {Promise} The response from the axios POST request.
+ *
+ */
+ async validateRecoveryToken(recoveryToken) {
+ return this.axiosInstance.post("/auth/recovery/validate", {
+ recoveryToken,
+ });
+ }
+
+ /**
+ * ************************************
+ * Requests password recovery
+ * ************************************
+ *
+ * @async
+ * @param {Object} form - The form data for the password recovery request.
+ * @returns {Promise} The response from the axios POST request.
+ *
+ */
+ async setNewPassword(recoveryToken, form) {
+ return this.axiosInstance.post("/auth/recovery/reset", {
+ ...form,
+ recoveryToken,
+ });
+ }
+
+ /**
+ * ************************************
+ * Checks if an admin user exists
+ * ************************************
+ *
+ * @async
+ * @returns {Promise} The response from the axios GET request.
+ *
+ */
+ async doesAdminExist() {
+ return this.axiosInstance.get("/auth/users/admin");
+ }
+
+ /**
+ * ************************************
+ * Get all users
+ * ************************************
+ *
+ * @async
+ * @param {string} authToken - The authorization token to be used in the request header.
+ * @returns {Promise} The response from the axios GET request.
+ *
+ */
+ async getAllUsers(authToken) {
+ return this.axiosInstance.get("/auth/users", {
+ headers: { Authorization: `Bearer ${authToken}` },
+ });
+ }
+
+ /**
+ * ************************************
+ * Requests an invitation token
+ * ************************************
+ *
+ * @async
+ * @param {string} authToken - The authorization token to be used in the request header.
+ * @param {string} email - The email of the user to be invited.
+ * @param {string} role - The role of the user to be invited.
+ * @returns {Promise} The response from the axios POST request.
+ *
+ */
+ async requestInvitationToken(authToken, email, role) {
+ return this.axiosInstance.post(
+ `/auth/invite`,
+ { email, role },
+ {
+ headers: { Authorization: `Bearer ${authToken}` },
+ }
+ );
+ }
+
+ /**
+ * ************************************
+ * Verifies an invitation token
+ * ************************************
+ *
+ * @async
+ * @param {string} token - The invitation token to be verified.
+ * @returns {Promise} The response from the axios POST request.
+ *
+ */
+ async verifyInvitationToken(token) {
+ return this.axiosInstance.post(`/auth/invite/verify`, {
+ token,
+ });
+ }
+
+ /**
+ * ************************************
+ * Get all checks for a given monitor
+ * ************************************
+ *
+ * @async
+ * @param {string} authToken - The authorization token to be used in the request header.
+ * @param {string} monitorId - The ID of the monitor.
+ * @param {string} sortOrder - The order in which to sort the checks.
+ * @param {number} limit - The maximum number of checks to retrieve.
+ * @param {string} dateRange - The range of dates for which to retrieve checks.
+ * @param {string} filter - The filter to apply to the checks.
+ * @param {number} page - The page number to retrieve in a paginated list.
+ * @param {number} rowsPerPage - The number of rows per page in a paginated list.
+ * @returns {Promise} The response from the axios GET request.
+ *
+ */
+
+ async getChecksByMonitor(
+ authToken,
+ monitorId,
+ sortOrder,
+ limit,
+ dateRange,
+ filter,
+ page,
+ rowsPerPage
+ ) {
+ const params = new URLSearchParams();
+ if (sortOrder) params.append("sortOrder", sortOrder);
+ if (limit) params.append("limit", limit);
+ if (dateRange) params.append("dateRange", dateRange);
+ if (filter) params.append("filter", filter);
+ if (page) params.append("page", page);
+ if (rowsPerPage) params.append("rowsPerPage", rowsPerPage);
+
+ return this.axiosInstance.get(`/checks/${monitorId}?${params.toString()}`, {
+ headers: { Authorization: `Bearer ${authToken}` },
+ });
+ }
+
+ /**
+ * ************************************
+ * Get all checks for a given user
+ * ************************************
+ *
+ * @async
+ * @param {string} authToken - The authorization token to be used in the request header.
+ * @param {string} userId - The ID of the user.
+ * @param {string} sortOrder - The order in which to sort the checks.
+ * @param {number} limit - The maximum number of checks to retrieve.
+ * @param {string} dateRange - The range of dates for which to retrieve checks.
+ * @param {string} filter - The filter to apply to the checks.
+ * @param {number} page - The page number to retrieve in a paginated list.
+ * @param {number} rowsPerPage - The number of rows per page in a paginated list.
+ * @returns {Promise} The response from the axios GET request.
+ *
+ */
+ async getChecksByUser(
+ authToken,
+ userId,
+ sortOrder,
+ limit,
+ dateRange,
+ filter,
+ page,
+ rowsPerPage
+ ) {
+ const params = new URLSearchParams();
+ if (sortOrder) params.append("sortOrder", sortOrder);
+ if (limit) params.append("limit", limit);
+ if (dateRange) params.append("dateRange", dateRange);
+ if (filter) params.append("filter", filter);
+ if (page) params.append("page", page);
+ if (rowsPerPage) params.append("rowsPerPage", rowsPerPage);
+ return this.axiosInstance.get(
+ `/checks/user/${userId}?${params.toString()}`,
+ {
+ headers: { Authorization: `Bearer ${authToken}` },
+ }
+ );
+ }
+}
+
+export default NetworkService;
diff --git a/Client/src/Utils/axiosConfig.js b/Client/src/Utils/axiosConfig.js
deleted file mode 100644
index 8d3040028..000000000
--- a/Client/src/Utils/axiosConfig.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import axios from "axios";
-import { clearAuthState } from "../Features/Auth/authSlice";
-const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL;
-
-let store;
-
-export const injectStore = (s) => {
- store = s;
-};
-
-const axiosInstance = axios.create({
- baseURL: BASE_URL,
-});
-
-axiosInstance.interceptors.response.use(
- (response) => response,
- (error) => {
- console.error(error);
- if (error.response && error.response.status === 401) {
- console.log("Invalid token revoked");
- store.dispatch(clearAuthState());
- }
- return Promise.reject(error);
- }
-);
-
-export default axiosInstance;
diff --git a/Client/src/assets/icons/bwu-abbreviated.svg b/Client/src/assets/icons/bwu-abbreviated.svg
new file mode 100644
index 000000000..a5e00f484
--- /dev/null
+++ b/Client/src/assets/icons/bwu-abbreviated.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/Client/src/assets/icons/left-arrow-double.svg b/Client/src/assets/icons/left-arrow-double.svg
new file mode 100644
index 000000000..6001a3ead
--- /dev/null
+++ b/Client/src/assets/icons/left-arrow-double.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/Client/src/assets/icons/left-arrow-long.svg b/Client/src/assets/icons/left-arrow-long.svg
new file mode 100644
index 000000000..db9b827ed
--- /dev/null
+++ b/Client/src/assets/icons/left-arrow-long.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/Client/src/assets/icons/left-arrow.svg b/Client/src/assets/icons/left-arrow.svg
index db9b827ed..8b38c408a 100644
--- a/Client/src/assets/icons/left-arrow.svg
+++ b/Client/src/assets/icons/left-arrow.svg
@@ -1,3 +1,3 @@
-
-
+
+
diff --git a/Client/src/assets/icons/right-arrow-double.svg b/Client/src/assets/icons/right-arrow-double.svg
new file mode 100644
index 000000000..4235134d7
--- /dev/null
+++ b/Client/src/assets/icons/right-arrow-double.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/Client/src/assets/icons/right-arrow.svg b/Client/src/assets/icons/right-arrow.svg
new file mode 100644
index 000000000..6b02062f2
--- /dev/null
+++ b/Client/src/assets/icons/right-arrow.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/Client/src/assets/icons/selector-vertical.svg b/Client/src/assets/icons/selector-vertical.svg
new file mode 100644
index 000000000..29d9a4998
--- /dev/null
+++ b/Client/src/assets/icons/selector-vertical.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/Client/src/index.css b/Client/src/index.css
index 58e26ed71..1c80b4dab 100644
--- a/Client/src/index.css
+++ b/Client/src/index.css
@@ -22,7 +22,7 @@
--env-var-font-familt-1: "Roboto", "Helvetica", "Arial", sans-serif;
--env-var-color-0: #fff;
- --env-var-color-1: #101828;
+ --env-var-color-1: #1c2130;
--env-var-color-2: #475467;
--env-var-color-2-light: #838c99;
--env-var-color-3: #1570ef;
@@ -67,7 +67,7 @@
--env-var-width-2: 360px;
--env-var-height-1: 100vh;
- --env-var-height-2: 34px;
+ --env-var-height-2: 36px;
--env-var-nav-bar-height: 70px;
--env-var-side-bar-width: 250px;
@@ -141,6 +141,10 @@ button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
+.MuiInputBase-root.Mui-disabled input {
+ cursor: not-allowed;
+}
+
.Toastify__toast-container {
width: auto;
}
diff --git a/Client/src/main.jsx b/Client/src/main.jsx
index f9fa3d640..00cec7d14 100644
--- a/Client/src/main.jsx
+++ b/Client/src/main.jsx
@@ -8,10 +8,8 @@ import { ThemeProvider } from "@mui/material";
import { Provider } from "react-redux";
import { persistor, store } from "./store";
import { PersistGate } from "redux-persist/integration/react";
-import { injectStore } from "./Utils/axiosConfig.js";
-
-injectStore(store);
-
+import NetworkService from "./Utils/NetworkService.js";
+export const networkService = new NetworkService(store);
ReactDOM.createRoot(document.getElementById("root")).render(
diff --git a/Client/src/store.js b/Client/src/store.js
index 27141202d..0c9a18b54 100644
--- a/Client/src/store.js
+++ b/Client/src/store.js
@@ -3,6 +3,7 @@ import { configureStore, combineReducers } from "@reduxjs/toolkit";
import uptimeMonitorsReducer from "./Features/UptimeMonitors/uptimeMonitorsSlice";
import pageSpeedMonitorReducer from "./Features/PageSpeedMonitor/pageSpeedMonitorSlice";
import authReducer from "./Features/Auth/authSlice";
+import uiReducer from "./Features/UI/uiSlice";
import storage from "redux-persist/lib/storage";
import { persistReducer, persistStore, createTransform } from "redux-persist";
@@ -20,7 +21,7 @@ const authTransform = createTransform(
const persistConfig = {
key: "root",
storage,
- whitielist: ["auth", "monitors", "pageSpeed"],
+ whitielist: ["auth", "monitors", "pageSpeed", "ui"],
transforms: [authTransform],
};
@@ -28,6 +29,7 @@ const rootReducer = combineReducers({
uptimeMonitors: uptimeMonitorsReducer,
auth: authReducer,
pageSpeedMonitors: pageSpeedMonitorReducer,
+ ui: uiReducer,
});
const persistedReducer = persistReducer(persistConfig, rootReducer);
diff --git a/Docker/client.Dockerfile b/Docker/client.Dockerfile
index 61b090ed7..5b43527f2 100644
--- a/Docker/client.Dockerfile
+++ b/Docker/client.Dockerfile
@@ -1,4 +1,4 @@
-FROM node:20-alpine
+FROM node:20-alpine as build
WORKDIR /app
@@ -10,8 +10,12 @@ COPY ./Client .
RUN npm run build
-RUN npm install -g serve
+# RUN npm install -g serve
-CMD ["serve","-s", "dist", "-l", "5173"]
+# CMD ["serve","-s", "dist", "-l", "5173"]
-EXPOSE 5000
+FROM nginx:1.27.1-alpine
+
+COPY --from=build /app/dist /usr/share/nginx/html
+
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/Docker/docker-compose.yaml b/Docker/docker-compose.yaml
index a60345765..1abbce9d4 100644
--- a/Docker/docker-compose.yaml
+++ b/Docker/docker-compose.yaml
@@ -3,7 +3,7 @@ services:
client:
image: uptime_client:latest
ports:
- - "80:5173"
+ - "80:80"
depends_on:
- server
server:
diff --git a/README.md b/README.md
index 2d0db36fc..adb9bc68f 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
An open source server monitoring application
-
+
BlueWave Uptime is an open source server monitoring application used to track the operational status and performance of servers and websites. It regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time.
@@ -22,6 +22,7 @@ BlueWave Uptime is an open source server monitoring application used to track th
- [x] Ping monitoring
- [x] Incidents at a glance
- [x] Page speed monitoring
+- [x] E-mail notifications
- [ ] Scheduled maintenance (in the works)
**Roadmap (short term):**
@@ -59,6 +60,15 @@ Made with [contrib.rocks](https://contrib.rocks).

+[](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
+
+Also check other developer and contributor-friendly projects of BlueWave:
+
+- [BlueWave HRM](https://github.com/bluewave-labs/bluewave-hrm)
+- [BlueWave Onboarding](https://github.com/bluewave-labs/bluewave-onboarding)
+- [BlueWave DataRoom](https://github.com/bluewave-labs/bluewave-dataroom)
+- [BlueWave ChatFabrica](https://github.com/bluewave-labs/bluewave-chatfabrica)
+
## Getting Started
- Clone this repository to your local machine
@@ -180,6 +190,7 @@ SYSTEM_EMAIL_PASSWORD=
```
VITE_APP_API_BASE_URL="http://localhost:5000/api/v1"
+VITE_APP_API_LOG_LEVEL="debug"
```
4. In the `Docker` directory run `docker compose up` to run the `docker-compose.yaml` file and start all four images.
@@ -200,9 +211,10 @@ That's it, the application is ready to use on port 80.
##### Environmental Variables
-| ENV Variable Name | Required/Optional | Type | Description | Accepted Values |
-| --------------------- | ----------------- | -------- | ------------------ | --------------- |
-| VITE_APP_API_BASE_URL | Required | `string` | Base URL of server | {host}/api/v1 |
+| ENV Variable Name | Required/Optional | Type | Description | Accepted Values |
+| --------------------- | ----------------- | -------- | ------------------ | ---------------------------------- |
+| VITE_APP_API_BASE_URL | Required | `string` | Base URL of server | {host}/api/v1 |
+| VITE_APP_LOG_LEVEL | Optional | `string` | Log level | `"none"`\|`"error"` \| `"warn"` \| |
diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js
index 93c83c6b8..4b5f51121 100644
--- a/Server/controllers/authController.js
+++ b/Server/controllers/authController.js
@@ -450,6 +450,7 @@ const deleteUserController = async (req, res, next) => {
}
// 1. Find all the monitors associated with the user id
+
const monitors = await req.db.getMonitorsByUserId({
params: { userId: _id },
});
@@ -459,6 +460,7 @@ const deleteUserController = async (req, res, next) => {
await Promise.all(
monitors.map(async (monitor) => {
await req.jobQueue.deleteJob(monitor);
+
await req.db.deleteChecks(monitor._id);
await req.db.deleteAlertByMonitorId(monitor._id);
await req.db.deletePageSpeedChecksByMonitorId(monitor._id);
diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js
index ac4e92da7..37aff6c96 100644
--- a/Server/controllers/monitorController.js
+++ b/Server/controllers/monitorController.js
@@ -303,7 +303,8 @@ const editMonitor = async (req, res, next) => {
}
try {
- const monitorBeforeEdit = await req.db.getMonitorById(req, res);
+ const { monitorId } = req.params;
+ const monitorBeforeEdit = await req.db.getMonitorById(monitorId);
// Get notifications from the request body
const notifications = req.body.notifications;
diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js
index 0fc56c755..9778b6224 100644
--- a/Server/db/mongo/modules/monitorModule.js
+++ b/Server/db/mongo/modules/monitorModule.js
@@ -172,6 +172,9 @@ const getMonitorStatsById = async (req) => {
// Get monitor
const monitor = await Monitor.findById(monitorId);
+ if (monitor === null || monitor === undefined) {
+ throw new Error(errorMessages.DB_FIND_MONTIOR_BY_ID(monitorId));
+ }
// Determine if this is a pagespeed monitor or an http/ping monitor
let model =
@@ -260,10 +263,9 @@ const getMonitorStatsById = async (req) => {
monitorStats.latestResponseTime = getLatestResponseTime(checksAll);
monitorStats.incidents = incidents;
monitorStats.checks = dateRangeChecks;
-
return monitorStats;
} catch (error) {
- console.log(error);
+ throw error;
}
};
@@ -294,7 +296,7 @@ const getMonitorById = async (monitorId) => {
*/
const getMonitorsByUserId = async (req, res) => {
try {
- let { limit, type, status, sortOrder, normalize } = req.query;
+ let { limit, type, status, sortOrder, normalize } = req.query || {};
const monitorQuery = { userId: req.params.userId };
if (type !== undefined) {
diff --git a/Server/db/mongo/modules/recoveryModule.js b/Server/db/mongo/modules/recoveryModule.js
index c1cc23b94..b643a1664 100644
--- a/Server/db/mongo/modules/recoveryModule.js
+++ b/Server/db/mongo/modules/recoveryModule.js
@@ -1,3 +1,4 @@
+const UserModel = require("../../../models/user");
const RecoveryToken = require("../../../models/RecoveryToken");
const crypto = require("crypto");
const { errorMessages } = require("../../../utils/messages");
diff --git a/Server/index.js b/Server/index.js
index 2cb941ed4..9a5f1797f 100644
--- a/Server/index.js
+++ b/Server/index.js
@@ -19,7 +19,7 @@ const JobQueue = require("./service/jobQueue");
const NetworkService = require("./service/networkService");
const EmailService = require("./service/emailService");
const PageSpeedService = require("./service/pageSpeedService");
-
+const SERVICE_NAME = "Server";
let cleaningUp = false;
// Need to wrap server setup in a function to handle async nature of JobQueue
diff --git a/Server/routes/checkRoute.js b/Server/routes/checkRoute.js
index 69aa682af..4bba24e79 100644
--- a/Server/routes/checkRoute.js
+++ b/Server/routes/checkRoute.js
@@ -15,12 +15,12 @@ router.get(
checkController.getChecks
);
+router.get("/user/:userId", checkController.getUserChecks);
+
router.delete(
"/:monitorId",
verifyOwnership(Monitor, "monitorId"),
checkController.deleteChecks
);
-router.get("/user/:userId", checkController.getUserChecks);
-
module.exports = router;
diff --git a/Server/service/networkService.js b/Server/service/networkService.js
index 375380f2e..b688b6fff 100644
--- a/Server/service/networkService.js
+++ b/Server/service/networkService.js
@@ -16,6 +16,14 @@ class NetworkService {
async handleNotification(job, isAlive) {
const { _id } = job.data;
const monitor = await this.db.getMonitorById(_id);
+ if (monitor === null || monitor === undefined) {
+ logger.error(`Null Monitor: ${_id}`, {
+ method: "handleNotification",
+ service: this.SERVICE_NAME,
+ jobId: job.id,
+ });
+ return;
+ }
// If monitor status changes, update monitor status and send notification
if (monitor.status !== isAlive) {
diff --git a/Server/utils/dataUtils.js b/Server/utils/dataUtils.js
index f1c4b39b2..b1180802b 100644
--- a/Server/utils/dataUtils.js
+++ b/Server/utils/dataUtils.js
@@ -40,7 +40,7 @@ const NormalizeData = (checks, rangeMin, rangeMax) => {
return normalizedChecks;
} else {
return checks.map((check) => {
- return { ...check, originalResponseTime: check.responseTime };
+ return { ...check._doc, originalResponseTime: check.responseTime };
});
}
};