diff --git a/Client/src/Components/Animated/PulseDot.jsx b/Client/src/Components/Animated/PulseDot.jsx
new file mode 100644
index 000000000..38e956b50
--- /dev/null
+++ b/Client/src/Components/Animated/PulseDot.jsx
@@ -0,0 +1,62 @@
+import PropTypes from "prop-types";
+import { Box, Stack } from "@mui/material";
+
+/**
+ * A component that renders a pulsating dot with a specified color.
+ *
+ * @component
+ * @example
+ * // Example usage:
+ *
+ *
+ * @param {Object} props
+ * @param {string} props.color - The color of the dot.
+ * @returns {JSX.Element} The PulseDot component.
+ */
+
+const PulseDot = ({ color }) => {
+ return (
+
+
+
+ );
+};
+
+PulseDot.propTypes = {
+ color: PropTypes.string.isRequired,
+};
+
+export default PulseDot;
diff --git a/Client/src/Components/BasicTable/index.css b/Client/src/Components/BasicTable/index.css
index 604dc412e..47a47439d 100644
--- a/Client/src/Components/BasicTable/index.css
+++ b/Client/src/Components/BasicTable/index.css
@@ -1,22 +1,12 @@
-.MuiTable-root .host a {
- width: var(--env-var-img-width-2);
- height: var(--env-var-img-width-2);
- color: var(--env-var-color-5);
- margin-right: 10px;
-}
-.MuiTable-root .host a svg {
- width: var(--env-var-font-size-large);
- height: var(--env-var-font-size-large);
-}
-.MuiTable-root .host div:nth-child(2) {
+.MuiTable-root .host {
width: fit-content;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
-.MuiTable-root .host div:nth-child(2) span {
+.MuiTable-root .host span {
font-size: var(--env-var-font-size-small);
- margin-left: 10px;
+ margin-left: 8px;
}
.MuiTable-root .label {
@@ -128,6 +118,7 @@
.MuiTablePagination-root p {
color: var(--env-var-color-2);
font-weight: 500;
+ font-size: var(--env-var-font-size-small-plus);
}
.MuiTablePagination-root button,
.MuiTablePagination-root .MuiSelect-select {
diff --git a/Client/src/Components/BasicTable/index.jsx b/Client/src/Components/BasicTable/index.jsx
index 8b6cafd91..bef5bd651 100644
--- a/Client/src/Components/BasicTable/index.jsx
+++ b/Client/src/Components/BasicTable/index.jsx
@@ -15,7 +15,6 @@ import {
Stack,
} from "@mui/material";
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";
@@ -90,6 +89,13 @@ const TablePaginationActions = (props) => {
);
};
+TablePaginationActions.propTypes = {
+ count: PropTypes.number.isRequired,
+ page: PropTypes.number.isRequired,
+ rowsPerPage: PropTypes.number.isRequired,
+ onPageChange: PropTypes.func.isRequired,
+};
+
/**
* BasicTable Component
* Renders a table with optional pagination.
@@ -143,13 +149,12 @@ const TablePaginationActions = (props) => {
*
*/
-const BasicTable = ({ data, paginated, reversed }) => {
+const BasicTable = ({ data, paginated, reversed, table }) => {
+ const DEFAULT_ROWS_PER_PAGE = 5;
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;
+ let rowsPerPage = uiState?.[table]?.rowsPerPage ?? DEFAULT_ROWS_PER_PAGE;
const [page, setPage] = useState(0);
useEffect(() => {
@@ -285,6 +290,7 @@ BasicTable.propTypes = {
paginated: PropTypes.bool,
reversed: PropTypes.bool,
rowPerPage: PropTypes.number,
+ table: PropTypes.string,
};
export default BasicTable;
diff --git a/Client/src/Components/Charts/BarChart/index.css b/Client/src/Components/Charts/BarChart/index.css
new file mode 100644
index 000000000..e386aef1c
--- /dev/null
+++ b/Client/src/Components/Charts/BarChart/index.css
@@ -0,0 +1,19 @@
+.bar-tooltip .MuiTooltip-tooltip {
+ background-color: white;
+ border: solid 1px black;
+ box-shadow: var(--env-var-shadow-1);
+ border: solid 1px var(--env-var-color-6);
+ border-radius: var(--env-var-radius-1);
+ padding: 4px 8px;
+}
+
+.bar-tooltip .MuiTooltip-tooltip p {
+ font-size: var(--env-var-font-size-small-plus);
+ color: var(--env-var-color-2);
+ font-weight: 500;
+}
+.bar-tooltip .MuiTooltip-tooltip span {
+ font-size: var(--env-var-font-size-small);
+ color: var(--env-var-color-2);
+ font-weight: 600;
+}
diff --git a/Client/src/Components/Charts/BarChart/index.jsx b/Client/src/Components/Charts/BarChart/index.jsx
new file mode 100644
index 000000000..9238374c2
--- /dev/null
+++ b/Client/src/Components/Charts/BarChart/index.jsx
@@ -0,0 +1,142 @@
+import { useTheme } from "@emotion/react";
+import { Box, Stack, Tooltip, Typography } from "@mui/material";
+import { formatDate } from "../../../Utils/timeUtils";
+import { useEffect, useState } from "react";
+import "./index.css";
+
+const BarChart = ({ checks = [] }) => {
+ const theme = useTheme();
+ const [animate, setAnimate] = useState(false);
+
+ useEffect(() => {
+ setAnimate(true);
+ });
+
+ // set responseTime to average if there's only one check
+ if (checks.length === 1) {
+ checks[0] = { ...checks[0], responseTime: 50 };
+ }
+
+ if (checks.length !== 25) {
+ const placeholders = Array(25 - checks.length).fill("placeholder");
+ checks = [...checks, ...placeholders];
+ }
+
+ return (
+ event.stopPropagation()}
+ sx={{
+ cursor: "default",
+ }}
+ >
+ {checks.map((check, index) =>
+ check === "placeholder" ? (
+
+ ) : (
+
+
+ {formatDate(new Date(check.createdAt), { year: undefined })}
+
+
+
+
+
+ Response Time
+
+
+ {check.originalResponseTime}
+
+ {" "}
+ ms
+
+
+
+
+ >
+ }
+ placement="top"
+ key={`check-${check?._id}`}
+ slotProps={{
+ popper: {
+ className: "bar-tooltip",
+ modifiers: [
+ {
+ name: "offset",
+ options: {
+ offset: [0, -10],
+ },
+ },
+ ],
+ },
+ }}
+ >
+ .MuiBox-root": {
+ filter: "brightness(0.8)",
+ },
+ }}
+ >
+
+
+
+ )
+ )}
+
+ );
+};
+
+export default BarChart;
diff --git a/Client/src/Components/Inputs/Field/index.jsx b/Client/src/Components/Inputs/Field/index.jsx
index 0b2fe56fe..335ed178f 100644
--- a/Client/src/Components/Inputs/Field/index.jsx
+++ b/Client/src/Components/Inputs/Field/index.jsx
@@ -48,6 +48,7 @@ const Field = forwardRef(
placeholder,
value,
onChange,
+ onInput,
error,
disabled,
},
@@ -87,6 +88,7 @@ const Field = forwardRef(
multiline={type === "description"}
rows={type === "description" ? 4 : 1}
value={value}
+ onInput={onInput}
onChange={onChange}
disabled={disabled}
inputRef={ref}
@@ -176,6 +178,8 @@ const Field = forwardRef(
}
);
+Field.displayName = "Field";
+
Field.propTypes = {
type: PropTypes.oneOf(["text", "password", "url", "email", "description"]),
id: PropTypes.string.isRequired,
@@ -189,6 +193,7 @@ Field.propTypes = {
placeholder: PropTypes.string,
value: PropTypes.string.isRequired,
onChange: PropTypes.func,
+ onInput: PropTypes.func,
error: PropTypes.string,
disabled: PropTypes.bool,
};
diff --git a/Client/src/Components/Sidebar/index.css b/Client/src/Components/Sidebar/index.css
index 10cdfc77d..8ce0089bc 100644
--- a/Client/src/Components/Sidebar/index.css
+++ b/Client/src/Components/Sidebar/index.css
@@ -76,16 +76,19 @@ aside .MuiCollapse-wrapperInner .MuiList-root > .MuiListItemButton-root::after {
content: "";
position: absolute;
left: -8px;
- top: 50%;
- transform: translateY(-50%);
+ top: 45%;
height: 3px;
width: 3px;
border-radius: 50%;
background-color: #d6d9dd;
}
-aside {
- transition: flex 300ms ease-out;
+aside
+ .MuiCollapse-wrapperInner
+ .MuiList-root
+ > .selected-path.MuiListItemButton-root::after {
+ background-color: var(--env-var-color-2);
+ transform: scale(1.2);
}
.sidebar-popup .MuiPaper-root {
@@ -95,7 +98,7 @@ aside {
gap: 1px;
}
.sidebar-popup .MuiList-root {
- padding: 6px 6px;
+ padding: 3px;
min-width: 100px;
}
.sidebar-popup li.MuiButtonBase-root {
@@ -104,21 +107,65 @@ aside {
padding: 5px 8px;
border-radius: var(--env-var-radius-1);
}
-.sidebar-popup li.MuiButtonBase-root:hover{
+.sidebar-popup li.MuiButtonBase-root:hover {
background-color: var(--env-var-color-13);
}
-.sidebar-popup li.MuiButtonBase-root:has(.MuiBox-root):hover{
+.sidebar-popup li.MuiButtonBase-root:has(.MuiBox-root):hover {
background-color: white;
}
-.sidebar-popup li.MuiButtonBase-root:has(.MuiBox-root){
+.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{
+.sidebar-popup svg {
width: 16px;
height: 16px;
opacity: 0.9;
}
+
+/* TRANSITIONS */
+aside {
+ flex: 1;
+ transition: max-width 650ms cubic-bezier(0.36, -0.01, 0, 0.77);
+}
+.home-layout aside.collapsed {
+ max-width: 64px;
+}
+
+aside.expanded .MuiTypography-root,
+aside.expanded p.MuiTypography-root,
+aside.expanded .MuiListItemText-root + svg,
+aside.expanded .MuiAvatar-root + .MuiBox-root + .MuiIconButton-root {
+ visibility: visible;
+ animation: fadeIn 1s ease;
+}
+
+aside.collapsed .MuiTypography-root,
+aside.collapsed p.MuiTypography-root,
+aside.collapsed .MuiListItemText-root + svg,
+aside.collapsed .MuiAvatar-root + .MuiBox-root + .MuiIconButton-root {
+ opacity: 0;
+ visibility: hidden;
+}
+
+aside .MuiListSubheader-root {
+ transition: all 200ms ease;
+}
+
+@keyframes fadeIn {
+ 0% {
+ opacity: 0;
+ visibility: hidden;
+ }
+ 30% {
+ opacity: 0;
+ visibility: hidden;
+ }
+ 100% {
+ opacity: 0.9;
+ visibility: visible;
+ }
+}
diff --git a/Client/src/Components/Sidebar/index.jsx b/Client/src/Components/Sidebar/index.jsx
index 69b44c125..ae6507b69 100644
--- a/Client/src/Components/Sidebar/index.jsx
+++ b/Client/src/Components/Sidebar/index.jsx
@@ -26,8 +26,6 @@ import LockSvg from "../../assets/icons/lock.svg?react";
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";
@@ -55,9 +53,9 @@ const menu = [
],
},
{ name: "Incidents", path: "incidents", icon: },
- { name: "Status pages", path: "status", icon: },
+ // { name: "Status pages", path: "status", icon: },
{ name: "Maintenance", path: "maintenance", icon: },
- { name: "Integrations", path: "integrations", icon: },
+ // { name: "Integrations", path: "integrations", icon: },
{
name: "Account",
icon: ,
@@ -125,24 +123,41 @@ function Sidebar() {
return (
-
- {collapsed ? (
-
- ) : (
-
- )}
+
+
+
+ BU
+
+
+ BlueWave Uptime
+
+
dispatch(toggleSidebar())}
+ onClick={() => {
+ setOpen({ Dashboard: false, Account: false });
+ dispatch(toggleSidebar());
+ }}
>
{collapsed ? : }
@@ -175,14 +193,13 @@ function Sidebar() {
id="nested-menu-subheader"
sx={{
pt: theme.gap.small,
- px: collapsed ? theme.gap.xs : theme.gap.ml,
- textAlign: collapsed ? "center" : "left",
+ px: collapsed ? theme.gap.xs : theme.gap.small,
}}
>
Menu
}
- sx={{ px: collapsed ? theme.gap.xs : theme.gap.ml }}
+ sx={{ px: theme.gap.medium }}
>
{menu.map((item) =>
item.path ? (
@@ -210,13 +227,14 @@ function Sidebar() {
}
onClick={() => navigate(`/${item.path}`)}
sx={{
- gap: theme.gap.medium,
+ height: "37px",
+ gap: theme.gap.small,
borderRadius: `${theme.shape.borderRadius}px`,
- justifyContent: collapsed ? "center" : "flex-start",
+ px: theme.gap.small,
}}
>
{item.icon}
- {!collapsed && {item.name}}
+ {item.name}
) : collapsed ? (
@@ -247,13 +265,13 @@ function Sidebar() {
onClick={(event) => openPopup(event, item.name)}
sx={{
position: "relative",
- gap: theme.gap.medium,
+ gap: theme.gap.small,
borderRadius: `${theme.shape.borderRadius}px`,
- justifyContent: collapsed ? "center" : "flex-start",
+ px: theme.gap.small,
}}
>
{item.icon}
- {!collapsed && {item.name}}
+ {item.name}
@@ -106,6 +129,7 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
placeholder="jordan.ellis@domain.com"
autoComplete="email"
value={form.email}
+ onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
onChange={onChange}
error={errors.email}
ref={inputRef}
diff --git a/Client/src/Pages/Auth/Register/Register.jsx b/Client/src/Pages/Auth/Register/Register.jsx
index 046aad64b..4680c1821 100644
--- a/Client/src/Pages/Auth/Register/Register.jsx
+++ b/Client/src/Pages/Auth/Register/Register.jsx
@@ -60,8 +60,31 @@ const LandingPage = ({ isAdmin, onSignup }) => {
By signing up, you agree to our{" "}
- Terms of Service and{" "}
- Privacy Policy.
+ {
+ window.open(
+ "https://bluewavelabs.ca/terms-of-service-open-source",
+ "_blank",
+ "noreferrer"
+ );
+ }}
+ >
+ Terms of Service
+ {" "}
+ and{" "}
+ {
+ window.open(
+ "https://bluewavelabs.ca/privacy-policy-open-source",
+ "_blank",
+ "noreferrer"
+ );
+ }}
+ >
+ Privacy Policy.
+
@@ -203,6 +226,7 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
placeholder="jordan.ellis@domain.com"
autoComplete="email"
value={form.email}
+ onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
onChange={onChange}
error={errors.email}
ref={inputRef}
diff --git a/Client/src/Pages/Monitors/Configure/index.jsx b/Client/src/Pages/Monitors/Configure/index.jsx
index 6b809433d..bd2a26df0 100644
--- a/Client/src/Pages/Monitors/Configure/index.jsx
+++ b/Client/src/Pages/Monitors/Configure/index.jsx
@@ -2,24 +2,23 @@ import { useNavigate, useParams } from "react-router";
import { useTheme } from "@emotion/react";
import { useDispatch, useSelector } from "react-redux";
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 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 { monitorValidation } from "../../../Validation/validation";
-import Select from "../../../Components/Inputs/Select";
import { createToast } from "../../../Utils/toastUtils";
+import { logger } from "../../../Utils/Logger";
import {
updateUptimeMonitor,
getUptimeMonitorsByUserId,
deleteUptimeMonitor,
} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
+import Button from "../../../Components/Button";
+import Field from "../../../Components/Inputs/Field";
+import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
+import Select from "../../../Components/Inputs/Select";
import Checkbox from "../../../Components/Inputs/Checkbox";
import Breadcrumbs from "../../../Components/Breadcrumbs";
+import PulseDot from "../../../Components/Animated/PulseDot";
import "./index.css";
-import { logger } from "../../../Utils/Logger";
/**
* Parses a URL string and returns a URL object.
@@ -229,8 +228,14 @@ const Configure = () => {
gap={theme.gap.large}
flex={1}
>
-
- {monitor?.status ? : }
+
+
{parsedUrl?.host ? (
diff --git a/Client/src/Pages/Monitors/Details/index.jsx b/Client/src/Pages/Monitors/Details/index.jsx
index b636444bd..98904587e 100644
--- a/Client/src/Pages/Monitors/Details/index.jsx
+++ b/Client/src/Pages/Monitors/Details/index.jsx
@@ -4,20 +4,19 @@ import { Box, Skeleton, Stack, Typography, useTheme } from "@mui/material";
import { useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import { networkService } from "../../../main";
-import MonitorDetailsAreaChart from "../../../Components/Charts/MonitorDetailsAreaChart";
-import ButtonGroup from "@mui/material/ButtonGroup";
-import Button from "../../../Components/Button";
-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";
-import PaginationTable from "./PaginationTable";
+import { logger } from "../../../Utils/Logger";
import {
formatDuration,
formatDurationRounded,
} from "../../../Utils/timeUtils";
-import "./index.css";
+import MonitorDetailsAreaChart from "../../../Components/Charts/MonitorDetailsAreaChart";
+import ButtonGroup from "@mui/material/ButtonGroup";
+import Button from "../../../Components/Button";
+import SettingsIcon from "../../../assets/icons/settings-bold.svg?react";
+import PaginationTable from "./PaginationTable";
import Breadcrumbs from "../../../Components/Breadcrumbs";
-import { logger } from "../../../Utils/Logger";
+import PulseDot from "../../../Components/Animated/PulseDot";
+import "./index.css";
const StatBox = ({ title, value }) => {
return (
@@ -171,8 +170,14 @@ const DetailsPage = () => {
]}
/>
-
- {monitor?.status ? : }
+
+
{monitor.url?.replace(/^https?:\/\//, "") || "..."}
@@ -325,9 +330,7 @@ const DetailsPage = () => {
History
- {/* TODO New Table */}
- {/* */}
>
diff --git a/Client/src/Pages/Monitors/index.jsx b/Client/src/Pages/Monitors/index.jsx
index dc293ef49..9eef7ec3a 100644
--- a/Client/src/Pages/Monitors/index.jsx
+++ b/Client/src/Pages/Monitors/index.jsx
@@ -10,11 +10,9 @@ import Button from "../../Components/Button";
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 BasicTable from "../../Components/BasicTable";
import { StatusLabel } from "../../Components/Label";
import { createToast } from "../../Utils/toastUtils";
-import ResponseTimeChart from "../../Components/Charts/ResponseTimeChart";
import {
Box,
IconButton,
@@ -28,6 +26,7 @@ import {
import Settings from "../../assets/icons/settings-bold.svg?react";
import PropTypes from "prop-types";
+import BarChart from "../../Components/Charts/BarChart";
const ActionsMenu = ({ monitor }) => {
const [anchorEl, setAnchorEl] = useState(null);
@@ -206,7 +205,6 @@ ActionsMenu.propTypes = {
*
* @component
* @param {Object} params - An object containing the following properties:
- * @param {string} params.url - The URL of the host.
* @param {string} params.title - The name of the host.
* @param {string} params.percentageColor - The color of the percentage text.
* @param {number} params.percentage - The percentage to display.
@@ -214,40 +212,17 @@ ActionsMenu.propTypes = {
*/
const Host = ({ params }) => {
return (
-
- {
- event.stopPropagation();
- window.open(params.url, "_blank", "noreferrer");
- }}
- sx={{
- "&:focus": {
- outline: "none",
- },
- mr: "5px",
- }}
- >
-
-
-
- {params.title}
-
- {params.percentage}%
-
-
+
+ {params.title}
+
+ {params.percentage}%
+
);
};
Host.propTypes = {
params: PropTypes.shape({
- url: PropTypes.string,
title: PropTypes.string,
percentageColor: PropTypes.string,
percentage: PropTypes.number,
@@ -326,7 +301,6 @@ const Monitors = () => {
data.rows = monitorState.monitors.map((monitor, idx) => {
const params = {
- url: monitor.url,
title: monitor.name,
percentage: 100,
percentageColor:
@@ -357,7 +331,7 @@ const Monitors = () => {
/>
),
},
- { id: idx + 2, data: },
+ { id: idx + 2, data: },
{
id: idx + 3,
data: (
@@ -437,6 +411,7 @@ const Monitors = () => {
{
{/* TODO - add search bar */}
-
+
>
)}
diff --git a/Client/src/Pages/PageSpeed/Configure/index.jsx b/Client/src/Pages/PageSpeed/Configure/index.jsx
index bc759db17..83484de7e 100644
--- a/Client/src/Pages/PageSpeed/Configure/index.jsx
+++ b/Client/src/Pages/PageSpeed/Configure/index.jsx
@@ -10,15 +10,14 @@ import {
} from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
import { monitorValidation } from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
+import { logger } from "../../../Utils/Logger";
import Button from "../../../Components/Button";
import Field from "../../../Components/Inputs/Field";
import Select from "../../../Components/Inputs/Select";
import Checkbox from "../../../Components/Inputs/Checkbox";
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
-import GreenCheck from "../../../assets/icons/checkbox-green.svg?react";
-import RedCheck from "../../../assets/icons/checkbox-red.svg?react";
import Breadcrumbs from "../../../Components/Breadcrumbs";
-import { logger } from "../../../Utils/Logger";
+import PulseDot from "../../../Components/Animated/PulseDot";
import "./index.css";
@@ -168,8 +167,14 @@ const PageSpeedConfigure = () => {
flex={1}
gap={theme.gap.large}
>
-
- {monitor?.status ? : }
+
+
{
const theme = useTheme();
@@ -325,8 +324,14 @@ const PageSpeedDetails = () => {
{ name: "details", path: `/pagespeed/${monitorId}` },
]}
/>
-
- {monitor?.status ? : }
+
+
{
return time;
};
-export const formatDate = (date) => {
+export const formatDate = (date, customOptions) => {
const options = {
year: "numeric",
month: "long",
@@ -51,6 +51,7 @@ export const formatDate = (date) => {
hour: "numeric",
minute: "numeric",
hour12: true,
+ ...customOptions
};
// Return the date using the specified options
diff --git a/Client/src/Utils/toastUtils.jsx b/Client/src/Utils/toastUtils.jsx
index 0f272a74e..ff3f341f8 100644
--- a/Client/src/Utils/toastUtils.jsx
+++ b/Client/src/Utils/toastUtils.jsx
@@ -1,5 +1,5 @@
import PropTypes from "prop-types";
-import { toast } from "react-toastify";
+import { toast, Slide } from "react-toastify";
import Alert from "../Components/Alert";
/**
@@ -23,6 +23,7 @@ export const createToast = ({
autoClose: 3000,
hideProgressBar: true,
closeButton: false,
+ transition: Slide,
...config,
};
diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js
index ab2501199..35d47d6b1 100644
--- a/Client/src/Validation/validation.js
+++ b/Client/src/Validation/validation.js
@@ -49,6 +49,13 @@ const credentials = joi.object({
.string()
.trim()
.email({ tlds: { allow: false } })
+ .custom((value, helpers) => {
+ const lowercasedValue = value.toLowerCase();
+ if (value !== lowercasedValue) {
+ return helpers.message("Email must be in lowercase");
+ }
+ return lowercasedValue;
+ })
.messages({
"string.empty": "Email is required",
"string.email": "Must be a valid email address",
@@ -72,14 +79,10 @@ const credentials = joi.object({
});
const monitorValidation = joi.object({
- url: joi
- .string()
- .uri({ allowRelative: true })
- .trim()
- .messages({
- "string.empty": "This field is required.",
- "string.uri": "The URL you provided is not valid.",
- }),
+ url: joi.string().uri({ allowRelative: true }).trim().messages({
+ "string.empty": "This field is required.",
+ "string.uri": "The URL you provided is not valid.",
+ }),
name: joi.string().trim().max(50).allow("").messages({
"string.max": "This field should not exceed the 50 characters limit.",
}),
diff --git a/Client/src/assets/icons/bwu-abbreviated.svg b/Client/src/assets/icons/bwu-abbreviated.svg
deleted file mode 100644
index a5e00f484..000000000
--- a/Client/src/assets/icons/bwu-abbreviated.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/Client/src/index.css b/Client/src/index.css
index 1c80b4dab..887d7da07 100644
--- a/Client/src/index.css
+++ b/Client/src/index.css
@@ -67,7 +67,7 @@
--env-var-width-2: 360px;
--env-var-height-1: 100vh;
- --env-var-height-2: 36px;
+ --env-var-height-2: 34px;
--env-var-nav-bar-height: 70px;
--env-var-side-bar-width: 250px;
@@ -168,6 +168,17 @@ body .MuiSkeleton-root {
background-color: var(--env-var-color-15);
}
+@keyframes ripple {
+ from {
+ opacity: 1;
+ transform: scale(0);
+ }
+ to {
+ opacity: 0;
+ transform: scale(2);
+ }
+}
+
@media (prefers-color-scheme: light) {
:root {
color: #213547;
diff --git a/Docker/.gitignore b/Docker/.gitignore
index e1b726025..89aa6bfac 100644
--- a/Docker/.gitignore
+++ b/Docker/.gitignore
@@ -2,4 +2,5 @@
!build_images.sh
mongo/data/*
redis/data/*
-*.env
\ No newline at end of file
+*.env
+certbot/*
\ No newline at end of file
diff --git a/Docker/client.Dockerfile b/Docker/client.Dockerfile
index 5b43527f2..0f7935cd1 100644
--- a/Docker/client.Dockerfile
+++ b/Docker/client.Dockerfile
@@ -16,6 +16,7 @@ RUN npm run build
FROM nginx:1.27.1-alpine
+# COPY ./Docker/nginx/default.conf /etc/nginx/conf.d/default.conf
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 1abbce9d4..c9fa3431f 100644
--- a/Docker/docker-compose.yaml
+++ b/Docker/docker-compose.yaml
@@ -1,11 +1,21 @@
-version: "3"
services:
client:
image: uptime_client:latest
ports:
- "80:80"
+ - "443:443"
depends_on:
- server
+ volumes:
+ - ./nginx/conf.d:/etc/nginx/conf.d/:ro
+ - ./certbot/www:/var/www/certbot/:ro
+ - ./certbot/conf/:/etc/nginx/ssl/:ro
+
+ certbot:
+ image: certbot/certbot:latest
+ volumes:
+ - ./certbot/www/:/var/www/certbot/:rw
+ - ./certbot/conf/:/etc/letsencrypt/:rw
server:
image: uptime_server:latest
ports:
diff --git a/Docker/nginx/conf.d/default.conf b/Docker/nginx/conf.d/default.conf
new file mode 100644
index 000000000..a04d32c87
--- /dev/null
+++ b/Docker/nginx/conf.d/default.conf
@@ -0,0 +1,51 @@
+server {
+ listen 80;
+ listen [::]:80;
+
+ server_name uptime-demo.bluewavelabs.ca;
+ server_tokens off;
+
+ location /.well-known/acme-challenge/ {
+ root /var/www/certbot;
+ }
+
+ location / {
+ root /usr/share/nginx/html;
+ index index.html index.htm;
+ try_files $uri $uri/ /index.html;
+ }
+
+ location /api/ {
+ proxy_pass http://server:5000/api/;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+}
+
+server {
+ listen 443 default_server ssl http2;
+ listen [::]:443 ssl http2;
+
+ server_name uptime-demo.bluewavelabs.ca;
+
+ ssl_certificate /etc/nginx/ssl/live/uptime-demo.bluewavelabs.ca/fullchain.pem;
+ ssl_certificate_key /etc/nginx/ssl/live/uptime-demo.bluewavelabs.ca/privkey.pem;
+
+ location / {
+ root /usr/share/nginx/html;
+ index index.html index.htm;
+ try_files $uri $uri/ /index.html;
+ }
+
+ location /api/ {
+ proxy_pass http://server:5000/api/;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+}
diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js
index 4b5f51121..6fbcdb669 100644
--- a/Server/controllers/authController.js
+++ b/Server/controllers/authController.js
@@ -90,7 +90,7 @@ const registerController = async (req, res, next) => {
const token = issueToken(userForToken);
- req.emailService.buildAndSendEmail(
+ await req.emailService.buildAndSendEmail(
"welcomeEmailTemplate",
{ name: newUser.firstName },
newUser.email,
@@ -240,7 +240,7 @@ const inviteController = async (req, res, next) => {
}
const inviteToken = await req.db.requestInviteToken(req, res);
- req.emailService.buildAndSendEmail(
+ await req.emailService.buildAndSendEmail(
"employeeActivationTemplate",
{
name: firstname,
diff --git a/Server/service/emailService.js b/Server/service/emailService.js
index 79d6213c6..27518484c 100644
--- a/Server/service/emailService.js
+++ b/Server/service/emailService.js
@@ -3,6 +3,8 @@ const path = require("path");
const nodemailer = require("nodemailer");
const { compile } = require("handlebars");
const mjml2html = require("mjml");
+const SERVICE_NAME = "EmailService";
+const logger = require("../utils/logger");
/**
* Represents an email service that can load templates, build, and send emails.
@@ -19,12 +21,19 @@ class EmailService {
* @returns {Function} A compiled template function that can be used to generate HTML email content.
*/
this.loadTemplate = (templateName) => {
- const templatePath = path.join(
- __dirname,
- `../templates/${templateName}.mjml`
- );
- const templateContent = fs.readFileSync(templatePath, "utf8");
- return compile(templateContent);
+ try {
+ const templatePath = path.join(
+ __dirname,
+ `../templates/${templateName}.mjml`
+ );
+ const templateContent = fs.readFileSync(templatePath, "utf8");
+ return compile(templateContent);
+ } catch (error) {
+ logger.error("Error loading Email templates", {
+ error,
+ service: this.SERVICE_NAME,
+ });
+ }
};
/**
@@ -67,25 +76,51 @@ class EmailService {
*/
buildAndSendEmail = async (template, context, to, subject) => {
const buildHtml = async (template, context) => {
- const mjml = this.templateLookup[template](context);
- const html = await mjml2html(mjml);
- return html.html;
+ try {
+ const mjml = this.templateLookup[template](context);
+ const html = await mjml2html(mjml);
+ return html.html;
+ } catch (error) {
+ logger.error("Error building Email HTML", {
+ error,
+ service: SERVICE_NAME,
+ });
+ }
};
const sendEmail = async (to, subject, html) => {
- const info = await this.transporter.sendMail({
- to: to,
- subject: subject,
- html: html,
- });
- return info;
+ try {
+ const info = await this.transporter.sendMail({
+ to: to,
+ subject: subject,
+ html: html,
+ });
+ return info;
+ } catch (error) {
+ logger.error("Error sending Email", {
+ error,
+ service: SERVICE_NAME,
+ });
+ }
};
- const info = await sendEmail(
- to,
- subject,
- await buildHtml(template, context)
- );
- return info.messageId;
+
+ try {
+ const info = await sendEmail(
+ to,
+ subject,
+ await buildHtml(template, context)
+ );
+ return info.messageId;
+ } catch (error) {
+ error.service = SERVICE_NAME;
+ if (error.method === undefined) {
+ error.method = "buildAndSendEmail";
+ }
+ logger.error("Error building and sending Email", {
+ error,
+ service: SERVICE_NAME,
+ });
+ }
};
}
diff --git a/Server/validation/joi.js b/Server/validation/joi.js
index fe7090083..4f8042e12 100644
--- a/Server/validation/joi.js
+++ b/Server/validation/joi.js
@@ -16,7 +16,17 @@ const roleValidatior = (role) => (value, helpers) => {
//****************************************
const loginValidation = joi.object({
- email: joi.string().email().required(),
+ email: joi
+ .string()
+ .email()
+ .required()
+ .custom((value, helpers) => {
+ const lowercasedValue = value.toLowerCase();
+ if (value !== lowercasedValue) {
+ return helpers.message("Email must be in lowercase");
+ }
+ return lowercasedValue;
+ }),
password: joi
.string()
.min(8)
@@ -35,7 +45,17 @@ const registerValidation = joi.object({
.string()
.required()
.pattern(/^[A-Za-z]+$/),
- email: joi.string().email().required(),
+ email: joi
+ .string()
+ .email()
+ .required()
+ .custom((value, helpers) => {
+ const lowercasedValue = value.toLowerCase();
+ if (value !== lowercasedValue) {
+ return helpers.message("Email must be in lowercase");
+ }
+ return lowercasedValue;
+ }),
password: joi
.string()
.min(8)
@@ -44,7 +64,11 @@ const registerValidation = joi.object({
/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*()])[A-Za-z0-9!@#$%^&*()]+$/
),
profileImage: joi.any(),
- role: joi.array().required(),
+ role: joi
+ .array()
+ .items(joi.string().valid("admin", "user"))
+ .min(1)
+ .required(),
});
const editUserParamValidation = joi.object({