From 2b425a27cf9172bfab336aea4c59c98d3246f532 Mon Sep 17 00:00:00 2001 From: Ananya Verma Date: Tue, 20 Aug 2024 17:44:26 +0530 Subject: [PATCH 01/30] resolve merge conflict --- Client/src/App.jsx | 4 ++++ .../Pages/Monitors/CreateMonitor/index.jsx | 21 +++++++++++++++++-- .../src/Pages/Monitors/Home/actionsMenu.jsx | 8 +++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/Client/src/App.jsx b/Client/src/App.jsx index 88e1e7019..6cda26892 100644 --- a/Client/src/App.jsx +++ b/Client/src/App.jsx @@ -59,6 +59,10 @@ function App() { path="/monitors/create" element={} /> + } + /> } diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.jsx b/Client/src/Pages/Monitors/CreateMonitor/index.jsx index bc0f55ed3..779bcf99f 100644 --- a/Client/src/Pages/Monitors/CreateMonitor/index.jsx +++ b/Client/src/Pages/Monitors/CreateMonitor/index.jsx @@ -1,9 +1,9 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Box, ButtonGroup, Stack, Typography } from "@mui/material"; import { useSelector, useDispatch } from "react-redux"; import { monitorValidation } from "../../../Validation/validation"; import { createUptimeMonitor } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import { useTheme } from "@emotion/react"; import { createToast } from "../../../Utils/toastUtils"; import { logger } from "../../../Utils/Logger"; @@ -19,6 +19,7 @@ import "./index.css"; const CreateMonitor = () => { const MS_PER_MINUTE = 60000; const { user, authToken } = useSelector((state) => state.auth); + const { monitors } = useSelector((state) => state.uptimeMonitors); const dispatch = useDispatch(); const navigate = useNavigate(); const theme = useTheme(); @@ -31,6 +32,7 @@ const CreateMonitor = () => { "notify-email-default": "notification-email", }; + const { monitorId } = useParams(); const [monitor, setMonitor] = useState({ url: "", name: "", @@ -41,6 +43,21 @@ const CreateMonitor = () => { const [https, setHttps] = useState(true); const [errors, setErrors] = useState({}); + useEffect(() => { + if(monitorId){ + const data = monitors.find((monitor) => monitor._id === monitorId); + if (!data) { + console.error("Error fetching monitor of id: " + monitorId); + navigate("/not-found"); + } + const {url, name, ...rest} = data; + rest.interval = rest.interval/60000; + setMonitor({ + ...rest, + }); + } + }, [monitorId, authToken, monitors]); + const handleChange = (event, name) => { const { value, id } = event.target; if (!name) name = idMap[id]; diff --git a/Client/src/Pages/Monitors/Home/actionsMenu.jsx b/Client/src/Pages/Monitors/Home/actionsMenu.jsx index 75274ba49..c4f1049a6 100644 --- a/Client/src/Pages/Monitors/Home/actionsMenu.jsx +++ b/Client/src/Pages/Monitors/Home/actionsMenu.jsx @@ -145,6 +145,14 @@ const ActionsMenu = ({ monitor, isAdmin }) => { Configure )} + { + e.stopPropagation(); + navigate(`/monitors/clone/${actions.id}`); + }} + > + Clone + {isAdmin && ( { From 20868135c1df33b8b735ae2296e4a11952b6a7ae Mon Sep 17 00:00:00 2001 From: Ananya Verma Date: Fri, 23 Aug 2024 00:28:21 +0530 Subject: [PATCH 02/30] remove pages/monitors/index.js --- Client/src/App.jsx | 6 +----- Client/src/Pages/Monitors/CreateMonitor/index.jsx | 8 +++++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Client/src/App.jsx b/Client/src/App.jsx index 6cda26892..405371db2 100644 --- a/Client/src/App.jsx +++ b/Client/src/App.jsx @@ -56,11 +56,7 @@ function App() { element={} /> } - /> - } /> { const data = monitors.find((monitor) => monitor._id === monitorId); if (!data) { console.error("Error fetching monitor of id: " + monitorId); - navigate("/not-found"); + navigate("/not-found", {replace: true}); } - const {url, name, ...rest} = data; - rest.interval = rest.interval/60000; + const {name, ...rest} = data; //data.name is read-only + if(rest.type == 'http') rest.url = rest.url.substring(8, rest.url.length) + rest.name = `${name} (Clone)`; + rest.interval /= 60000; setMonitor({ ...rest, }); From 2446b81e79efeab0be52ae67a9f35ff948660488 Mon Sep 17 00:00:00 2001 From: Alexander Holliday Date: Sat, 24 Aug 2024 17:48:35 -0700 Subject: [PATCH 03/30] Use MS_PER_MINUTE --- Client/src/Pages/Monitors/CreateMonitor/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.jsx b/Client/src/Pages/Monitors/CreateMonitor/index.jsx index c98c17e35..428248255 100644 --- a/Client/src/Pages/Monitors/CreateMonitor/index.jsx +++ b/Client/src/Pages/Monitors/CreateMonitor/index.jsx @@ -53,7 +53,7 @@ const CreateMonitor = () => { const {name, ...rest} = data; //data.name is read-only if(rest.type == 'http') rest.url = rest.url.substring(8, rest.url.length) rest.name = `${name} (Clone)`; - rest.interval /= 60000; + rest.interval /= MS_PER_MINUTE; setMonitor({ ...rest, }); From bc9e59315a8353399e39e7d67a3602490b77341c Mon Sep 17 00:00:00 2001 From: Ananya Verma Date: Tue, 27 Aug 2024 15:28:03 +0530 Subject: [PATCH 04/30] use URL instance to parse host from url --- Client/src/Pages/Monitors/CreateMonitor/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.jsx b/Client/src/Pages/Monitors/CreateMonitor/index.jsx index 428248255..f77ca5478 100644 --- a/Client/src/Pages/Monitors/CreateMonitor/index.jsx +++ b/Client/src/Pages/Monitors/CreateMonitor/index.jsx @@ -51,7 +51,7 @@ const CreateMonitor = () => { navigate("/not-found", {replace: true}); } const {name, ...rest} = data; //data.name is read-only - if(rest.type == 'http') rest.url = rest.url.substring(8, rest.url.length) + if(rest.type === 'http') rest.url = URL.parse(rest.url)?.host; rest.name = `${name} (Clone)`; rest.interval /= MS_PER_MINUTE; setMonitor({ From 89b042fc3d9c675d52eddade1039687d51b1fc26 Mon Sep 17 00:00:00 2001 From: Ananya Verma Date: Sat, 31 Aug 2024 01:26:30 +0530 Subject: [PATCH 05/30] replace url.parse() with new URL() --- Client/src/Pages/Monitors/CreateMonitor/index.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.jsx b/Client/src/Pages/Monitors/CreateMonitor/index.jsx index f77ca5478..e2944b659 100644 --- a/Client/src/Pages/Monitors/CreateMonitor/index.jsx +++ b/Client/src/Pages/Monitors/CreateMonitor/index.jsx @@ -51,7 +51,10 @@ const CreateMonitor = () => { navigate("/not-found", {replace: true}); } const {name, ...rest} = data; //data.name is read-only - if(rest.type === 'http') rest.url = URL.parse(rest.url)?.host; + if(rest.type === 'http'){ + const url = new URL(rest.url) + rest.url = url.host + } rest.name = `${name} (Clone)`; rest.interval /= MS_PER_MINUTE; setMonitor({ From 9c59330cb067cc6026c5498039157b288706f6b8 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Sep 2024 09:42:34 -0700 Subject: [PATCH 06/30] Fixed property name for check --- Server/service/jobQueue.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/service/jobQueue.js b/Server/service/jobQueue.js index 1c44c4062..240df8f1a 100644 --- a/Server/service/jobQueue.js +++ b/Server/service/jobQueue.js @@ -39,7 +39,7 @@ class JobQueue { queue.networkService = networkService; const monitors = await db.getAllMonitors(); for (const monitor of monitors) { - if (monitor.active) { + if (monitor.isActive) { await queue.addJob(monitor.id, monitor); } } From f85c2d001a8030cdea8b399965fdc22f87eb60aa Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Sep 2024 10:09:13 -0700 Subject: [PATCH 07/30] Resolve conflicts, update create monitor --- .../Pages/Monitors/CreateMonitor/index.jsx | 43 ++++++++++++------- .../src/Pages/Monitors/Home/actionsMenu.jsx | 2 +- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.jsx b/Client/src/Pages/Monitors/CreateMonitor/index.jsx index 408c5788d..36a63f0e6 100644 --- a/Client/src/Pages/Monitors/CreateMonitor/index.jsx +++ b/Client/src/Pages/Monitors/CreateMonitor/index.jsx @@ -13,6 +13,7 @@ import Field from "../../../Components/Inputs/Field"; import Select from "../../../Components/Inputs/Select"; import Checkbox from "../../../Components/Inputs/Checkbox"; import Breadcrumbs from "../../../Components/Breadcrumbs"; +import { getUptimeMonitorById } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; import "./index.css"; const CreateMonitor = () => { @@ -43,23 +44,33 @@ const CreateMonitor = () => { const [errors, setErrors] = useState({}); useEffect(() => { - if (monitorId) { - const data = monitors.find((monitor) => monitor._id === monitorId); - if (!data) { - console.error("Error fetching monitor of id: " + monitorId); - navigate("/not-found", { replace: true }); + const fetchMonitor = async () => { + if (monitorId) { + const action = await dispatch( + getUptimeMonitorById({ authToken, monitorId }) + ); + + if (action.payload.success) { + const data = action.payload.data; + const { name, ...rest } = data; //data.name is read-only + if (rest.type === "http") { + const url = new URL(rest.url); + rest.url = url.host; + } + rest.name = `${name} (Clone)`; + rest.interval /= MS_PER_MINUTE; + setMonitor({ + ...rest, + }); + } else { + navigate("/not-found", { replace: true }); + createToast({ + body: "There was an error cloning the monitor.", + }); + } } - const { name, ...rest } = data; //data.name is read-only - if (rest.type === "http") { - const url = new URL(rest.url); - rest.url = url.host; - } - rest.name = `${name} (Clone)`; - rest.interval /= MS_PER_MINUTE; - setMonitor({ - ...rest, - }); - } + }; + fetchMonitor(); }, [monitorId, authToken, monitors]); const handleChange = (event, name) => { diff --git a/Client/src/Pages/Monitors/Home/actionsMenu.jsx b/Client/src/Pages/Monitors/Home/actionsMenu.jsx index 5a25dfc90..7f2eb0d97 100644 --- a/Client/src/Pages/Monitors/Home/actionsMenu.jsx +++ b/Client/src/Pages/Monitors/Home/actionsMenu.jsx @@ -140,7 +140,7 @@ const ActionsMenu = ({ monitor, isAdmin, updateCallback }) => { { e.stopPropagation(); - navigate(`/monitors/clone/${actions.id}`); + navigate(`/monitors/create/${actions.id}`); }} > Clone From 7526fe2a683152e4212f37f7214b620e73661bf0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Sep 2024 10:52:17 -0700 Subject: [PATCH 08/30] fix delete user method --- Server/controllers/authController.js | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index 0cf297a47..69483f5e9 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -465,11 +465,10 @@ 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 }, + const result = await req.db.getMonitorsByTeamId({ + params: { teamId: user.teamId }, }); - - if (monitors) { + if (result?.monitors.length > 0) { //2. Remove all jobs, delete checks and alerts await Promise.all( monitors.map(async (monitor) => { @@ -484,22 +483,17 @@ const deleteUserController = async (req, res, next) => { // 3. Delete each monitor await req.db.deleteMonitorsByUserId(user._id); - - // 4. Delete the user by id - await req.db.deleteUser(user._id); - - return res.status(200).json({ - success: true, - msg: successMessages.AUTH_DELETE_USER, - }); - } else { - return res.status(404).json({ - success: false, - msg: errorMessages.MONITOR_GET_BY_USER_ID, - }); } + // 4. Delete the user by id + await req.db.deleteUser(user._id); + + return res.status(200).json({ + success: true, + msg: successMessages.AUTH_DELETE_USER, + }); } catch (error) { error.service = SERVICE_NAME; + error.method === undefined && error.method === "deleteUserController"; next(error); } }; From 484231144d090ad08cddf3b4e5103c7ebc26b6e6 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Sep 2024 10:56:07 -0700 Subject: [PATCH 09/30] Add superadmin check --- Server/controllers/authController.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index 69483f5e9..fe406a6b6 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -463,12 +463,12 @@ const deleteUserController = async (req, res, next) => { throw new Error(errorMessages.DB_USER_NOT_FOUND); } - // 1. Find all the monitors associated with the user id + // 1. Find all the monitors associated with the team ID if superadmin const result = await req.db.getMonitorsByTeamId({ params: { teamId: user.teamId }, }); - if (result?.monitors.length > 0) { + if (user.role.includes("superadmin") && result?.monitors.length > 0) { //2. Remove all jobs, delete checks and alerts await Promise.all( monitors.map(async (monitor) => { From 2c204e6e1c59a598a8c383c01145e424c6d342b5 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Sep 2024 11:10:08 -0700 Subject: [PATCH 10/30] Remove features from demo user --- Client/src/Components/Sidebar/index.jsx | 7 ++++ .../TabPanels/Account/ProfilePanel.jsx | 32 ++++++++++--------- Client/src/Pages/Account/index.jsx | 9 ++++-- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/Client/src/Components/Sidebar/index.jsx b/Client/src/Components/Sidebar/index.jsx index c372f8c66..08943807a 100644 --- a/Client/src/Components/Sidebar/index.jsx +++ b/Client/src/Components/Sidebar/index.jsx @@ -90,6 +90,13 @@ function Sidebar() { const [open, setOpen] = useState({ Dashboard: false, Account: false }); const [anchorEl, setAnchorEl] = useState(null); const [popup, setPopup] = useState(); + const { user } = useSelector((state) => state.auth); + + if (!user.role.includes("superadmin")) { + menu[3].nested = menu[3].nested.filter((item) => { + return item.name !== "Password"; + }); + } const openPopup = (event, id) => { setAnchorEl(event.currentTarget); diff --git a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx index 885c795b1..69c221142 100644 --- a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx +++ b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx @@ -331,22 +331,24 @@ const ProfilePanel = () => { borderColor: theme.palette.border.light, }} /> - - - Delete account - - Note that deleting your account will remove all data from our - system. This is permanent and non-recoverable. - + {user.role.includes("superadmin") && ( + + + Delete account + + Note that deleting your account will remove all data from our + system. This is permanent and non-recoverable. + + + - - + )} {/* TODO - Update ModalPopup Component with @mui for reusability */} { const handleTabChange = (event, newTab) => { navigate(`/account/${newTab}`); }; - - let tabList = ["Profile", "Password", "Team"]; const { user } = useSelector((state) => state.auth); + + let tabList = ["Profile", "Team"]; + if (user.role.includes("superadmin")) { + tabList.push("Password"); + } const requiredRoles = ["superadmin", "admin"]; const hideTeams = !requiredRoles.some((role) => user.role.includes(role)); if (hideTeams) { @@ -74,7 +77,7 @@ const Account = ({ open = "profile" }) => { - + {user.role.includes("superadmin") && } {!hideTeams && } From 7c81259e191394b27d154833f914d77b58add61e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Sep 2024 11:25:06 -0700 Subject: [PATCH 11/30] Prefill username and password --- Client/src/Pages/Auth/Login.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Client/src/Pages/Auth/Login.jsx b/Client/src/Pages/Auth/Login.jsx index c8946b7fb..588708261 100644 --- a/Client/src/Pages/Auth/Login.jsx +++ b/Client/src/Pages/Auth/Login.jsx @@ -280,7 +280,7 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => { position: "absolute", top: "103%", left: "50%", - transform: "translateX(-50%)" + transform: "translateX(-50%)", }} > { }; const [form, setForm] = useState({ - email: "", - password: "", + email: "uptimedemo@demo.com", + password: "Demouser1!", }); const [errors, setErrors] = useState({}); const [step, setStep] = useState(0); From 63da8bd5718bf02d04783aaa189f95a59ae64c57 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Sep 2024 11:42:05 -0700 Subject: [PATCH 12/30] Add demo role --- Server/models/user.js | 2 +- Server/validation/joi.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Server/models/user.js b/Server/models/user.js index 829129b4d..888bfd7eb 100644 --- a/Server/models/user.js +++ b/Server/models/user.js @@ -38,7 +38,7 @@ const UserSchema = mongoose.Schema( role: { type: [String], default: "user", - enum: ["user", "admin", "superadmin"], + enum: ["user", "admin", "superadmin", "demo"], }, teamId: { type: mongoose.Schema.Types.ObjectId, diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 417965b95..918400c21 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -70,7 +70,7 @@ const registrationBodyValidation = joi.object({ profileImage: joi.any(), role: joi .array() - .items(joi.string().valid("superadmin", "admin", "user")) + .items(joi.string().valid("superadmin", "admin", "user", "demo")) .min(1) .required(), teamId: joi.string().allow("").required(), From 7f9950b9e56d4b8560ea383bec1363a91ab16b4c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Sep 2024 11:51:55 -0700 Subject: [PATCH 13/30] use demo role to show/hide features --- Client/src/Components/Sidebar/index.jsx | 3 ++- .../src/Components/TabPanels/Account/ProfilePanel.jsx | 2 +- Client/src/Pages/Account/index.jsx | 8 +++++--- Client/src/Pages/Auth/Login.jsx | 5 +++-- README.md | 11 +++++------ 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/Client/src/Components/Sidebar/index.jsx b/Client/src/Components/Sidebar/index.jsx index 08943807a..27fb35329 100644 --- a/Client/src/Components/Sidebar/index.jsx +++ b/Client/src/Components/Sidebar/index.jsx @@ -92,7 +92,8 @@ function Sidebar() { const [popup, setPopup] = useState(); const { user } = useSelector((state) => state.auth); - if (!user.role.includes("superadmin")) { + // Remove demo password if demo + if (user.role.includes("demo")) { menu[3].nested = menu[3].nested.filter((item) => { return item.name !== "Password"; }); diff --git a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx index 69c221142..476f47293 100644 --- a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx +++ b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx @@ -331,7 +331,7 @@ const ProfilePanel = () => { borderColor: theme.palette.border.light, }} /> - {user.role.includes("superadmin") && ( + {!user.role.includes("demo") && ( Delete account diff --git a/Client/src/Pages/Account/index.jsx b/Client/src/Pages/Account/index.jsx index 85150bbd1..eda4d96d8 100644 --- a/Client/src/Pages/Account/index.jsx +++ b/Client/src/Pages/Account/index.jsx @@ -24,9 +24,11 @@ const Account = ({ open = "profile" }) => { }; const { user } = useSelector((state) => state.auth); - let tabList = ["Profile", "Team"]; - if (user.role.includes("superadmin")) { - tabList.push("Password"); + let tabList = ["Profile", "Team", "Password"]; + + // Remove passwrod for demo + if (user.role.includes("demo")) { + tabList = tabList.filter((tab) => tab !== "Password"); } const requiredRoles = ["superadmin", "admin"]; const hideTeams = !requiredRoles.some((role) => user.role.includes(role)); diff --git a/Client/src/Pages/Auth/Login.jsx b/Client/src/Pages/Auth/Login.jsx index 588708261..d3384c0de 100644 --- a/Client/src/Pages/Auth/Login.jsx +++ b/Client/src/Pages/Auth/Login.jsx @@ -15,6 +15,7 @@ import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; import PropTypes from "prop-types"; import { logger } from "../../Utils/Logger"; import "./index.css"; +const DEMO = import.meta.env.VITE_APP_DEMO; /** * Displays the initial landing page. @@ -327,8 +328,8 @@ const Login = () => { }; const [form, setForm] = useState({ - email: "uptimedemo@demo.com", - password: "Demouser1!", + email: DEMO !== undefined ? "uptimedemo@demo.com" : "", + password: DEMO !== undefined ? "Demouser1!" : "", }); const [errors, setErrors] = useState({}); const [step, setStep] = useState(0); diff --git a/README.md b/README.md index e050b221e..d396ae397 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,9 @@

An open source server monitoring application

- ![Dashboard-dark](https://github.com/user-attachments/assets/db875138-164f-453c-a75e-889f88747578) (yes, we have a light theme as well, but this looks better on readme.md) - 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. ## Features @@ -214,10 +212,11 @@ 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 | -| VITE_APP_LOG_LEVEL | Optional | `string` | Log level | `"none"`\|`"error"` \| `"warn"` \| | +| 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"` \| | +| VITE_APP_DEMO | Optional | `boolean` | Demo server or not | `true`\|`false` \| |
From 6fc06ab9b60a3fe3e3d1c31a98754bc2f1133a41 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Sep 2024 11:53:58 -0700 Subject: [PATCH 14/30] Add demo user to role map --- Client/src/Components/TabPanels/Account/TeamPanel.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/Client/src/Components/TabPanels/Account/TeamPanel.jsx b/Client/src/Components/TabPanels/Account/TeamPanel.jsx index 9809ac710..6a15cfeb9 100644 --- a/Client/src/Components/TabPanels/Account/TeamPanel.jsx +++ b/Client/src/Components/TabPanels/Account/TeamPanel.jsx @@ -24,6 +24,7 @@ const TeamPanel = () => { superadmin: "Super admin", admin: "Admin", user: "Team member", + demo: "Demo User", }; const theme = useTheme(); From 00074e039e70b7b8f1d7262f6a1dcd5ac1d3026c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 6 Sep 2024 12:01:58 -0700 Subject: [PATCH 15/30] Simplify tab check --- Client/src/Pages/Account/index.jsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Client/src/Pages/Account/index.jsx b/Client/src/Pages/Account/index.jsx index eda4d96d8..391d3adc9 100644 --- a/Client/src/Pages/Account/index.jsx +++ b/Client/src/Pages/Account/index.jsx @@ -24,18 +24,18 @@ const Account = ({ open = "profile" }) => { }; const { user } = useSelector((state) => state.auth); - let tabList = ["Profile", "Team", "Password"]; - - // Remove passwrod for demo - if (user.role.includes("demo")) { - tabList = tabList.filter((tab) => tab !== "Password"); - } const requiredRoles = ["superadmin", "admin"]; + let tabList = ["Profile", "Team", "Password"]; const hideTeams = !requiredRoles.some((role) => user.role.includes(role)); if (hideTeams) { tabList = ["Profile", "Password"]; } + // Remove passwrod for demo + if (user.role.includes("demo")) { + tabList = ["Profile"]; + } + return ( Date: Fri, 6 Sep 2024 16:32:38 -0400 Subject: [PATCH 16/30] Fixed background color not changing on theme swap --- Client/src/App.jsx | 2 ++ .../Charts/MonitorDetailsAreaChart/index.jsx | 1 + Client/src/Layouts/HomeLayout/index.css | 4 --- Client/src/Layouts/HomeLayout/index.jsx | 19 ++++---------- Client/src/Pages/Auth/ForgotPassword.jsx | 18 ++++++++++--- Client/src/Pages/Auth/styled.jsx | 26 +++++++++++++++++++ Client/src/Utils/Theme/darkTheme.js | 13 ++++++++-- Client/src/Utils/Theme/lightTheme.js | 12 +++++++-- Client/src/assets/icons/key.svg | 19 ++------------ 9 files changed, 71 insertions(+), 43 deletions(-) create mode 100644 Client/src/Pages/Auth/styled.jsx diff --git a/Client/src/App.jsx b/Client/src/App.jsx index 783f28330..5db3ea623 100644 --- a/Client/src/App.jsx +++ b/Client/src/App.jsx @@ -32,6 +32,7 @@ import { ThemeProvider } from "@emotion/react"; import lightTheme from "./Utils/Theme/lightTheme"; import darkTheme from "./Utils/Theme/darkTheme"; import { useSelector } from "react-redux"; +import { CssBaseline } from "@mui/material"; function App() { const AdminCheckedRegister = withAdminCheck(Register); @@ -45,6 +46,7 @@ function App() { return ( + }> { dataKey="responseTime" stroke={theme.palette.primary.main} fill="url(#colorUv)" + strokeWidth={1.5} /> diff --git a/Client/src/Layouts/HomeLayout/index.css b/Client/src/Layouts/HomeLayout/index.css index 855bf33fc..d9fe9e06b 100644 --- a/Client/src/Layouts/HomeLayout/index.css +++ b/Client/src/Layouts/HomeLayout/index.css @@ -1,7 +1,3 @@ -#root:has(.home-layout) { - background-color: var(--secondary-bg); -} - .home-layout { position: relative; min-height: 100vh; diff --git a/Client/src/Layouts/HomeLayout/index.jsx b/Client/src/Layouts/HomeLayout/index.jsx index fd92c59fa..62af75075 100644 --- a/Client/src/Layouts/HomeLayout/index.jsx +++ b/Client/src/Layouts/HomeLayout/index.jsx @@ -1,24 +1,15 @@ import Sidebar from "../../Components/Sidebar"; import { Outlet } from "react-router"; -import { Box, Stack } from "@mui/material"; -import { useTheme } from "@emotion/react"; +import { Stack } from "@mui/material"; import "./index.css"; const HomeLayout = () => { - const theme = useTheme(); - return ( - - - - - - + + + + ); }; diff --git a/Client/src/Pages/Auth/ForgotPassword.jsx b/Client/src/Pages/Auth/ForgotPassword.jsx index 338da645c..10b6d3693 100644 --- a/Client/src/Pages/Auth/ForgotPassword.jsx +++ b/Client/src/Pages/Auth/ForgotPassword.jsx @@ -6,9 +6,11 @@ import { forgotPassword } from "../../Features/Auth/authSlice"; import { useEffect, useState } from "react"; import { credentials } from "../../Validation/validation"; import { useNavigate } from "react-router-dom"; +import { IconBox } from "./styled"; import Field from "../../Components/Inputs/Field"; import Logo from "../../assets/icons/bwu-icon.svg?react"; import Key from "../../assets/icons/key.svg?react"; +import Background from "../../assets/Images/background-grid.svg?react"; import background from "../../assets/Images/background_pattern_decorative.png"; import LoadingButton from "@mui/lab/LoadingButton"; import "./index.css"; @@ -94,7 +96,7 @@ const ForgotPassword = () => { "& h1": { color: theme.palette.primary.main, fontWeight: 600, - fontSize: 24, + fontSize: 21, }, "& p": { fontSize: 14, @@ -104,8 +106,14 @@ const ForgotPassword = () => { > + sx={{ + "& svg g g:last-of-type path": { + stroke: theme.palette.border.light, + }, + }} + > + + { textAlign="center" > - + + + Forgot password? No worries, we'll send you reset instructions. diff --git a/Client/src/Pages/Auth/styled.jsx b/Client/src/Pages/Auth/styled.jsx new file mode 100644 index 000000000..c6a23d521 --- /dev/null +++ b/Client/src/Pages/Auth/styled.jsx @@ -0,0 +1,26 @@ +import { Box, styled } from "@mui/material"; + +export const IconBox = styled(Box)(({ theme }) => ({ + height: 48, + minWidth: 48, + width: 48, + position: "relative", + border: 1, + borderStyle: "solid", + borderColor: theme.palette.border.dark, + borderRadius: 12, + backgroundColor: theme.palette.background.accent, + margin: "auto", + marginBottom: 4, + "& svg": { + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + width: 24, + height: 24, + "& path": { + stroke: theme.palette.text.tertiary, + }, + }, +})); diff --git a/Client/src/Utils/Theme/darkTheme.js b/Client/src/Utils/Theme/darkTheme.js index e54d9eaf4..d590614c3 100644 --- a/Client/src/Utils/Theme/darkTheme.js +++ b/Client/src/Utils/Theme/darkTheme.js @@ -53,8 +53,8 @@ const darkTheme = createTheme({ warning: { text: "#e88c30", main: "#FF9F00", - light: "#272115", - bg: "#624711", + light: "#624711", + bg: "#262115", border: "#e88c30", }, percentage: { @@ -74,6 +74,15 @@ const darkTheme = createTheme({ }, spacing: 2, components: { + MuiCssBaseline: { + styleOverrides: { + body: { + backgroundImage: + "radial-gradient(circle, #09090b, #0c0c0e, #0f0f11, #111113, #131315, #131315, #131315, #131315, #111113, #0f0f11, #0c0c0e, #09090b)", + lineHeight: "inherit", + }, + }, + }, MuiButton: { defaultProps: { disableRipple: true, diff --git a/Client/src/Utils/Theme/lightTheme.js b/Client/src/Utils/Theme/lightTheme.js index 60a8e0b2d..19e44f4f7 100644 --- a/Client/src/Utils/Theme/lightTheme.js +++ b/Client/src/Utils/Theme/lightTheme.js @@ -50,8 +50,8 @@ const lightTheme = createTheme({ warning: { text: "#DC6803", main: "#fdb022", - light: "#fffcf5", - bg: "#ffecbc", + light: "#ffecbc", + bg: "#fffcf5", border: "#fec84b", }, percentage: { @@ -71,6 +71,14 @@ const lightTheme = createTheme({ }, spacing: 2, components: { + MuiCssBaseline: { + styleOverrides: { + body: { + backgroundImage: + "radial-gradient(circle, #fcfcfd, #fdfcfd, #fdfdfd, #fefdfe, #fefefe, #fefefe, #fefefe, #fefefe, #fefdfe, #fdfdfd, #fdfcfd, #fcfcfd)", + }, + }, + }, MuiButton: { defaultProps: { disableRipple: true, diff --git a/Client/src/assets/icons/key.svg b/Client/src/assets/icons/key.svg index c6401e09f..5db8d5f16 100644 --- a/Client/src/assets/icons/key.svg +++ b/Client/src/assets/icons/key.svg @@ -1,18 +1,3 @@ - - - - - - - - - - - - - - - - - + + From ee55394719f924c1ecbf0c953ae84605ebed8ec5 Mon Sep 17 00:00:00 2001 From: Daniel Cojocea Date: Fri, 6 Sep 2024 17:14:23 -0400 Subject: [PATCH 17/30] Fixed auth pages dark mode styling --- Client/src/Pages/Auth/CheckEmail.jsx | 29 +++++++++++------- Client/src/Pages/Auth/ForgotPassword.jsx | 1 - Client/src/Pages/Auth/Login.jsx | 24 +++++++++------ .../src/Pages/Auth/NewPasswordConfirmed.jsx | 28 ++++++++++------- Client/src/Pages/Auth/Register/Register.jsx | 20 +++++++----- Client/src/Pages/Auth/SetNewPassword.jsx | 19 +++++++++--- Client/src/Pages/Auth/index.css | 4 +-- Client/src/Pages/Auth/styled.jsx | 2 +- Client/src/Utils/Theme/darkTheme.js | 2 +- Client/src/Utils/Theme/lightTheme.js | 1 + .../Images/background_pattern_decorative.png | Bin 30372 -> 0 bytes Client/src/assets/icons/check-outlined.svg | 2 +- Client/src/assets/icons/confirm-icon.svg | 18 ----------- Client/src/assets/icons/email.svg | 19 ++---------- Client/src/assets/icons/lock-button-icon.svg | 18 ----------- Client/src/assets/icons/lock.svg | 2 +- 16 files changed, 83 insertions(+), 106 deletions(-) delete mode 100644 Client/src/assets/Images/background_pattern_decorative.png delete mode 100644 Client/src/assets/icons/confirm-icon.svg delete mode 100644 Client/src/assets/icons/lock-button-icon.svg diff --git a/Client/src/Pages/Auth/CheckEmail.jsx b/Client/src/Pages/Auth/CheckEmail.jsx index 88bc1983c..d28a5b48f 100644 --- a/Client/src/Pages/Auth/CheckEmail.jsx +++ b/Client/src/Pages/Auth/CheckEmail.jsx @@ -5,7 +5,8 @@ import { useDispatch } from "react-redux"; import { useNavigate } from "react-router"; import { createToast } from "../../Utils/toastUtils"; import { forgotPassword } from "../../Features/Auth/authSlice"; -import background from "../../assets/Images/background_pattern_decorative.png"; +import { IconBox } from "./styled"; +import Background from "../../assets/Images/background-grid.svg?react"; import EmailIcon from "../../assets/icons/email.svg?react"; import Logo from "../../assets/icons/bwu-icon.svg?react"; import "./index.css"; @@ -93,16 +94,20 @@ const CheckEmail = () => { fontWeight: 600, fontSize: 22, }, - "& p": { - fontSize: 14, - color: theme.palette.text.accent, - }, + "& p": { color: theme.palette.text.accent, fontSize: 13.5 }, + "& span": { fontSize: "inherit" }, }} > + sx={{ + "& svg g g:last-of-type path": { + stroke: theme.palette.border.light, + }, + }} + > + + { }} > - + + + Check your email - + We sent a password reset link to{" "} {email || "username@email.com"} @@ -159,7 +166,7 @@ const CheckEmail = () => { > Open email app - + Didn't receive the email?{" "} { textAlign="center" sx={{ position: "absolute", - top: "103%", + top: "104%", left: "50%", - transform: "translateX(-50%)" + transform: "translateX(-50%)", }} > { "& h1": { color: theme.palette.primary.main, fontWeight: 600, - fontSize: 30, - }, - "& p": { - fontSize: 14, - color: theme.palette.text.accent, + fontSize: 28, }, + "& p": { fontSize: 14, color: theme.palette.text.accent }, + "& span": { fontSize: "inherit" }, }} > + sx={{ + "& svg g g:last-of-type path": { + stroke: theme.palette.border.light, + }, + }} + > + + { "& h1": { color: theme.palette.primary.main, fontWeight: 600, - fontSize: 22, - }, - "& p": { - fontSize: 14, - color: theme.palette.text.accent, + fontSize: 21, }, + "& p": { fontSize: 13.5, color: theme.palette.text.accent }, }} > + sx={{ + "& svg g g:last-of-type path": { + stroke: theme.palette.border.light, + }, + }} + > + + { textAlign="center" > - + + + Password reset - + Your password has been successfully reset. Click below to log in magically. diff --git a/Client/src/Pages/Auth/Register/Register.jsx b/Client/src/Pages/Auth/Register/Register.jsx index e454b355f..7f5ed6390 100644 --- a/Client/src/Pages/Auth/Register/Register.jsx +++ b/Client/src/Pages/Auth/Register/Register.jsx @@ -8,7 +8,7 @@ import { credentials } from "../../../Validation/validation"; import { createToast } from "../../../Utils/toastUtils"; import { register } from "../../../Features/Auth/authSlice"; import { useParams } from "react-router-dom"; -import background from "../../../assets/Images/background_pattern_decorative.png"; +import Background from "../../../assets/Images/background-grid.svg?react"; import Logo from "../../../assets/icons/bwu-icon.svg?react"; import Mail from "../../../assets/icons/mail.svg?react"; import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; @@ -658,18 +658,22 @@ const Register = ({ isSuperAdmin }) => { "& h1": { color: theme.palette.primary.main, fontWeight: 600, - fontSize: 30, - }, - "& p": { - fontSize: 14, - color: theme.palette.text.accent, + fontSize: 28, }, + "& p": { fontSize: 14, color: theme.palette.text.accent }, + "& span": { fontSize: "inherit" }, }} > + sx={{ + "& svg g g:last-of-type path": { + stroke: theme.palette.border.light, + }, + }} + > + + { const navigate = useNavigate(); @@ -121,8 +122,14 @@ const SetNewPassword = () => { > + sx={{ + "& svg g g:last-of-type path": { + stroke: theme.palette.border.light, + }, + }} + > + + { textAlign="center" > - + + + Set new password Your new password must be different to previously used passwords. diff --git a/Client/src/Pages/Auth/index.css b/Client/src/Pages/Auth/index.css index 33280e8f4..ba7d0e266 100644 --- a/Client/src/Pages/Auth/index.css +++ b/Client/src/Pages/Auth/index.css @@ -6,9 +6,7 @@ font-size: var(--env-var-font-size-xlarge); font-weight: 600; } -.auth button:not(.MuiIconButton-root), -.auth p, -.auth span { +.auth button:not(.MuiIconButton-root) { font-size: var(--env-var-font-size-medium-plus); } diff --git a/Client/src/Pages/Auth/styled.jsx b/Client/src/Pages/Auth/styled.jsx index c6a23d521..a6e4a3343 100644 --- a/Client/src/Pages/Auth/styled.jsx +++ b/Client/src/Pages/Auth/styled.jsx @@ -11,7 +11,7 @@ export const IconBox = styled(Box)(({ theme }) => ({ borderRadius: 12, backgroundColor: theme.palette.background.accent, margin: "auto", - marginBottom: 4, + marginBottom: 8, "& svg": { position: "absolute", top: "50%", diff --git a/Client/src/Utils/Theme/darkTheme.js b/Client/src/Utils/Theme/darkTheme.js index d590614c3..a49a85a86 100644 --- a/Client/src/Utils/Theme/darkTheme.js +++ b/Client/src/Utils/Theme/darkTheme.js @@ -136,7 +136,7 @@ const darkTheme = createTheme({ padding: 4, transition: "none", "&:hover": { - backgroundColor: background.fill, + backgroundColor: border.light, }, }, }, diff --git a/Client/src/Utils/Theme/lightTheme.js b/Client/src/Utils/Theme/lightTheme.js index 19e44f4f7..4fc565da3 100644 --- a/Client/src/Utils/Theme/lightTheme.js +++ b/Client/src/Utils/Theme/lightTheme.js @@ -76,6 +76,7 @@ const lightTheme = createTheme({ body: { backgroundImage: "radial-gradient(circle, #fcfcfd, #fdfcfd, #fdfdfd, #fefdfe, #fefefe, #fefefe, #fefefe, #fefefe, #fefdfe, #fdfdfd, #fdfcfd, #fcfcfd)", + lineHeight: "inherit", }, }, }, diff --git a/Client/src/assets/Images/background_pattern_decorative.png b/Client/src/assets/Images/background_pattern_decorative.png deleted file mode 100644 index 34f6ebce6864c7e4ade2e4befacf404f0e4a9cba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30372 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 diff --git a/Client/src/assets/icons/check-outlined.svg b/Client/src/assets/icons/check-outlined.svg index d26657863..1e779d547 100644 --- a/Client/src/assets/icons/check-outlined.svg +++ b/Client/src/assets/icons/check-outlined.svg @@ -1,3 +1,3 @@ - + diff --git a/Client/src/assets/icons/confirm-icon.svg b/Client/src/assets/icons/confirm-icon.svg deleted file mode 100644 index d5cdd42de..000000000 --- a/Client/src/assets/icons/confirm-icon.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/Client/src/assets/icons/email.svg b/Client/src/assets/icons/email.svg index f501bd75c..01f55e961 100644 --- a/Client/src/assets/icons/email.svg +++ b/Client/src/assets/icons/email.svg @@ -1,18 +1,3 @@ - - - - - - - - - - - - - - - - - + + diff --git a/Client/src/assets/icons/lock-button-icon.svg b/Client/src/assets/icons/lock-button-icon.svg deleted file mode 100644 index 5048a6a80..000000000 --- a/Client/src/assets/icons/lock-button-icon.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/Client/src/assets/icons/lock.svg b/Client/src/assets/icons/lock.svg index eb625f0ee..a87535d54 100644 --- a/Client/src/assets/icons/lock.svg +++ b/Client/src/assets/icons/lock.svg @@ -1,3 +1,3 @@ - + From ce66bd6d3c3dbca3e17f4ecd69d217c515a0d1b5 Mon Sep 17 00:00:00 2001 From: Daniel Cojocea Date: Fri, 6 Sep 2024 18:13:38 -0400 Subject: [PATCH 18/30] Fixed panels darkmode styles --- Client/src/Components/Avatar/index.jsx | 8 ++-- Client/src/Components/ProgressBars/index.jsx | 30 ++++++++---- .../TabPanels/Account/ProfilePanel.jsx | 4 +- .../TabPanels/Account/TeamPanel.jsx | 47 ++++++++++++------- Client/src/Utils/Theme/darkTheme.js | 2 +- Client/src/index.css | 6 --- 6 files changed, 59 insertions(+), 38 deletions(-) diff --git a/Client/src/Components/Avatar/index.jsx b/Client/src/Components/Avatar/index.jsx index 975f27a32..e1263d3ad 100644 --- a/Client/src/Components/Avatar/index.jsx +++ b/Client/src/Components/Avatar/index.jsx @@ -64,10 +64,10 @@ const Avatar = ({ src, small, sx }) => { "&::before": { content: `""`, position: "absolute", - top: "0", - left: "0", - width: `calc(100% - ${border * 2}px)`, - height: `calc(100% - ${border * 2}px)`, + top: 0, + left: 0, + width: `100%`, + height: `100%`, border: `${border}px solid rgba(255,255,255,0.2)`, borderRadius: "50%", }, diff --git a/Client/src/Components/ProgressBars/index.jsx b/Client/src/Components/ProgressBars/index.jsx index 51d70bb2c..d1dcc6cfe 100644 --- a/Client/src/Components/ProgressBars/index.jsx +++ b/Client/src/Components/ProgressBars/index.jsx @@ -44,7 +44,7 @@ const ProgressUpload = ({ borderRadius: theme.shape.borderRadius, border: 1, borderColor: theme.palette.border.light, - backgroundColor: "#fafafa", + backgroundColor: theme.palette.background.fill, "&:has(.input-error)": { borderColor: theme.palette.error.main, backgroundColor: theme.palette.error.bg, @@ -62,23 +62,35 @@ const ProgressUpload = ({ direction="row" mb={error ? 0 : theme.spacing(5)} gap={theme.spacing(5)} - alignItems={error ? "center" : "flex-end"} + alignItems={error ? "center" : "flex-start"} > {error ? ( ) : icon ? ( - {icon} - + ) : ( "" )} diff --git a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx index 885c795b1..fb782da92 100644 --- a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx +++ b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx @@ -414,12 +414,12 @@ const ProfilePanel = () => { top: "50%", left: "50%", transform: "translate(-50%, -50%)", - width: 450, + width: 475, bgcolor: theme.palette.background.main, border: 1, borderColor: theme.palette.border.light, borderRadius: theme.shape.borderRadius, - boxShadow: 24, + boxShadow: theme.shape.boxShadow, p: theme.spacing(15), "&:focus": { outline: "none", diff --git a/Client/src/Components/TabPanels/Account/TeamPanel.jsx b/Client/src/Components/TabPanels/Account/TeamPanel.jsx index 9809ac710..540f2b5fa 100644 --- a/Client/src/Components/TabPanels/Account/TeamPanel.jsx +++ b/Client/src/Components/TabPanels/Account/TeamPanel.jsx @@ -1,6 +1,13 @@ import { useTheme } from "@emotion/react"; import TabPanel from "@mui/lab/TabPanel"; -import { Button, ButtonGroup, Modal, Stack, Typography } from "@mui/material"; +import { + Box, + Button, + ButtonGroup, + Modal, + Stack, + Typography, +} from "@mui/material"; import { useEffect, useState } from "react"; import Field from "../../Inputs/Field"; import { credentials } from "../../../Validation/validation"; @@ -88,7 +95,7 @@ const TeamPanel = () => { id: idx, data: ( - + {member.firstName + " " + member.lastName} @@ -322,7 +329,6 @@ const TeamPanel = () => { aria-describedby="invite-member-to-team" open={isOpen} onClose={closeInviteModal} - disablePortal > { top: "50%", left: "50%", transform: "translate(-50%, -50%)", - width: 400, + width: 450, bgcolor: theme.palette.background.main, border: 1, borderColor: theme.palette.border.light, borderRadius: theme.shape.borderRadius, - boxShadow: 24, + boxShadow: theme.shape.boxShadow, p: theme.spacing(15), "&:focus": { outline: "none", }, }} > - - Invite new team member - - - When you add a new team member, they will get access to all - monitors. - + + + Invite new team member + + + When you add a new team member, they will get access to all + monitors. + + Date: Fri, 6 Sep 2024 18:34:41 -0400 Subject: [PATCH 19/30] Fixed area chart not updating on filter change --- Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx index ead38c6ac..121200acf 100644 --- a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx +++ b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx @@ -97,7 +97,7 @@ const MonitorDetailsAreaChart = ({ checks }) => { }); }; - const memoizedChecks = useMemo(() => checks, []); + const memoizedChecks = useMemo(() => checks, [checks[0]]); const theme = useTheme(); From 8d3823ad052abc03de8b90890da48be0b1e72b63 Mon Sep 17 00:00:00 2001 From: Alexander Holliday Date: Fri, 6 Sep 2024 22:06:48 -0700 Subject: [PATCH 20/30] Update index.jsx fix typo --- Client/src/Pages/Account/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Client/src/Pages/Account/index.jsx b/Client/src/Pages/Account/index.jsx index 391d3adc9..4f5b0c440 100644 --- a/Client/src/Pages/Account/index.jsx +++ b/Client/src/Pages/Account/index.jsx @@ -31,7 +31,7 @@ const Account = ({ open = "profile" }) => { tabList = ["Profile", "Password"]; } - // Remove passwrod for demo + // Remove password for demo if (user.role.includes("demo")) { tabList = ["Profile"]; } From 723b1750e99fffbe57024d0aace2afc6e4869036 Mon Sep 17 00:00:00 2001 From: Daniel Cojocea Date: Sat, 7 Sep 2024 13:08:38 -0400 Subject: [PATCH 21/30] Fixed minor bugs and updated area tooltip styling --- .../Components/Charts/MonitorDetailsAreaChart/index.jsx | 8 ++++++-- Client/src/Components/Sidebar/index.jsx | 2 +- Client/src/Pages/Monitors/Home/index.jsx | 3 +-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx index 121200acf..fd28b6e7e 100644 --- a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx +++ b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx @@ -9,8 +9,8 @@ import { } from "recharts"; import { Box, Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; +import { useMemo } from "react"; import "./index.css"; -import React, { useMemo } from "react"; const CustomToolTip = ({ active, payload, label }) => { const theme = useTheme(); @@ -143,13 +143,17 @@ const MonitorDetailsAreaChart = ({ checks }) => { tickLine={false} height={18} /> - } /> + } + /> diff --git a/Client/src/Components/Sidebar/index.jsx b/Client/src/Components/Sidebar/index.jsx index 27fb35329..c95157449 100644 --- a/Client/src/Components/Sidebar/index.jsx +++ b/Client/src/Components/Sidebar/index.jsx @@ -93,7 +93,7 @@ function Sidebar() { const { user } = useSelector((state) => state.auth); // Remove demo password if demo - if (user.role.includes("demo")) { + if (user.role?.includes("demo")) { menu[3].nested = menu[3].nested.filter((item) => { return item.name !== "Password"; }); diff --git a/Client/src/Pages/Monitors/Home/index.jsx b/Client/src/Pages/Monitors/Home/index.jsx index 9bc4496db..accc758e9 100644 --- a/Client/src/Pages/Monitors/Home/index.jsx +++ b/Client/src/Pages/Monitors/Home/index.jsx @@ -110,8 +110,7 @@ const Monitors = ({ isAdmin }) => { borderColor={theme.palette.border.light} backgroundColor={theme.palette.background.accent} > - {monitorState?.monitorsSummary?.monitors?.monitors - ?.length || 0} + {monitorState?.monitorsSummary?.monitorCounts?.total || 0} {/* TODO - add search bar */} From 3e5912cf0955ddce0c31f802d4c8dddae7e38663 Mon Sep 17 00:00:00 2001 From: Daniel Cojocea Date: Sat, 7 Sep 2024 14:20:09 -0400 Subject: [PATCH 22/30] Added svg icons --- Client/src/assets/icons/pause-icon.svg | 3 +++ Client/src/assets/icons/resume-icon.svg | 4 ++++ 2 files changed, 7 insertions(+) create mode 100644 Client/src/assets/icons/pause-icon.svg create mode 100644 Client/src/assets/icons/resume-icon.svg diff --git a/Client/src/assets/icons/pause-icon.svg b/Client/src/assets/icons/pause-icon.svg new file mode 100644 index 000000000..c73abfc1d --- /dev/null +++ b/Client/src/assets/icons/pause-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/Client/src/assets/icons/resume-icon.svg b/Client/src/assets/icons/resume-icon.svg new file mode 100644 index 000000000..1da787081 --- /dev/null +++ b/Client/src/assets/icons/resume-icon.svg @@ -0,0 +1,4 @@ + + + + From b0432c522cac657275974ddc66665051d56ab472 Mon Sep 17 00:00:00 2001 From: Daniel Cojocea Date: Sat, 7 Sep 2024 14:22:58 -0400 Subject: [PATCH 23/30] Fixed header and other small styling changes --- Client/src/Components/Sidebar/index.jsx | 3 + Client/src/Pages/Monitors/Configure/index.css | 7 -- Client/src/Pages/Monitors/Configure/index.jsx | 104 +++++++++++++----- Client/src/Pages/Monitors/Details/index.jsx | 10 +- Client/src/Pages/Monitors/styled.jsx | 3 + 5 files changed, 90 insertions(+), 37 deletions(-) diff --git a/Client/src/Components/Sidebar/index.jsx b/Client/src/Components/Sidebar/index.jsx index 27fb35329..16911abeb 100644 --- a/Client/src/Components/Sidebar/index.jsx +++ b/Client/src/Components/Sidebar/index.jsx @@ -652,6 +652,9 @@ function Sidebar() { gap: theme.spacing(4), borderRadius: theme.shape.borderRadius, pl: theme.spacing(4), + "& svg path": { + stroke: theme.palette.other.icon, + }, }} > diff --git a/Client/src/Pages/Monitors/Configure/index.css b/Client/src/Pages/Monitors/Configure/index.css index 23b6af57a..032833924 100644 --- a/Client/src/Pages/Monitors/Configure/index.css +++ b/Client/src/Pages/Monitors/Configure/index.css @@ -1,10 +1,3 @@ -.configure-monitor h1.MuiTypography-root { - font-size: var(--env-var-font-size-large-plus); - font-weight: 600; -} -.configure-monitor p.MuiTypography-root { - font-size: var(--env-var-font-size-medium); -} .configure-monitor h1.MuiTypography-root + span.MuiTypography-root { font-size: var(--env-var-font-size-small-plus); } diff --git a/Client/src/Pages/Monitors/Configure/index.jsx b/Client/src/Pages/Monitors/Configure/index.jsx index 1ee171034..34076dd89 100644 --- a/Client/src/Pages/Monitors/Configure/index.jsx +++ b/Client/src/Pages/Monitors/Configure/index.jsx @@ -2,7 +2,7 @@ import { useNavigate, useParams } from "react-router"; import { useTheme } from "@emotion/react"; import { useDispatch, useSelector } from "react-redux"; import { useEffect, useState } from "react"; -import { Box, Button, Modal, Stack, Typography } from "@mui/material"; +import { Box, Button, Modal, Stack, Tooltip, Typography } from "@mui/material"; import { monitorValidation } from "../../../Validation/validation"; import { createToast } from "../../../Utils/toastUtils"; import { logger } from "../../../Utils/Logger"; @@ -15,8 +15,8 @@ import { deleteUptimeMonitor, } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; import Field from "../../../Components/Inputs/Field"; -import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline"; -import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded"; +import PauseIcon from "../../../assets/icons/pause-icon.svg?react"; +import ResumeIcon from "../../../assets/icons/resume-icon.svg?react"; import Select from "../../../Components/Inputs/Select"; import Checkbox from "../../../Components/Inputs/Checkbox"; import Breadcrumbs from "../../../Components/Breadcrumbs"; @@ -202,7 +202,7 @@ const Configure = () => { }; return ( - + {Object.keys(monitor).length === 0 ? ( ) : ( @@ -222,27 +222,74 @@ const Configure = () => { flex={1} > - - {parsedUrl?.host ? ( - - {parsedUrl.host || "..."} - - ) : ( - "" - )} - {statusMsg[monitor?.status ?? undefined]} + {monitor.name} + + + + + + + + {monitor.url?.replace(/^https?:\/\//, "") || "..."} + + + Editting... + + { color="secondary" loading={isLoading} sx={{ - border: "none", - backgroundColor: theme.palette.background.main, - px: theme.spacing(5), + pl: theme.spacing(4), + pr: theme.spacing(6), mr: theme.spacing(6), "& svg": { mr: theme.spacing(2), + width: 22, + height: 22, + "& path": { + stroke: theme.palette.text.tertiary, + strokeWidth: 1.7, + }, }, }} onClick={handlePause} > {monitor?.isActive ? ( <> - + Pause ) : ( <> - + Resume )} diff --git a/Client/src/Pages/Monitors/Details/index.jsx b/Client/src/Pages/Monitors/Details/index.jsx index e31de38d7..ea356a610 100644 --- a/Client/src/Pages/Monitors/Details/index.jsx +++ b/Client/src/Pages/Monitors/Details/index.jsx @@ -15,7 +15,6 @@ import { networkService } from "../../../main"; import { logger } from "../../../Utils/Logger"; import { formatDate, - formatDuration, formatDurationRounded, formatDurationSplit, } from "../../../Utils/timeUtils"; @@ -158,7 +157,8 @@ const DetailsPage = ({ isAdmin }) => { { {monitor.url?.replace(/^https?:\/\//, "") || "..."} { backgroundColor: theme.palette.text.tertiary, opacity: 0.8, left: -9, - top: "42%", + top: "50%", + transform: "translateY(-50%)", }, }} > diff --git a/Client/src/Pages/Monitors/styled.jsx b/Client/src/Pages/Monitors/styled.jsx index afef6508e..bb35e1705 100644 --- a/Client/src/Pages/Monitors/styled.jsx +++ b/Client/src/Pages/Monitors/styled.jsx @@ -32,4 +32,7 @@ export const ConfigBox = styled(Stack)(({ theme }) => ({ "& h3, & p": { color: theme.palette.text.tertiary, }, + "& p": { + fontSize: 13, + }, })); From 409bb3262f1b0452970659b7fed3b13fc342efd5 Mon Sep 17 00:00:00 2001 From: Daniel Cojocea Date: Sat, 7 Sep 2024 14:54:19 -0400 Subject: [PATCH 24/30] Prevent scrollbar from shifting content --- Client/src/Components/Sidebar/index.jsx | 2 ++ Client/src/Pages/Account/index.jsx | 2 +- Client/src/Utils/Theme/darkTheme.js | 1 + Client/src/Utils/Theme/lightTheme.js | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Client/src/Components/Sidebar/index.jsx b/Client/src/Components/Sidebar/index.jsx index 27fb35329..2ffe5d968 100644 --- a/Client/src/Components/Sidebar/index.jsx +++ b/Client/src/Components/Sidebar/index.jsx @@ -306,6 +306,7 @@ function Sidebar() { anchorEl={anchorEl} open={Boolean(anchorEl) && popup === item.name} onClose={closePopup} + disableScrollLock anchorOrigin={{ vertical: "top", horizontal: "right", @@ -591,6 +592,7 @@ function Sidebar() { anchorEl={anchorEl} open={Boolean(anchorEl) && popup === "logout"} onClose={closePopup} + disableScrollLock anchorOrigin={{ vertical: "top", horizontal: "right", diff --git a/Client/src/Pages/Account/index.jsx b/Client/src/Pages/Account/index.jsx index 4f5b0c440..5543b38ee 100644 --- a/Client/src/Pages/Account/index.jsx +++ b/Client/src/Pages/Account/index.jsx @@ -25,7 +25,7 @@ const Account = ({ open = "profile" }) => { const { user } = useSelector((state) => state.auth); const requiredRoles = ["superadmin", "admin"]; - let tabList = ["Profile", "Team", "Password"]; + let tabList = ["Profile", "Password", "Team"]; const hideTeams = !requiredRoles.some((role) => user.role.includes(role)); if (hideTeams) { tabList = ["Profile", "Password"]; diff --git a/Client/src/Utils/Theme/darkTheme.js b/Client/src/Utils/Theme/darkTheme.js index b86dedeae..261f8135f 100644 --- a/Client/src/Utils/Theme/darkTheme.js +++ b/Client/src/Utils/Theme/darkTheme.js @@ -80,6 +80,7 @@ const darkTheme = createTheme({ backgroundImage: "radial-gradient(circle, #09090b, #0c0c0e, #0f0f11, #111113, #131315, #131315, #131315, #131315, #111113, #0f0f11, #0c0c0e, #09090b)", lineHeight: "inherit", + paddingLeft: "calc(100vw - 100%)", }, }, }, diff --git a/Client/src/Utils/Theme/lightTheme.js b/Client/src/Utils/Theme/lightTheme.js index 4fc565da3..f00ece091 100644 --- a/Client/src/Utils/Theme/lightTheme.js +++ b/Client/src/Utils/Theme/lightTheme.js @@ -77,6 +77,7 @@ const lightTheme = createTheme({ backgroundImage: "radial-gradient(circle, #fcfcfd, #fdfcfd, #fdfdfd, #fefdfe, #fefefe, #fefefe, #fefefe, #fefefe, #fefdfe, #fdfdfd, #fdfcfd, #fcfcfd)", lineHeight: "inherit", + paddingLeft: "calc(100vw - 100%)", }, }, }, From 8d8117464ba3e63d2aee497008df2c10b046c85c Mon Sep 17 00:00:00 2001 From: Daniel Cojocea Date: Sun, 8 Sep 2024 14:01:44 -0400 Subject: [PATCH 25/30] fix: prevent content shift on select menu open --- Client/src/Components/Inputs/Select/index.jsx | 1 + Client/src/Pages/Monitors/Home/MonitorTable/index.jsx | 1 + 2 files changed, 2 insertions(+) diff --git a/Client/src/Components/Inputs/Select/index.jsx b/Client/src/Components/Inputs/Select/index.jsx index e30b5d951..3d34fcca5 100644 --- a/Client/src/Components/Inputs/Select/index.jsx +++ b/Client/src/Components/Inputs/Select/index.jsx @@ -79,6 +79,7 @@ const Select = ({ displayEmpty inputProps={{ id: id }} IconComponent={KeyboardArrowDownIcon} + MenuProps={{ disableScrollLock: true }} sx={{ fontSize: 13, minWidth: "125px", diff --git a/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx b/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx index 4a0325246..f3c02f920 100644 --- a/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx +++ b/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx @@ -302,6 +302,7 @@ const MonitorTable = ({ isAdmin }) => { select: { MenuProps: { keepMounted: true, + disableScrollLock: true, PaperProps: { className: "pagination-dropdown", sx: { From 698dac3413e20aa573908c90243683b7872bfa70 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 8 Sep 2024 11:21:35 -0700 Subject: [PATCH 26/30] Add paused for state, refactored out logic for determining monitor state --- Client/src/Components/Label/index.jsx | 13 ++- Client/src/Pages/Monitors/Details/index.jsx | 61 +++------- .../Monitors/Home/MonitorTable/index.jsx | 12 +- .../src/Pages/Monitors/Home/monitorData.jsx | 110 ------------------ Client/src/Pages/Monitors/utils.jsx | 54 +++++++++ 5 files changed, 86 insertions(+), 164 deletions(-) delete mode 100644 Client/src/Pages/Monitors/Home/monitorData.jsx create mode 100644 Client/src/Pages/Monitors/utils.jsx diff --git a/Client/src/Components/Label/index.jsx b/Client/src/Components/Label/index.jsx index 886f66fb9..bdc34982a 100644 --- a/Client/src/Components/Label/index.jsx +++ b/Client/src/Components/Label/index.jsx @@ -138,6 +138,11 @@ const StatusLabel = ({ status, text, customStyles }) => { bgColor: theme.palette.error.bg, borderColor: theme.palette.error.light, }, + paused: { + dotColor: theme.palette.warning.main, + bgColor: theme.palette.warning.bg, + borderColor: theme.palette.warning.light, + }, pending: { dotColor: theme.palette.warning.main, bgColor: theme.palette.warning.bg, @@ -175,7 +180,13 @@ const StatusLabel = ({ status, text, customStyles }) => { }; StatusLabel.propTypes = { - status: PropTypes.oneOf(["up", "down", "pending", "cannot resolve"]), + status: PropTypes.oneOf([ + "up", + "down", + "paused", + "pending", + "cannot resolve", + ]), text: PropTypes.string, customStyles: PropTypes.object, }; diff --git a/Client/src/Pages/Monitors/Details/index.jsx b/Client/src/Pages/Monitors/Details/index.jsx index ea356a610..41e529eb9 100644 --- a/Client/src/Pages/Monitors/Details/index.jsx +++ b/Client/src/Pages/Monitors/Details/index.jsx @@ -34,13 +34,14 @@ import { StatBox, ChartBox, IconBox } from "./styled"; import { DownBarChart, ResponseGaugeChart, UpBarChart } from "./Charts"; import SkeletonLayout from "./skeleton"; import "./index.css"; - +import useUtils from "../utils"; /** * Details page component displaying monitor details and related information. * @component */ const DetailsPage = ({ isAdmin }) => { const theme = useTheme(); + const { statusColor, statusStyles, statusMsg, determineState } = useUtils(); const [monitor, setMonitor] = useState({}); const { monitorId } = useParams(); const { authToken } = useSelector((state) => state.auth); @@ -89,15 +90,17 @@ const DetailsPage = ({ isAdmin }) => { monitorId ); - let [month, day, year] = res?.data?.data?.certificateDate.split("/"); - const date = new Date(year, month - 1, day); + if (res?.data?.data?.certificateDate) { + let [month, day, year] = res.data.data.certificateDate.split("/"); + const date = new Date(year, month - 1, day); - setCertificateExpiry( - formatDate(date, { - hour: undefined, - minute: undefined, - }) ?? "N/A" - ); + setCertificateExpiry( + formatDate(date, { + hour: undefined, + minute: undefined, + }) ?? "N/A" + ); + } } catch (error) { console.error(error); } @@ -120,18 +123,6 @@ const DetailsPage = ({ isAdmin }) => { const [hoveredUptimeData, setHoveredUptimeData] = useState(null); const [hoveredIncidentsData, setHoveredIncidentsData] = useState(null); - const statusColor = { - true: theme.palette.success.main, - false: theme.palette.error.main, - undefined: theme.palette.warning.main, - }; - - const statusMsg = { - true: "Your site is up.", - false: "Your site is down.", - undefined: "Pending...", - }; - return ( {loading ? ( @@ -162,7 +153,7 @@ const DetailsPage = ({ isAdmin }) => { gap={theme.spacing(2)} > { }} > - + { - + active for {splitDuration(monitor?.uptimeDuration)} diff --git a/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx b/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx index 4a0325246..b68542f31 100644 --- a/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx +++ b/Client/src/Pages/Monitors/Home/MonitorTable/index.jsx @@ -32,6 +32,7 @@ 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 ActionsMenu from "../actionsMenu"; +import useUtils from "../../utils"; /** * Component for pagination actions (first, previous, next, last). @@ -47,7 +48,6 @@ import ActionsMenu from "../actionsMenu"; */ const TablePaginationActions = (props) => { const { count, page, rowsPerPage, onPageChange } = props; - const handleFirstPageButtonClick = (event) => { onPageChange(event, 0); }; @@ -110,6 +110,7 @@ const MonitorTable = ({ isAdmin }) => { const theme = useTheme(); const navigate = useNavigate(); const dispatch = useDispatch(); + const { determineState } = useUtils(); const { rowsPerPage } = useSelector((state) => state.ui.monitors); const [page, setPage] = useState(0); @@ -219,14 +220,11 @@ const MonitorTable = ({ isAdmin }) => { title: monitor.name, percentage: uptimePercentage, percentageColor, - status: - monitor.status === undefined - ? "pending" - : monitor.status === true - ? "up" - : "down", + status: determineState(monitor), }; + console.log(params); + return ( - Status - - - - - ), - }, - { id: 3, name: "Response Time" }, - { id: 4, name: "Type" }, - { id: 5, name: "Actions" }, - ], - rows: [], -}; - -/** - * Builds table data for a list of monitors. - * - * @param {Array} monitors - An array of monitor objects containing information about each monitor. - * @param {boolean} isAdmin - Flag indicating if the current user is an admin. - * @param {Function} navigate - A function to navigate to the monitor detail page. - * @returns {Object} The data structure containing columns and rows for the table. - */ -export const buildData = (monitors, isAdmin, navigate) => { - const theme = useTheme(); - - data.rows = monitors.map((monitor, idx) => { - let uptimePercentage = ""; - let percentageColor = theme.palette.percentage.uptimeExcellent; - - // Determine uptime percentage and color based on the monitor's uptimePercentage value - if (monitor.uptimePercentage !== undefined) { - uptimePercentage = - monitor.uptimePercentage === 0 - ? "0" - : (monitor.uptimePercentage * 100).toFixed(2); - - percentageColor = - monitor.uptimePercentage < 0.25 - ? theme.palette.percentage.uptimePoor - : monitor.uptimePercentage < 0.5 - ? theme.palette.percentage.uptimeFair - : monitor.uptimePercentage < 0.75 - ? theme.palette.percentage.uptimeGood - : theme.palette.percentage.uptimeExcellent; - } - - const params = { - url: monitor.url, - title: monitor.name, - percentage: uptimePercentage, - percentageColor, - status: - monitor.status === undefined - ? "pending" - : monitor.status === true - ? "up" - : "down", - }; - - // Reverse checks so the latest check is on the right - const reversedChecks = monitor.checks.slice().reverse(); - - return { - id: monitor._id, - handleClick: () => { - navigate(`/monitors/${monitor._id}`); - }, - data: [ - { id: idx, data: }, - { - id: idx + 1, - data: ( - - ), - }, - { id: idx + 2, data: }, - { - id: idx + 3, - data: ( - {monitor.type} - ), - }, - { - id: idx + 4, - data: , - }, - ], - }; - }); - return data; -}; diff --git a/Client/src/Pages/Monitors/utils.jsx b/Client/src/Pages/Monitors/utils.jsx new file mode 100644 index 000000000..0551a65aa --- /dev/null +++ b/Client/src/Pages/Monitors/utils.jsx @@ -0,0 +1,54 @@ +import { useTheme } from "@mui/material"; + +const useUtils = () => { + const theme = useTheme(); + const determineState = (monitor) => { + if (monitor.isActive === false) return "paused"; + if (monitor?.status === undefined) return "pending"; + return monitor?.status == true ? "up" : "down"; + }; + + const statusColor = { + up: theme.palette.success.main, + down: theme.palette.error.main, + paused: theme.palette.warning.main, + pending: theme.palette.warning.main, + }; + + const statusMsg = { + up: "Your site is up.", + down: "Your site is down.", + paused: "Pending...", + }; + const statusStyles = { + up: { + backgroundColor: theme.palette.success.bg, + borderColor: theme.palette.success.light, + "& h2": { color: theme.palette.success.main }, + }, + down: { + backgroundColor: theme.palette.error.bg, + borderColor: theme.palette.error.light, + "& h2": { color: theme.palette.error.main }, + }, + paused: { + backgroundColor: theme.palette.warning.light, + borderColor: theme.palette.warning.border, + "& h2": { color: theme.palette.warning.main }, + }, + pending: { + backgroundColor: theme.palette.warning.light, + borderColor: theme.palette.warning.border, + "& h2": { color: theme.palette.warning.main }, + }, + }; + + return { + determineState, + statusColor, + statusMsg, + statusStyles, + }; +}; + +export default useUtils; From db9f8e26e99d5dc3d2e0e921e2b363515f43cb6c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 8 Sep 2024 14:29:17 -0700 Subject: [PATCH 27/30] Add env files, update config --- Docker/docker-compose.yaml | 5 ++++- Docker/mongo/init/create_users.js | 16 ++++++++++++++++ Docker/mongoDB.Dockerfile | 3 ++- 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 Docker/mongo/init/create_users.js diff --git a/Docker/docker-compose.yaml b/Docker/docker-compose.yaml index c9fa3431f..b72b5fc60 100644 --- a/Docker/docker-compose.yaml +++ b/Docker/docker-compose.yaml @@ -33,8 +33,11 @@ services: - ./redis/data:/data mongodb: image: uptime_database_mongo:latest - command: ["mongod", "--quiet"] + command: ["mongod", "--quiet", "--auth"] ports: - "27017:27017" volumes: - ./mongo/data:/data/db + - ./mongo/init/create_users.js:/docker-entrypoint-initdb.d/create_users.js + env_file: + - mongo.env \ No newline at end of file diff --git a/Docker/mongo/init/create_users.js b/Docker/mongo/init/create_users.js new file mode 100644 index 000000000..67a21f910 --- /dev/null +++ b/Docker/mongo/init/create_users.js @@ -0,0 +1,16 @@ +var username = process.env.USERNAME_ENV_VAR; +var password = process.env.PASSWORD_ENV_VAR; + +db = db.getSiblingDB("uptime_db"); + +db.createUser({ + user: username, + pwd: password, + roles: [ + { + role: "readWrite", + db: "uptime_db", + }, + ], +}); +print("User uptime_user created successfully"); diff --git a/Docker/mongoDB.Dockerfile b/Docker/mongoDB.Dockerfile index fa79e8a92..a2ee5a645 100644 --- a/Docker/mongoDB.Dockerfile +++ b/Docker/mongoDB.Dockerfile @@ -1,2 +1,3 @@ FROM mongo -EXPOSE 27017 \ No newline at end of file +EXPOSE 27017 +CMD ["mongod", "--auth"] From 7e8f1cc3601e09d0214cf7019b5ca36313a6a35f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 8 Sep 2024 14:36:33 -0700 Subject: [PATCH 28/30] Add instructions to readme --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d396ae397..ce9d1dbf8 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ To get the application up and running you need to: CLIENT_HOST="http://localhost:5173" JWT_SECRET= DB_TYPE="MongoDB" -DB_CONNECTION_STRING="mongodb://mongodb:27017/uptime_db" +DB_CONNECTION_STRING="mongodb://:@mongodb:27017/uptime_db" REDIS_HOST="redis" REDIS_PORT=6379 TOKEN_TTL="99d" @@ -194,6 +194,13 @@ VITE_APP_API_BASE_URL="http://localhost:5000/api/v1" VITE_APP_API_LOG_LEVEL="debug" ``` +3. In the `Dokcer` directory create a `mongo.env` file with a username and password: + +``` +USERNAME_ENV_VAR=user +PASSWORD_ENV_VAR=password +``` + 4. In the `Docker` directory run `docker compose up` to run the `docker-compose.yaml` file and start all four images. That's it, the application is ready to use on port 80. From 58be4801ba5ef87169baba3aa47aa7ce127db0f1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 8 Sep 2024 14:37:21 -0700 Subject: [PATCH 29/30] revise order of readme --- README.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ce9d1dbf8..92ee37e35 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,15 @@ The fastest way to start the application is to use our Dockerfiles and [Docker C To get the application up and running you need to: 1. In the `Docker` directory run the build script `build_images.sh` to build docker images for the client, server, Redis database, and MongoDB database. -2. In the `Docker` directory, create a `server.env` file with the [requried environtmental variables](#env-vars-server) for the server. Sample file: + +2. In the `Dokcer` directory create a `mongo.env` file with a username and password: + +``` +USERNAME_ENV_VAR=user +PASSWORD_ENV_VAR=password +``` + +3. In the `Docker` directory, create a `server.env` file with the [requried environtmental variables](#env-vars-server) for the server. Sample file: ``` CLIENT_HOST="http://localhost:5173" @@ -187,21 +195,14 @@ SYSTEM_EMAIL_ADDRESS= SYSTEM_EMAIL_PASSWORD= ``` -3. In the `Client` directory, create a `client.env` file with the [required environtmental variables](#env-vars-client) for the client. Sample file: +4. In the `Client` directory, create a `client.env` file with the [required environtmental variables](#env-vars-client) for the client. Sample file: ``` VITE_APP_API_BASE_URL="http://localhost:5000/api/v1" VITE_APP_API_LOG_LEVEL="debug" ``` -3. In the `Dokcer` directory create a `mongo.env` file with a username and password: - -``` -USERNAME_ENV_VAR=user -PASSWORD_ENV_VAR=password -``` - -4. In the `Docker` directory run `docker compose up` to run the `docker-compose.yaml` file and start all four images. +5. In the `Docker` directory run `docker compose up` to run the `docker-compose.yaml` file and start all four images. That's it, the application is ready to use on port 80.
From b5a6c853e3a1f09c2453bf5065b8de47637eab19 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 9 Sep 2024 10:37:24 -0700 Subject: [PATCH 30/30] remove auth flag from mongoDB.Dockerfile --- Docker/mongoDB.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Docker/mongoDB.Dockerfile b/Docker/mongoDB.Dockerfile index a2ee5a645..969a320c2 100644 --- a/Docker/mongoDB.Dockerfile +++ b/Docker/mongoDB.Dockerfile @@ -1,3 +1,3 @@ FROM mongo EXPOSE 27017 -CMD ["mongod", "--auth"] +CMD ["mongod"]