From 3f418a45f51f32e51b8b4d8a364e83e6d70dc592 Mon Sep 17 00:00:00 2001 From: M M Date: Tue, 11 Jun 2024 15:15:32 -0700 Subject: [PATCH 01/12] Implemented the toastify notifications. --- Client/package-lock.json | 16 ++++++++++++++-- Client/package.json | 3 ++- Client/src/Components/Notifications/index.css | 0 Client/src/Components/Notifications/index.jsx | 10 ++++++++++ 4 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 Client/src/Components/Notifications/index.css create mode 100644 Client/src/Components/Notifications/index.jsx diff --git a/Client/package-lock.json b/Client/package-lock.json index 73edbf7fb..9fc7a39c1 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -25,7 +25,8 @@ "react-dom": "^18.2.0", "react-redux": "9.1.2", "react-router": "^6.23.0", - "react-router-dom": "^6.23.1" + "react-router-dom": "^6.23.1", + "react-toastify": "^10.0.5" }, "devDependencies": { "@types/react": "^18.2.66", @@ -2647,7 +2648,6 @@ "integrity": "sha512-NglN/xprcM+SHD2XCli4oC6bWe6kHoytcyLKCWXmRL854F0qhPhaYgUswUsglnPxYaNQIg2uMY4BvaomIf3kLA==", "dev": true, "license": "ISC" - }, "node_modules/error-ex": { "version": "1.3.2", @@ -4672,6 +4672,18 @@ "react-dom": ">=16.8" } }, + "node_modules/react-toastify": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.5.tgz", + "integrity": "sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==", + "dependencies": { + "clsx": "^2.1.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", diff --git a/Client/package.json b/Client/package.json index 505fe25d8..546b138cf 100644 --- a/Client/package.json +++ b/Client/package.json @@ -27,7 +27,8 @@ "react-dom": "^18.2.0", "react-redux": "9.1.2", "react-router": "^6.23.0", - "react-router-dom": "^6.23.1" + "react-router-dom": "^6.23.1", + "react-toastify": "^10.0.5" }, "devDependencies": { "@types/react": "^18.2.66", diff --git a/Client/src/Components/Notifications/index.css b/Client/src/Components/Notifications/index.css new file mode 100644 index 000000000..e69de29bb diff --git a/Client/src/Components/Notifications/index.jsx b/Client/src/Components/Notifications/index.jsx new file mode 100644 index 000000000..b2bf09fb9 --- /dev/null +++ b/Client/src/Components/Notifications/index.jsx @@ -0,0 +1,10 @@ +import { ToastContainer, toast } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; + +const Notification = () => { + return ; +}; + +export const notify = (message) => toast(message); + +export default Notification; From 9cf4f7bb606a66039c0bc04d6d3aa876402de8dc Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 12 Jun 2024 14:15:58 -0700 Subject: [PATCH 02/12] Added error messages to utility class --- Server/utils/messages.js | 57 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 Server/utils/messages.js diff --git a/Server/utils/messages.js b/Server/utils/messages.js new file mode 100644 index 000000000..d49aa5e68 --- /dev/null +++ b/Server/utils/messages.js @@ -0,0 +1,57 @@ +const errorMessages = { + // General Errors: + FRIENDLY_ERROR: "Something went wrong...", + UNKNOWN_ERROR: "An unknown error occurred", + // Auth Controller + UNAUTHORIZED: "Unauthorized access", + + //Error handling middleware + NO_AUTH_TOKEN: "No auth token provided", + INVALID_AUTH_TOKEN: "Invalid auth token", + + //Onwership Middleware + VERIFY_OWNER_NOT_FOUND: "Document not found", + VERIFY_OWNER_UNAUTHORIZED: "Unauthorized access", + + //DB Errors + DB_USER_NOT_FOUND: "User not found", + DB_TOKEN_NOT_FOUND: "Token not found", + DB_RESET_PASSWORD_BAD_MATCH: + "New password must be different from old password", + DB_FIND_MONTIOR_BY_ID: (monitorId) => + `Monitor with id ${monitorId} not found`, + DB_DELETE_CHECKS: (monitorId) => + `No checks found for monitor with id ${monitorId}`, + DB_DELETE_ALERT: (alertId) => `Alert with id ${alertId} not found`, +}; + +const successMessages = { + //Alert Controller + ALERT_CREATE: "Alert created successfully", + ALERT_GET: "Got alerts successfully", + ALERT_GET_BY_MONITOR: "Got alerts by Monitor successfully", + ALERT_GET_BY_ID: "Got alert by Id successfully", + ALERT_EDIT: "Alert edited successfully", + ALERT_DELETE: "Alert deleted successfully", + + // Auth Controller + AUTH_CREATE_USER: "User created successfully", + AUTH_LOGIN_USER: "User logged in successfully", + AUTH_UPDATE_USER: "User updated successfully", + AUTH_CREATE_RECOVERY_TOKEN: "Recovery token created successfully", + AUTH_VERIFY_RECOVERY_TOKEN: "Recovery token verified successfully", + AUTH_RESET_PASSWORD: "Password reset successfully", + + // Check Controller + CHECK_CREATE: "Check created successfully", + CHECK_GET: "Got checks successfully", + CHECK_DELETE: "Checks deleted successfully", + + //Monitor Controller + MONITOR_GET_ALL: "Got all monitors successfully", + MONTIOR_GET_BY_ID: "Got monitor by Id successfully", + MONITOR_GET_BY_USERID: "Got monitor by UserId successfully", + MONITOR_CREATE: "Monitor created successfully", + MONITOR_DELETE: "Monitor deleted successfully", + MONITOR_EDIT: "Monitor edited successfully", +}; From 58313dde5ce7f2d1955bd8e2d372420ff77edb4a Mon Sep 17 00:00:00 2001 From: M M Date: Wed, 12 Jun 2024 16:12:18 -0700 Subject: [PATCH 03/12] Matched toast notification to existing error notifications. --- Client/src/Components/Notifications/index.css | 0 Client/src/Components/Notifications/index.jsx | 10 -- Client/src/Components/Toast/index.css | 103 ++++++++++++++++++ Client/src/Components/Toast/index.jsx | 63 +++++++++++ Client/src/Pages/PlayGround/PlayGround.jsx | 2 + 5 files changed, 168 insertions(+), 10 deletions(-) delete mode 100644 Client/src/Components/Notifications/index.css delete mode 100644 Client/src/Components/Notifications/index.jsx create mode 100644 Client/src/Components/Toast/index.css create mode 100644 Client/src/Components/Toast/index.jsx diff --git a/Client/src/Components/Notifications/index.css b/Client/src/Components/Notifications/index.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/Client/src/Components/Notifications/index.jsx b/Client/src/Components/Notifications/index.jsx deleted file mode 100644 index b2bf09fb9..000000000 --- a/Client/src/Components/Notifications/index.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import { ToastContainer, toast } from 'react-toastify'; -import 'react-toastify/dist/ReactToastify.css'; - -const Notification = () => { - return ; -}; - -export const notify = (message) => toast(message); - -export default Notification; diff --git a/Client/src/Components/Toast/index.css b/Client/src/Components/Toast/index.css new file mode 100644 index 000000000..0653f3d27 --- /dev/null +++ b/Client/src/Components/Toast/index.css @@ -0,0 +1,103 @@ +.toast-container { + position: fixed; + bottom: 20px; /* Adjust as needed */ + right: 20px; /* Adjust as needed */ + z-index: 1000; /* Ensure it's above other content */ + } + + .toast { + background-color: #ffffff; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2); + border-radius: 8px; + padding: 16px; + min-width: 300px; /* Adjust minimum width as needed */ + max-width: 600px; /* Adjust maximum width as needed */ + position: relative; + animation: slideIn 0.3s ease forwards; + overflow: hidden; /* Ensure progress bar stays within bounds */ + display: flex; /* Use flexbox for layout */ + align-items: center; /* Center items vertically */ + } + + @keyframes slideIn { + 0% { + transform: translateY(-100%); + opacity: 0; + } + 100% { + transform: translateY(0%); + opacity: 1; + } + } + + .toast .info-icon-frame { + position: absolute; + top: 15px; /* Adjust top position as needed */ + left: 20px; /* Adjust left position as needed */ + } + + .icon { + position: absolute; + top: 15px; /* Adjust top position as needed */ + left: 20px; /* Adjust left position as needed */ + } + + .toast .toast-content { + flex-grow: 1; /* Allow content to expand */ + } + + .toast .announcement-content { + display: flex; + flex-direction: column; + margin-left: 10px; + } + + .toast .announcement-content-subject { + font-weight: bold; + margin-bottom: 8px; /* Space below subject */ + margin-left: 40px; /* Adjust left margin for more space */ + } + + .toast .announcement-content-body { + color: #555; + white-space: nowrap; /* Prevent text from wrapping */ + overflow: hidden; /* Hide any overflow */ + text-overflow: ellipsis; /* Show ellipsis (...) if the text overflows */ + margin-left: 40px; /* Adjust left margin for more space */ + } + + .toast .announcement-content-body-title { + display: inline-block; + margin-right: 5px; /* Adjust spacing between title and body */ + } + + .toast .announcement-content-controllers { + margin-top: 12px; + } + + .toast .controllers-button-esc, + .toast .controllers-button-primary { + cursor: pointer; + padding: 8px 16px; + margin-left: 23px; /* Adjust left margin for closer proximity */ + } + + + .toast-progress { + position: absolute; + bottom: 0; + left: 0; + height: 4px; + background-color: #29b6f6; /* Adjust color as needed */ + transition: width 0.1s linear; + } + + .close-button { + position: absolute; + top: 1px; + right: 10px; + cursor: pointer; + font-size: 35px; + color: #aaa; + } + \ No newline at end of file diff --git a/Client/src/Components/Toast/index.jsx b/Client/src/Components/Toast/index.jsx new file mode 100644 index 000000000..37870a970 --- /dev/null +++ b/Client/src/Components/Toast/index.jsx @@ -0,0 +1,63 @@ +import { useState, useEffect } from "react"; +import ComplexAlert from "../../Components/Icons/ComplexAlert/ComplexAlert"; +import "./index.css"; // Import your CSS file for toast styles + +const Toast = () => { + const [showToast, setShowToast] = useState(false); + const [progress, setProgress] = useState(100); // Start with full progress + + useEffect(() => { + let timer; + if (showToast) { + timer = setTimeout(() => { + setShowToast(false); + }, 5000); // 5000 milliseconds = 5 seconds + + // Interval to update progress bar + const interval = setInterval(() => { + setProgress((prevProgress) => prevProgress - (100 / 5000 * 100)); // Decrease progress based on time + }, 100); // Update every 100 milliseconds + + return () => { + clearTimeout(timer); + clearInterval(interval); + }; + } + }, [showToast]); + + const toggleToast = () => { + setShowToast(!showToast); + setProgress(100); // Reset progress when showing toast + }; + + return ( +
+ + {showToast && ( +
+ × +
+ {} +
+
+
+
+ There was a problem with that action +
+
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquid pariatur, ipsum dolor. +
+
+
Dismiss
+
View changes
+
+
+
+
+
+ )} +
+ ); +}; + +export default Toast; diff --git a/Client/src/Pages/PlayGround/PlayGround.jsx b/Client/src/Pages/PlayGround/PlayGround.jsx index b5e439aed..5f46af08d 100644 --- a/Client/src/Pages/PlayGround/PlayGround.jsx +++ b/Client/src/Pages/PlayGround/PlayGround.jsx @@ -13,6 +13,7 @@ import AnnouncementUpdateSubscription from "../../Components/Announcements/Annou import PlayGroundCharts from "./PlayGround-Charts"; import PlayGroundPopupModals from "./PlayGround-Popup-Modals"; import PlayGroundTooltips from "./PlayGround-Tooltips"; +import Toast from "../../Components/Toast"; // This Component is just for the development and test // purposes and just to see what my components look like while development @@ -52,6 +53,7 @@ function PlayGround() { hasCopyButton={true} hintText="This is a hint text to help user." /> +
{/* Now, illustration of the Description text fields */} From 5da22f440360520d33820ab0289c22caf0570151 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 14 Jun 2024 13:45:45 -0700 Subject: [PATCH 04/12] Refactored hardcoded error messages to util file. Added logging to job queue --- Server/controllers/alertController.js | 20 ++++++------ Server/controllers/authController.js | 43 ++++++++++++++++--------- Server/controllers/checkController.js | 11 +++++-- Server/controllers/monitorController.js | 38 ++++++++++++++++------ Server/db/MongoDB.js | 21 ++++++------ Server/middleware/handleErrors.js | 5 +-- Server/middleware/verifyJWT.js | 12 ++++--- Server/middleware/verifyOwnership.js | 7 ++-- Server/service/jobQueue.js | 14 ++++++-- Server/utils/messages.js | 27 ++++++++++++++-- 10 files changed, 138 insertions(+), 60 deletions(-) diff --git a/Server/controllers/alertController.js b/Server/controllers/alertController.js index bcc408a25..cd2fa01ac 100644 --- a/Server/controllers/alertController.js +++ b/Server/controllers/alertController.js @@ -10,7 +10,7 @@ const { editAlertBodyValidation, deleteAlertParamValidation, } = require("../validation/joi"); - +const { successMessages } = require("../utils/messages"); const SERVICE_NAME = "alerts"; /** @@ -38,7 +38,7 @@ const createAlert = async (req, res, next) => { const alert = await req.db.createAlert(alertData); return res .status(200) - .json({ success: true, msg: "Alert created", data: alert }); + .json({ success: true, msg: successMessages.ALERT_CREATE, data: alert }); } catch (error) { error.service = SERVICE_NAME; next(error); @@ -66,9 +66,11 @@ const getAlertsByUserId = async (req, res, next) => { try { const alerts = await req.db.getAlertsByUserId(req.params.userId); - return res - .status(200) - .json({ success: true, msg: "Got alerts", data: alerts }); + return res.status(200).json({ + success: true, + msg: successMessages.ALERT_GET_BY_USER, + data: alerts, + }); } catch (error) { error.service = SERVICE_NAME; next(error); @@ -90,7 +92,7 @@ const getAlertsByMonitorId = async (req, res, next) => { const alerts = await req.db.getAlertsByMonitorId(req.params.monitorId); return res.status(200).json({ success: true, - msg: "Got alerts by Monitor", + msg: successMessages.ALERT_GET_BY_MONITOR, data: alerts, }); } catch (error) { @@ -114,7 +116,7 @@ const getAlertById = async (req, res, next) => { const alert = await req.db.getAlertById(req.params.alertId); return res.status(200).json({ success: true, - msg: "Got Alert By alertID", + msg: successMessages.ALERT_GET_BY_ID, data: alert, }); } catch (error) { @@ -141,7 +143,7 @@ const editAlert = async (req, res, next) => { const alert = await req.db.editAlert(req.params.alertId, alertData); return res.status(200).json({ success: true, - msg: "Edited alert", + msg: successMessages.ALERT_EDIT, data: alert, }); } catch (error) { @@ -164,7 +166,7 @@ const deleteAlert = async (req, res, next) => { const deleted = await req.db.deleteAlert(req.params.alertId); return res.status(200).json({ success: true, - msg: "Deleted alert", + msg: successMessages.ALERT_DELETE, data: deleted, }); } catch (error) { diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index aa3b0f536..6c23ba404 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -10,6 +10,7 @@ const { } = require("../validation/joi"); const logger = require("../utils/logger"); require("dotenv").config(); +const { errorMessages, successMessages } = require("../utils/messages"); var jwt = require("jsonwebtoken"); const SERVICE_NAME = "auth"; const { sendEmail } = require("../utils/sendEmail"); @@ -49,7 +50,7 @@ const registerController = async (req, res, next) => { // Create a new user try { const newUser = await req.db.insertUser(req, res); - logger.info("New user created!", { + logger.info(successMessages.AUTH_CREATE_USER, { service: SERVICE_NAME, userId: newUser._id, }); @@ -64,9 +65,11 @@ const registerController = async (req, res, next) => { "Registered." ); - return res - .status(200) - .json({ success: true, msg: "User created", data: token }); + return res.status(200).json({ + success: true, + msg: successMessages.AUTH_CREATE_USER, + data: token, + }); } catch (error) { error.service = SERVICE_NAME; next(error); @@ -92,7 +95,7 @@ const loginController = async (req, res, next) => { const match = await user.comparePassword(req.body.password); if (match !== true) { - throw new Error("Incorrect password"); + throw new Error(errorMessages.AUTH_INCORRECT_PASSWORD); } // Remove password from user object. Should this be abstracted to DB layer? @@ -101,9 +104,11 @@ const loginController = async (req, res, next) => { // Happy path, return token const token = issueToken(userWithoutPassword); - return res - .status(200) - .json({ success: true, msg: "Found user", data: token }); + return res.status(200).json({ + success: true, + msg: successMessages.AUTH_LOGIN_USER, + data: token, + }); } catch (error) { error.status = 500; // Anything else should be an error @@ -124,7 +129,7 @@ const userEditController = async (req, res, next) => { } if (req.params.userId !== req.user._id.toString()) { - const error = new Error("Unauthorized access"); + const error = new Error(errorMessages.AUTH_UNAUTHORIZED); error.status = 401; error.service = SERVICE_NAME; next(error); @@ -133,9 +138,11 @@ const userEditController = async (req, res, next) => { try { const updatedUser = await req.db.updateUser(req, res); - return res - .status(200) - .json({ success: true, msg: "User updated", data: updatedUser }); + return res.status(200).json({ + success: true, + msg: successMessages.AUTH_UPDATE_USER, + data: updatedUser, + }); } catch (error) { error.service = SERVICE_NAME; next(error); @@ -166,7 +173,7 @@ const recoveryRequestController = async (req, res, next) => { ); return res.status(200).json({ success: true, - msg: "Created recovery token", + msg: successMessages.AUTH_CREATE_RECOVERY_TOKEN, }); } // TODO Email token to user @@ -193,7 +200,7 @@ const validateRecoveryTokenController = async (req, res, next) => { // TODO Redirect user to reset password after validating token return res.status(200).json({ success: true, - msg: "Token is valid", + msg: successMessages.AUTH_VERIFY_RECOVERY_TOKEN, }); } catch (error) { error.service = SERVICE_NAME; @@ -216,7 +223,13 @@ const resetPasswordController = async (req, res, next) => { try { await newPasswordValidation.validateAsync(req.body); user = await req.db.resetPassword(req, res); - res.status(200).json({ success: true, msg: "Password reset", data: user }); + res + .status(200) + .json({ + success: true, + msg: successMessages.AUTH_RESET_PASSWORD, + data: user, + }); } catch (error) { error.service = SERVICE_NAME; next(error); diff --git a/Server/controllers/checkController.js b/Server/controllers/checkController.js index 7734da45b..391ae2700 100644 --- a/Server/controllers/checkController.js +++ b/Server/controllers/checkController.js @@ -4,6 +4,7 @@ const { getChecksParamValidation, deleteChecksParamValidation, } = require("../validation/joi"); +const { successMessages } = require("../utils/messages"); const SERVICE_NAME = "check"; const createCheck = async (req, res, next) => { @@ -23,7 +24,7 @@ const createCheck = async (req, res, next) => { const check = await req.db.createCheck(checkData); return res .status(200) - .json({ success: true, msg: "Check created", data: check }); + .json({ success: true, msg: successMessages.CHECK_CREATE, data: check }); } catch (error) { error.service = SERVICE_NAME; next(error); @@ -45,7 +46,7 @@ const getChecks = async (req, res, next) => { const checks = await req.db.getChecks(req.params.monitorId); return res .status(200) - .json({ success: true, msg: "Checks retrieved", data: checks }); + .json({ success: true, msg: successMessages.CHECK_GET, data: checks }); } catch (error) { error.service = SERVICE_NAME; next(error); @@ -67,7 +68,11 @@ const deleteChecks = async (req, res, next) => { const deletedCount = await req.db.deleteChecks(req.params.monitorId); return res .status(200) - .json({ success: true, msg: "Checks deleted", data: { deletedCount } }); + .json({ + success: true, + msg: successMessages.CHECK_DELETE, + data: { deletedCount }, + }); } catch (error) { error.service = SERVICE_NAME; next(error); diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index a8743fe5e..fb1a0fc78 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -5,6 +5,8 @@ const { } = require("../validation/joi"); const SERVICE_NAME = "monitorController"; +const { errorMessages, successMessages } = require("../utils/messages"); +const { error } = require("winston"); /** * Returns all monitors @@ -17,7 +19,11 @@ const SERVICE_NAME = "monitorController"; const getAllMonitors = async (req, res, next) => { try { const monitors = await req.db.getAllMonitors(); - return res.json({ success: true, msg: "Monitors found", data: monitors }); + return res.json({ + success: true, + msg: successMessages.MONITOR_GET_ALL, + data: monitors, + }); } catch (error) { error.service = SERVICE_NAME; next(error); @@ -45,12 +51,16 @@ const getMonitorById = async (req, res, next) => { try { const monitor = await req.db.getMonitorById(req, res); if (!monitor) { - const error = new Error("Monitor not found"); + const error = new Error(errorMessages.MONITOR_GET_BY_ID); error.status = 404; throw error; } - return res.json({ success: true, msg: "Monitor found", data: monitor }); + return res.json({ + success: true, + msg: successMessages.MONTIOR_GET_BY_ID, + data: monitor, + }); } catch (error) { error.service = SERVICE_NAME; next(error); @@ -78,14 +88,14 @@ const getMonitorsByUserId = async (req, res, next) => { const monitors = await req.db.getMonitorsByUserId(req, res); if (monitors && monitors.length === 0) { - const err = new Error("No monitors found"); + const err = new Error(errorMessages.MONITOR_GET_BY_USER_ID); err.status = 404; throw err; } return res.json({ success: true, - msg: `Monitors for user ${userId} found`, + msg: successMessages.MONITOR_GET_BY_USER_ID(userId), data: monitors, }); } catch (error) { @@ -118,9 +128,11 @@ const createMonitor = async (req, res, next) => { const monitor = await req.db.createMonitor(req, res); // Add monitor to job queue req.jobQueue.addJob(monitor._id, monitor); - return res - .status(201) - .json({ success: true, msg: "Monitor created", data: monitor }); + return res.status(201).json({ + success: true, + msg: successMessages.MONITOR_CREATE, + data: monitor, + }); } catch (error) { error.service = SERVICE_NAME; next(error); @@ -157,7 +169,9 @@ const deleteMonitor = async (req, res, next) => { * when it is deleted so there is no orphaned data * We also need to make sure to stop all running services for this monitor */ - return res.status(200).json({ success: true, msg: "Monitor deleted" }); + return res + .status(200) + .json({ success: true, msg: successMessages.MONITOR_DELETE }); } catch (error) { error.service = SERVICE_NAME; next(error); @@ -188,7 +202,11 @@ const editMonitor = async (req, res, next) => { const editedMonitor = await req.db.editMonitor(req, res); return res .status(200) - .json({ success: true, msg: "Monitor edited", data: editedMonitor }); + .json({ + success: true, + msg: successMessages.MONITOR_EDIT, + data: editedMonitor, + }); } catch (error) { error.service = SERVICE_NAME; next(error); diff --git a/Server/db/MongoDB.js b/Server/db/MongoDB.js index 4ec6e8422..4f1d0c5a7 100644 --- a/Server/db/MongoDB.js +++ b/Server/db/MongoDB.js @@ -6,7 +6,8 @@ const Alert = require("../models/Alert"); const RecoveryToken = require("../models/RecoveryToken"); const crypto = require("crypto"); const DUPLICATE_KEY_CODE = 11000; // MongoDB error code for duplicate key - +const { errorMessages, successMessages } = require("../utils/messages"); +const { error } = require("console"); const connect = async () => { try { await mongoose.connect(process.env.DB_CONNECTION_STRING); @@ -36,7 +37,7 @@ const insertUser = async (req, res) => { return await UserModel.findOne({ _id: newUser._id }).select("-password"); // .select() doesn't work with create, need to save then find } catch (error) { if (error.code === DUPLICATE_KEY_CODE) { - throw new Error("Email already exists"); + throw new Error(errorMessages.DB_USER_EXISTS); } throw error; } @@ -61,7 +62,7 @@ const getUserByEmail = async (req, res) => { if (user) { return user; } else { - throw new Error("User not found"); + throw new Error(errorMessages.DB_USER_NOT_FOUND); } } catch (error) { throw error; @@ -125,7 +126,7 @@ const validateRecoveryToken = async (req, res) => { if (recoveryToken !== null) { return recoveryToken; } else { - throw new Error("Token not found"); + throw new Error(errorMessages.DB_TOKEN_NOT_FOUND); } } catch (error) { throw error; @@ -142,7 +143,7 @@ const resetPassword = async (req, res) => { const match = await user.comparePassword(newPassword); if (match === true) { - throw new Error("New password must be different from old password"); + throw new Error(errorMessages.DB_RESET_PASSWORD_BAD_MATCH); } if (user !== null) { @@ -155,7 +156,7 @@ const resetPassword = async (req, res) => { }).select("-password"); return userWithoutPassword; } else { - throw new Error("User not found"); + throw new Error(errorMessages.DB_USER_NOT_FOUND); } } catch (error) { throw error; @@ -249,7 +250,7 @@ const deleteMonitor = async (req, res) => { try { const monitor = await Monitor.findByIdAndDelete(monitorId); if (!monitor) { - throw new Error(`Monitor with id ${monitorId} not found`); + throw new Error(errorMessages.DB_FIND_MONTIOR_BY_ID(monitorId)); } return monitor; } catch (error) { @@ -338,7 +339,7 @@ const deleteChecks = async (monitorId) => { if (result.deletedCount > 0) { return result.deletedCount; } else { - throw new Error(`No checks found for monitor with id ${monitorId}`); + throw new Error(errorMessages.DB_DELETE_CHECKS(monitorId)); } } catch (error) { throw error; @@ -458,7 +459,9 @@ const deleteAlert = async (alertId) => { const result = await Alert.findByIdAndDelete(alertId); if (result) { return result; - } else throw new Error(`Alert with id ${alertId} not found`); + } else { + throw new Error(errorMessages.DB_DELETE_ALERT(alertId)); + } } catch (error) { throw error; } diff --git a/Server/middleware/handleErrors.js b/Server/middleware/handleErrors.js index b8690b438..1e1e955c5 100644 --- a/Server/middleware/handleErrors.js +++ b/Server/middleware/handleErrors.js @@ -1,9 +1,10 @@ const logger = require("../utils/logger"); +const { errorMessages } = require("../utils/messages"); const handleErrors = (error, req, res, next) => { const status = error.status || 500; - const message = error.message || "Something went wrong"; - const service = error.service || "Unknown service"; + const message = error.message || errorMessages.FRIENDLY_ERROR; + const service = error.service || errorMessages.UNKNOWN_SERVICE; logger.error(error.message, { service: service }); res.status(status).json({ success: false, msg: message }); }; diff --git a/Server/middleware/verifyJWT.js b/Server/middleware/verifyJWT.js index 2e8895623..9c9a3759a 100644 --- a/Server/middleware/verifyJWT.js +++ b/Server/middleware/verifyJWT.js @@ -2,7 +2,7 @@ const jwt = require("jsonwebtoken"); const logger = require("../utils/logger"); const SERVICE_NAME = "verifyJWT"; const TOKEN_PREFIX = "Bearer "; - +const { errorMessages } = require("../utils/messages"); /** * Verifies the JWT token * @function @@ -15,7 +15,7 @@ const verifyJWT = (req, res, next) => { const token = req.headers["authorization"]; // Make sure a token is provided if (!token) { - const error = new Error("No token provided"); + const error = new Error(error.errorMessages.NO_AUTH_TOKEN); error.status = 401; error.service = SERVICE_NAME; next(error); @@ -27,8 +27,12 @@ const verifyJWT = (req, res, next) => { // Verify the token's authenticity jwt.verify(parsedToken, process.env.JWT_SECRET, (err, decoded) => { if (err) { - logger.error("Invalid token", { service: SERVICE_NAME }); - return res.status(401).json({ success: false, msg: "Invalid token" }); + logger.error(errorMessages.INVALID_AUTH_TOKEN, { + service: SERVICE_NAME, + }); + return res + .status(401) + .json({ success: false, msg: errorMessages.INVALID_AUTH_TOKEN }); } //Add the user to the request object for use in the route req.user = decoded; diff --git a/Server/middleware/verifyOwnership.js b/Server/middleware/verifyOwnership.js index 409303fa4..cc7f544f7 100644 --- a/Server/middleware/verifyOwnership.js +++ b/Server/middleware/verifyOwnership.js @@ -1,5 +1,6 @@ const logger = require("../utils/logger"); const SERVICE_NAME = "verifyOwnership"; +const { errorMessages } = require("../utils/messages"); const verifyOwnership = (Model, paramName) => { return async (req, res, next) => { @@ -9,10 +10,10 @@ const verifyOwnership = (Model, paramName) => { const doc = await Model.findById(documentId); //If the document is not found, return a 404 error if (!doc) { - logger.error("Document not found", { + logger.error(errorMessages.VERIFY_OWNER_NOT_FOUND, { service: SERVICE_NAME, }); - const error = new Error("Document not found"); + const error = new Error(errorMessages.VERIFY_OWNER_NOT_FOUND); error.status = 404; throw error; } @@ -20,7 +21,7 @@ const verifyOwnership = (Model, paramName) => { // If the userID does not match the document's userID, return a 403 error if (userId.toString() !== doc.userId.toString()) { console.log("boom"); - const error = new Error("Unauthorized access"); + const error = new Error(errorMessages.VERIFY_OWNER_UNAUTHORIZED); error.status = 403; throw error; } diff --git a/Server/service/jobQueue.js b/Server/service/jobQueue.js index 58bbbfc09..21e315a4d 100644 --- a/Server/service/jobQueue.js +++ b/Server/service/jobQueue.js @@ -6,6 +6,7 @@ const connection = { }; const JOBS_PER_WORKER = 5; const logger = require("../utils/logger"); +const { errorMessages, successMessages } = require("../utils/messages"); const SERVICE_NAME = "JobQueue"; const axios = require("axios"); @@ -52,6 +53,7 @@ class JobQueue { const worker = new Worker( QUEUE_NAME, async (job) => { + //TODO add a check to see what type of service const response = await axios.get(job.data.url); // TODO create a check object and save it to the db console.log(response.status); @@ -135,7 +137,9 @@ class JobQueue { await worker.close(); } catch (error) { // Catch the error instead of throwing it - console.error("Error closing worker", error); + logger.error(errorMessages.JOB_QUEUE_WORKER_CLOSE, { + service: SERVICE_NAME, + }); } } return true; @@ -194,12 +198,12 @@ class JobQueue { every: monitor.interval, }); if (deleted) { - logger.info("Job removed from queue", { + logger.info(successMessages.JOB_QUEUE_DELETE_JOB, { service: SERVICE_NAME, jobId: monitor.id, }); } else { - logger.error("Job not found in queue", { + logger.error(errorMessages.JOB_QUEUE_DELETE_JOB, { service: SERVICE_NAME, jobId: monitor.id, }); @@ -216,8 +220,12 @@ class JobQueue { async obliterate() { try { await this.queue.obliterate(); + logger.info(successMessages.JOB_QUEUE_OBLITERATE, { + service: SERVICE_NAME, + }); return true; } catch (error) { + logger.error(errorMessages.JOB_QUEUE_OBLITERATE); throw error; } } diff --git a/Server/utils/messages.js b/Server/utils/messages.js index d49aa5e68..fea235baa 100644 --- a/Server/utils/messages.js +++ b/Server/utils/messages.js @@ -6,6 +6,7 @@ const errorMessages = { UNAUTHORIZED: "Unauthorized access", //Error handling middleware + UNKNOWN_SERVICE: "Unknown service", NO_AUTH_TOKEN: "No auth token provided", INVALID_AUTH_TOKEN: "Invalid auth token", @@ -14,6 +15,7 @@ const errorMessages = { VERIFY_OWNER_UNAUTHORIZED: "Unauthorized access", //DB Errors + DB_USER_EXISTS: "User already exists", DB_USER_NOT_FOUND: "User not found", DB_TOKEN_NOT_FOUND: "Token not found", DB_RESET_PASSWORD_BAD_MATCH: @@ -23,12 +25,24 @@ const errorMessages = { DB_DELETE_CHECKS: (monitorId) => `No checks found for monitor with id ${monitorId}`, DB_DELETE_ALERT: (alertId) => `Alert with id ${alertId} not found`, + + //Auth errors + AUTH_INCORRECT_PASSWORD: "Incorrect password", + AUTH_UNAUTHORIZED: "Unauthorized access", + + // Monitor Errros + MONITOR_GET_BY_ID: "Monitor not found", + MONITOR_GET_BY_USER_ID: "No monitors found for user", + + // Job Queue Errors + JOB_QUEUE_WORKER_CLOSE: "Error closing worker", + JOB_QUEUE_DELETE_JOB: "Job not found in queue", }; const successMessages = { //Alert Controller ALERT_CREATE: "Alert created successfully", - ALERT_GET: "Got alerts successfully", + ALERT_GET_BY_USER: "Got alerts successfully", ALERT_GET_BY_MONITOR: "Got alerts by Monitor successfully", ALERT_GET_BY_ID: "Got alert by Id successfully", ALERT_EDIT: "Alert edited successfully", @@ -50,8 +64,17 @@ const successMessages = { //Monitor Controller MONITOR_GET_ALL: "Got all monitors successfully", MONTIOR_GET_BY_ID: "Got monitor by Id successfully", - MONITOR_GET_BY_USERID: "Got monitor by UserId successfully", + MONITOR_GET_BY_USER_ID: (userId) => `Got monitor for ${userId} successfully"`, MONITOR_CREATE: "Monitor created successfully", MONITOR_DELETE: "Monitor deleted successfully", MONITOR_EDIT: "Monitor edited successfully", + + //Job Queue + JOB_QUEUE_DELETE_JOB: "Job removed successfully", + JOB_QUEUE_OBLITERATE: "Queue OBLITERATED!!!", +}; + +module.exports = { + errorMessages, + successMessages, }; From 3269e07edfaa935ad027822f1d9ac22fd7c5c0bd Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 14 Jun 2024 14:02:18 -0700 Subject: [PATCH 05/12] Fixed obliterate queue msg --- Server/utils/messages.js | 1 + 1 file changed, 1 insertion(+) diff --git a/Server/utils/messages.js b/Server/utils/messages.js index fea235baa..cb435757c 100644 --- a/Server/utils/messages.js +++ b/Server/utils/messages.js @@ -37,6 +37,7 @@ const errorMessages = { // Job Queue Errors JOB_QUEUE_WORKER_CLOSE: "Error closing worker", JOB_QUEUE_DELETE_JOB: "Job not found in queue", + JOB_QUEUE_OBLITERATE: "Error obliterating queue", }; const successMessages = { From f9f73dbc4719448521658696e8d1a199b01c2fd6 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 14 Jun 2024 14:31:07 -0700 Subject: [PATCH 06/12] Implemented ping type for monitors, added cleanup for job queue --- Server/index.js | 10 ++++++++++ Server/models/Monitor.js | 5 +++++ Server/package-lock.json | 10 ++++++++++ Server/package.json | 1 + Server/service/jobQueue.js | 33 ++++++++++++++++++++++++++++++--- Server/validation/joi.js | 3 ++- 6 files changed, 58 insertions(+), 4 deletions(-) diff --git a/Server/index.js b/Server/index.js index 0d5be15ea..eab64e36d 100644 --- a/Server/index.js +++ b/Server/index.js @@ -93,6 +93,16 @@ const startApp = async () => { await connectDbAndRunServer(app, db); const jobQueue = await JobQueue.createJobQueue(db); + + const cleanup = async () => { + console.log("Shutting down gracefully"); + await jobQueue.obliterate(); + console.log("Finished cleanup"); + process.exit(0); + }; + + process.on("SIGINT", cleanup); + process.on("SIGTERM", cleanup); }; startApp().catch((error) => { diff --git a/Server/models/Monitor.js b/Server/models/Monitor.js index 4aaad1497..8455aa312 100644 --- a/Server/models/Monitor.js +++ b/Server/models/Monitor.js @@ -14,6 +14,11 @@ const MonitorSchema = mongoose.Schema( description: { type: String, }, + type: { + type: String, + required: true, + enum: ["http", "ping"], + }, url: { type: String, required: true, diff --git a/Server/package-lock.json b/Server/package-lock.json index 9b8273ebd..edfb2ef44 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -21,6 +21,7 @@ "jsonwebtoken": "9.0.2", "mailersend": "^2.2.0", "mongoose": "^8.3.3", + "ping": "0.4.4", "winston": "^3.13.0" }, "devDependencies": { @@ -2205,6 +2206,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/ping": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/ping/-/ping-0.4.4.tgz", + "integrity": "sha512-56ZMC0j7SCsMMLdOoUg12VZCfj/+ZO+yfOSjaNCRrmZZr6GLbN2X/Ui56T15dI8NhiHckaw5X2pvyfAomanwqQ==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", diff --git a/Server/package.json b/Server/package.json index 2d21452c1..3df37cda6 100644 --- a/Server/package.json +++ b/Server/package.json @@ -23,6 +23,7 @@ "jsonwebtoken": "9.0.2", "mailersend": "^2.2.0", "mongoose": "^8.3.3", + "ping": "0.4.4", "winston": "^3.13.0" }, "devDependencies": { diff --git a/Server/service/jobQueue.js b/Server/service/jobQueue.js index 58bbbfc09..cf0c795f4 100644 --- a/Server/service/jobQueue.js +++ b/Server/service/jobQueue.js @@ -8,6 +8,7 @@ const JOBS_PER_WORKER = 5; const logger = require("../utils/logger"); const SERVICE_NAME = "JobQueue"; const axios = require("axios"); +const ping = require("ping"); class JobQueue { /** @@ -35,6 +36,9 @@ class JobQueue { try { queue.db = db; const monitors = await db.getAllMonitors(); + for (const monitor of monitors) { + await queue.addJob(monitor.id, monitor); + } const workerStats = await queue.getWorkerStats(); await queue.scaleWorkers(workerStats); return queue; @@ -52,9 +56,25 @@ class JobQueue { const worker = new Worker( QUEUE_NAME, async (job) => { - const response = await axios.get(job.data.url); - // TODO create a check object and save it to the db - console.log(response.status); + // TODO Extract to service + if (job.data.type === "ping") { + const response = await ping.promise.probe(job.data.url); + // TODO insert checks into DB + if (response.alive === true) { + console.log(`${job.data.url} is alive`); + } else { + console.log(`${job.data.url} is dead`); + } + } else if (job.data.type === "http") { + const response = await axios.get(job.data.url); + // TODO create a check object and save it to the db + if (response.status >= 200 && response.status < 300) { + // TODO insert checks into DB + console.log(`${job.data.url} is alive`); + } else { + console.log(`${job.data.url} is dead`); + } + } }, { connection, @@ -215,12 +235,19 @@ class JobQueue { */ async obliterate() { try { + const jobs = await this.getJobs(); + for (const job of jobs) { + await this.queue.remove(job.id); + } + console.log(jobs); await this.queue.obliterate(); return true; } catch (error) { throw error; } } + + //TODO Cleanup Queue on shutdown } module.exports = JobQueue; diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 2277083e7..4316939b4 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -60,7 +60,8 @@ const monitorValidation = joi.object({ userId: joi.string().required(), name: joi.string().required(), description: joi.string().required(), - url: joi.string().uri().required(), + type: joi.string().required(), + url: joi.string().required(), isActive: joi.boolean(), interval: joi.number(), }); From eb9201b6e2e1586459275aa4f7fb304d631a77c6 Mon Sep 17 00:00:00 2001 From: "Gorkem Cetin (BWL)" <167266851+gorkem-bwl@users.noreply.github.com> Date: Sat, 15 Jun 2024 11:01:15 -0400 Subject: [PATCH 07/12] Update CONTRIBUTING.md --- CONTRIBUTING.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cc69fd34a..fd4a3f373 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,6 +17,7 @@ All types of contributions are encouraged and valued. See the [Table of Contents - [I Want To Contribute](#i-want-to-contribute) - [Reporting Bugs](#reporting-bugs) - [Suggesting Enhancements](#suggesting-enhancements) +- [Design Guidelines][#design-guidelines] - [Styleguides](#Developer-guide-for-issue-lifecycle-process) @@ -54,7 +55,7 @@ A good bug report shouldn't leave others needing to chase you up for more inform - Collect information about the bug: - Stack trace (Traceback) - OS, Platform and Version (Windows, Linux, macOS, x86, ARM) -- Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant. +- Version of the interpreter, compiler, SDK, runtime environment and package manager, depending on what seems relevant. - Possibly your input and the output - Can you reliably reproduce the issue? And can you also reproduce it with older versions? @@ -80,15 +81,11 @@ Once it's filed: This section guides you through submitting an enhancement suggestion for CONTRIBUTING.md, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions. - -#### Before Submitting an Enhancement - - Make sure that you are using the latest version. - Find out if the functionality is already covered, maybe by an individual configuration. - Perform a [search](/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. - Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library. - #### How Do I Submit a Good Enhancement Suggestion? Enhancement suggestions are tracked as [GitHub issues](/issues). @@ -99,6 +96,15 @@ Enhancement suggestions are tracked as [GitHub issues](/issues). - You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. - **Explain why this enhancement would be useful** to most CONTRIBUTING.md users. You may also want to point out the other projects that solved it better and which could serve as inspiration. +### Design guidelines + +We have a Figma file that includes: + +- All the dashboard elements and components +- The design guideline for the app + +You can see it [here](https://www.figma.com/design/RPSfaw66HjzSwzntKcgDUV/Uptime-Genie?node-id=0-1&t=WqOFv9jqNTFGItpL-1). Since it is read-only, we encourage you to copy to your own Figma page, then work on it. + ### Developer Guide for Issue Lifecycle Process [This document](https://docs.google.com/document/d/1Gy3LiimGUNoSiWAMbwyK3SeMADcCMjCLu6cQYoawtSE/edit#heading=h.1lj2lgut6m7h) outlines the process every developer should follow for managing the issues lifecycle. From 3d515cc676622cd8c6225904ec99420ea8464164 Mon Sep 17 00:00:00 2001 From: "Gorkem Cetin (BWL)" <167266851+gorkem-bwl@users.noreply.github.com> Date: Sat, 15 Jun 2024 11:06:28 -0400 Subject: [PATCH 08/12] Better explanation of the project. Add contributing section. --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 40c8ea677..8a8c864e6 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,18 @@ # BlueWave Uptime -BlueWave uptime monitoring application +BlueWave Uptime is an open source server monitoring application. It is a tool used to track the operational status and performance of servers and websites. It regularly checks whether a server/website is accessible and performing optimally, providing real-time alerts and reports on the availability, downtime, and response time of the monitored services. + +## Contributing + +You are welcome to provide contributions to the project. It uses React on the FE, and Nodejs and MongoDB on the BE, hence if you are comfortable with working with those technologies, you are encouraged to send your PRs. Please read [Contributor's guideline](https://github.com/bluewave-labs/bluewave-uptime/blob/master/CONTRIBUTING.md) + +Note that We have a Figma file that includes: + +- All the dashboard elements and components +- The design guideline for the app + +You can see the designs [here](https://www.figma.com/design/RPSfaw66HjzSwzntKcgDUV/Uptime-Genie?node-id=0-1&t=WqOFv9jqNTFGItpL-1). Since it is read-only, we encourage you to copy to your own Figma page, then work on it. ## Getting Started From d39107a5da7eb96733938e090b911dc2fdc6e6d0 Mon Sep 17 00:00:00 2001 From: Daniel Cojocea Date: Sat, 15 Jun 2024 14:27:32 -0400 Subject: [PATCH 09/12] Implemented Loading Button Spinner Component, #142 --- Client/package-lock.json | 42 ++++++- Client/package.json | 1 + Client/src/Components/ButtonSpinner/index.jsx | 105 ++++++++++++++++++ Client/src/Pages/Demo/Demo.jsx | 64 ++++++++++- 4 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 Client/src/Components/ButtonSpinner/index.jsx diff --git a/Client/package-lock.json b/Client/package-lock.json index 73edbf7fb..a4462bddf 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -12,6 +12,7 @@ "@emotion/styled": "^11.11.5", "@fontsource/roboto": "^5.0.13", "@mui/icons-material": "^5.15.17", + "@mui/lab": "^5.0.0-alpha.170", "@mui/material": "^5.15.16", "@mui/x-charts": "^7.5.1", "@mui/x-data-grid": "7.3.2", @@ -1152,6 +1153,46 @@ } } }, + "node_modules/@mui/lab": { + "version": "5.0.0-alpha.170", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.170.tgz", + "integrity": "sha512-0bDVECGmrNjd3+bLdcLiwYZ0O4HP5j5WSQm5DV6iA/Z9kr8O6AnvZ1bv9ImQbbX7Gj3pX4o43EKwCutj3EQxQg==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@mui/base": "5.0.0-beta.40", + "@mui/system": "^5.15.15", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", + "clsx": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material": ">=5.15.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, "node_modules/@mui/material": { "version": "5.15.19", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.19.tgz", @@ -2647,7 +2688,6 @@ "integrity": "sha512-NglN/xprcM+SHD2XCli4oC6bWe6kHoytcyLKCWXmRL854F0qhPhaYgUswUsglnPxYaNQIg2uMY4BvaomIf3kLA==", "dev": true, "license": "ISC" - }, "node_modules/error-ex": { "version": "1.3.2", diff --git a/Client/package.json b/Client/package.json index 505fe25d8..f5df1b5f8 100644 --- a/Client/package.json +++ b/Client/package.json @@ -14,6 +14,7 @@ "@emotion/styled": "^11.11.5", "@fontsource/roboto": "^5.0.13", "@mui/icons-material": "^5.15.17", + "@mui/lab": "^5.0.0-alpha.170", "@mui/material": "^5.15.16", "@mui/x-charts": "^7.5.1", "@mui/x-data-grid": "7.3.2", diff --git a/Client/src/Components/ButtonSpinner/index.jsx b/Client/src/Components/ButtonSpinner/index.jsx new file mode 100644 index 000000000..2cfdaad5d --- /dev/null +++ b/Client/src/Components/ButtonSpinner/index.jsx @@ -0,0 +1,105 @@ +import LoadingButton from "@mui/lab/LoadingButton"; +import PropTypes from "prop-types"; + +const levelConfig = { + primary: { + variant: "contained", + color: "primary", + }, + secondary: { + variant: "outlined", + color: "secondary", + }, + tertiary: { + variant: "text", + color: "tertiary", + }, + error: { + variant: "contained", + color: "error", + }, + imagePrimary: { + color: "primary", + variant: "contained", + }, + imageSecondary: { + color: "secondary", + variant: "outlined", + }, + imageTertiary: { + color: "tertiary", + variant: "text", + }, +}; + +/** + * ButtonSpinner component displays a button with loading loadingText capability. + * @component + * @param {Object} props + * @param {'primary' | 'secondary' | 'tertiary' | 'error' | 'imagePrimary' | 'imageSecondary' | 'imageTertiary'} props.level - The style level of the button. + * @param {string} props.label - The label text displayed on the button. + * @param {React.ReactNode} [props.img] - Icon or image element to display within the button. + * @param {boolean} [props.disabled=false] - Determines if the button is disabled. + * @param {string} [props.loadingText] - Text displayed when the button is in a loading state. + * @param {'start' | 'end'} [props.position] - Specifies where the icon or loading loadingText should be positioned relative to the label. + * @param {function} props.onClick - Callback function invoked when the button is clicked. + * @param {boolean} props.isLoading - Indicates if the button is in a loading state. + * @param {Object} [props.sx] - Additional styles to apply to the button. + * @returns {JSX.Element} + * @example + * // Render a primary button with an icon at the end + * } + * position="end" + * onClick={handleSaveClick} + * isLoading={loading} + * /> + */ + +const ButtonSpinner = ({ + level, + label, + disabled, + loadingText, + position, + img, + onClick, + isLoading, + sx, +}) => { + const { variant, color } = levelConfig[level]; + //if both a loadingPosition and loadingIndicator are provided, the spinner overlaps with the text and it breaks + if (position && loadingText) loadingText = null; + return ( + + {label} + + ); +}; + +ButtonSpinner.propTypes = { + level: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + loadingText: PropTypes.string, + position: PropTypes.oneOf(["start", "end"]), + img: PropTypes.node, + onClick: PropTypes.func.isRequired, + isLoading: PropTypes.bool.isRequired, + sx: PropTypes.object, + disabled: PropTypes.bool, +}; + +export default ButtonSpinner; diff --git a/Client/src/Pages/Demo/Demo.jsx b/Client/src/Pages/Demo/Demo.jsx index f2af76fb4..583f8bda0 100644 --- a/Client/src/Pages/Demo/Demo.jsx +++ b/Client/src/Pages/Demo/Demo.jsx @@ -1,5 +1,6 @@ -import React from "react"; +import React, { useState } from "react"; import Button from "../../Components/Button/"; +import ButtonSpinner from "../../Components/ButtonSpinner"; import Link from "../../Components/Link"; import { useTheme, @@ -13,6 +14,7 @@ import { Tab, Tabs, Typography, + Stack, } from "@mui/material"; import { ColoredLabel, StatusLabel } from "../../Components/Label/"; import Avatar from "../../Components/Avatar/"; @@ -25,6 +27,8 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import AddIcon from "@mui/icons-material/Add"; import Divider from "@mui/material/Divider"; +import UploadIcon from '@mui/icons-material/Upload'; +import SendIcon from '@mui/icons-material/Send'; // Redux import { useSelector } from "react-redux"; @@ -181,6 +185,16 @@ const Demo = () => { // ********************************* const monitorState = useSelector((state) => state.monitors); const { monitors, msg } = monitorState; + + //Button Spinner state + const [isLoading, setIsLoading] = useState(false); + const handleButtonSpinnerClick = () => { + setIsLoading(true); + + setTimeout(() => { + setIsLoading(false); + }, 4000); + }; return (
@@ -352,6 +366,54 @@ const Demo = () => {

Section

+ +

Loading Buttons

+ + + + + } + onClick={handleButtonSpinnerClick} + isLoading={isLoading} + /> + } + onClick={handleButtonSpinnerClick} + isLoading={isLoading} + /> + + +
); }; From 18fe82b04853bb7c147c0adfe2912aff1a3313cd Mon Sep 17 00:00:00 2001 From: M M Date: Sat, 15 Jun 2024 16:39:05 -0700 Subject: [PATCH 10/12] Committing changes to build_images.sh before switching branches --- Server/docker/build_images.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 Server/docker/build_images.sh diff --git a/Server/docker/build_images.sh b/Server/docker/build_images.sh old mode 100755 new mode 100644 From 2de2b359afc0766e654038c148e7855af791383f Mon Sep 17 00:00:00 2001 From: M M Date: Sat, 15 Jun 2024 19:06:51 -0700 Subject: [PATCH 11/12] Refactored Toast Component into a custom React Toastify component. --- Client/src/App.jsx | 2 + Client/src/Components/Toast/index.css | 106 +------------------------- Client/src/Components/Toast/index.jsx | 75 +++++------------- 3 files changed, 26 insertions(+), 157 deletions(-) diff --git a/Client/src/App.jsx b/Client/src/App.jsx index 16774c60a..367a66758 100644 --- a/Client/src/App.jsx +++ b/Client/src/App.jsx @@ -15,6 +15,7 @@ import ForgotPassword from "./Pages/ForgotPassword"; import CheckEmail from "./Pages/CheckEmail"; import SetNewPassword from "./Pages/SetNewPassword"; import NewPasswordConfirmed from "./Pages/NewPasswordConfirmed"; +import ToastComponent from "./Components/Toast"; function App() { return ( @@ -30,6 +31,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/Client/src/Components/Toast/index.css b/Client/src/Components/Toast/index.css index 0653f3d27..c778d22f5 100644 --- a/Client/src/Components/Toast/index.css +++ b/Client/src/Components/Toast/index.css @@ -1,103 +1,3 @@ -.toast-container { - position: fixed; - bottom: 20px; /* Adjust as needed */ - right: 20px; /* Adjust as needed */ - z-index: 1000; /* Ensure it's above other content */ - } - - .toast { - background-color: #ffffff; - box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2); - border-radius: 8px; - padding: 16px; - min-width: 300px; /* Adjust minimum width as needed */ - max-width: 600px; /* Adjust maximum width as needed */ - position: relative; - animation: slideIn 0.3s ease forwards; - overflow: hidden; /* Ensure progress bar stays within bounds */ - display: flex; /* Use flexbox for layout */ - align-items: center; /* Center items vertically */ - } - - @keyframes slideIn { - 0% { - transform: translateY(-100%); - opacity: 0; - } - 100% { - transform: translateY(0%); - opacity: 1; - } - } - - .toast .info-icon-frame { - position: absolute; - top: 15px; /* Adjust top position as needed */ - left: 20px; /* Adjust left position as needed */ - } - - .icon { - position: absolute; - top: 15px; /* Adjust top position as needed */ - left: 20px; /* Adjust left position as needed */ - } - - .toast .toast-content { - flex-grow: 1; /* Allow content to expand */ - } - - .toast .announcement-content { - display: flex; - flex-direction: column; - margin-left: 10px; - } - - .toast .announcement-content-subject { - font-weight: bold; - margin-bottom: 8px; /* Space below subject */ - margin-left: 40px; /* Adjust left margin for more space */ - } - - .toast .announcement-content-body { - color: #555; - white-space: nowrap; /* Prevent text from wrapping */ - overflow: hidden; /* Hide any overflow */ - text-overflow: ellipsis; /* Show ellipsis (...) if the text overflows */ - margin-left: 40px; /* Adjust left margin for more space */ - } - - .toast .announcement-content-body-title { - display: inline-block; - margin-right: 5px; /* Adjust spacing between title and body */ - } - - .toast .announcement-content-controllers { - margin-top: 12px; - } - - .toast .controllers-button-esc, - .toast .controllers-button-primary { - cursor: pointer; - padding: 8px 16px; - margin-left: 23px; /* Adjust left margin for closer proximity */ - } - - - .toast-progress { - position: absolute; - bottom: 0; - left: 0; - height: 4px; - background-color: #29b6f6; /* Adjust color as needed */ - transition: width 0.1s linear; - } - - .close-button { - position: absolute; - top: 1px; - right: 10px; - cursor: pointer; - font-size: 35px; - color: #aaa; - } - \ No newline at end of file +.Toastify__toast-container { + width: auto; +} \ No newline at end of file diff --git a/Client/src/Components/Toast/index.jsx b/Client/src/Components/Toast/index.jsx index 37870a970..f3a06bdb1 100644 --- a/Client/src/Components/Toast/index.jsx +++ b/Client/src/Components/Toast/index.jsx @@ -1,63 +1,30 @@ -import { useState, useEffect } from "react"; import ComplexAlert from "../../Components/Icons/ComplexAlert/ComplexAlert"; -import "./index.css"; // Import your CSS file for toast styles +import AnnouncementsDualButtonWithIcon from "../../Components/Announcements/AnnouncementsDualButtonWithIcon/AnnouncementsDualButtonWithIcon"; +import "react-toastify/dist/ReactToastify.css"; +import { toast, ToastContainer } from "react-toastify"; +import "./index.css"; -const Toast = () => { - const [showToast, setShowToast] = useState(false); - const [progress, setProgress] = useState(100); // Start with full progress - - useEffect(() => { - let timer; - if (showToast) { - timer = setTimeout(() => { - setShowToast(false); - }, 5000); // 5000 milliseconds = 5 seconds - - // Interval to update progress bar - const interval = setInterval(() => { - setProgress((prevProgress) => prevProgress - (100 / 5000 * 100)); // Decrease progress based on time - }, 100); // Update every 100 milliseconds - - return () => { - clearTimeout(timer); - clearInterval(interval); - }; - } - }, [showToast]); - - const toggleToast = () => { - setShowToast(!showToast); - setProgress(100); // Reset progress when showing toast +const ToastComponent = () => { + const displayMsg = () => { + toast( + } + subject="There was a problem with that action" + body="Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquid pariatur, ipsum dolor." + esc="Dismiss" + primary="Learn more" + dismiss={() => toast.dismiss()} + />, + { closeButton: false } + ); }; return ( -
- - {showToast && ( -
- × -
- {} -
-
-
-
- There was a problem with that action -
-
- Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquid pariatur, ipsum dolor. -
-
-
Dismiss
-
View changes
-
-
-
-
-
- )} +
+ +
); }; -export default Toast; +export default ToastComponent; From de04a99bf7d501fd93a17246f13cfbffa65a448a Mon Sep 17 00:00:00 2001 From: M M Date: Sun, 16 Jun 2024 13:49:25 -0700 Subject: [PATCH 12/12] Modified the AnnouncementsDualButtonWithIcon to handle closing the toast. --- .../AnnouncementsDualButtonWithIcon.jsx | 12 +++++++++--- Client/src/Components/Toast/index.jsx | 18 ++++++++++-------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/Client/src/Components/Announcements/AnnouncementsDualButtonWithIcon/AnnouncementsDualButtonWithIcon.jsx b/Client/src/Components/Announcements/AnnouncementsDualButtonWithIcon/AnnouncementsDualButtonWithIcon.jsx index 574c255a0..bd2675f4c 100644 --- a/Client/src/Components/Announcements/AnnouncementsDualButtonWithIcon/AnnouncementsDualButtonWithIcon.jsx +++ b/Client/src/Components/Announcements/AnnouncementsDualButtonWithIcon/AnnouncementsDualButtonWithIcon.jsx @@ -1,5 +1,4 @@ import "./announcementsDualButtonWithIcon.css"; -import React from "react"; import CloseIcon from "@mui/icons-material/Close"; import PropTypes from "prop-types"; import { useTheme } from "@mui/material"; @@ -12,6 +11,7 @@ import { useTheme } from "@mui/material"; * @param {string} props.body - The announcement body text content * @param {string} props.esc - The text for the escape button (usually "Close") * @param {string} props.primary - The text for the primary button + * @param {function} props.closeToast - Function to close the toast notification * @returns {JSX.Element} - Renders the announcement dual button with icon component */ @@ -21,6 +21,7 @@ const AnnouncementsDualButtonWithIcon = ({ body, esc, primary, + closeToast, }) => { const theme = useTheme(); @@ -40,14 +41,18 @@ const AnnouncementsDualButtonWithIcon = ({ {body &&
{body}
} {(esc || primary) && (
- {esc &&
{esc}
} + {esc && ( +
+ {esc} +
+ )} {primary && (
{primary}
)}
)}
-
+
@@ -60,6 +65,7 @@ AnnouncementsDualButtonWithIcon.propTypes = { body: PropTypes.string, esc: PropTypes.string, primary: PropTypes.string, + closeToast: PropTypes.func.isRequired, }; export default AnnouncementsDualButtonWithIcon; diff --git a/Client/src/Components/Toast/index.jsx b/Client/src/Components/Toast/index.jsx index f3a06bdb1..ceb7a6f60 100644 --- a/Client/src/Components/Toast/index.jsx +++ b/Client/src/Components/Toast/index.jsx @@ -7,14 +7,16 @@ import "./index.css"; const ToastComponent = () => { const displayMsg = () => { toast( - } - subject="There was a problem with that action" - body="Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquid pariatur, ipsum dolor." - esc="Dismiss" - primary="Learn more" - dismiss={() => toast.dismiss()} - />, + ({ closeToast, toastProps }) => ( + } + subject="There was a problem with that action" + body="Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquid pariatur, ipsum dolor." + esc="Dismiss" + primary="Learn more" + closeToast={closeToast} + /> + ), { closeButton: false } ); };