diff --git a/Client/src/App.jsx b/Client/src/App.jsx index 783f28330..8fb4bf932 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 ( + }> } /> } /> { "&::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/Charts/MonitorDetailsAreaChart/index.jsx b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx index cb01734fd..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(); @@ -97,7 +97,7 @@ const MonitorDetailsAreaChart = ({ checks }) => { }); }; - const memoizedChecks = useMemo(() => checks, []); + const memoizedChecks = useMemo(() => checks, [checks[0]]); const theme = useTheme(); @@ -143,12 +143,17 @@ const MonitorDetailsAreaChart = ({ checks }) => { tickLine={false} height={18} /> - } /> + } + /> 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/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/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/Sidebar/index.jsx b/Client/src/Components/Sidebar/index.jsx index c372f8c66..145b6f02e 100644 --- a/Client/src/Components/Sidebar/index.jsx +++ b/Client/src/Components/Sidebar/index.jsx @@ -90,6 +90,14 @@ 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); + + // Remove demo password if demo + if (user.role?.includes("demo")) { + menu[3].nested = menu[3].nested.filter((item) => { + return item.name !== "Password"; + }); + } const openPopup = (event, id) => { setAnchorEl(event.currentTarget); @@ -298,6 +306,7 @@ function Sidebar() { anchorEl={anchorEl} open={Boolean(anchorEl) && popup === item.name} onClose={closePopup} + disableScrollLock anchorOrigin={{ vertical: "top", horizontal: "right", @@ -583,6 +592,7 @@ function Sidebar() { anchorEl={anchorEl} open={Boolean(anchorEl) && popup === "logout"} onClose={closePopup} + disableScrollLock anchorOrigin={{ vertical: "top", horizontal: "right", @@ -644,6 +654,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/Components/TabPanels/Account/ProfilePanel.jsx b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx index 885c795b1..36441c53c 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("demo") && ( + + + 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 */} { 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..07d209fc1 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"; @@ -24,6 +31,7 @@ const TeamPanel = () => { superadmin: "Super admin", admin: "Admin", user: "Team member", + demo: "Demo User", }; const theme = useTheme(); @@ -88,7 +96,7 @@ const TeamPanel = () => { id: idx, data: ( - + {member.firstName + " " + member.lastName} @@ -322,7 +330,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. + + { - const theme = useTheme(); - return ( - - - - - - + + + + ); }; diff --git a/Client/src/Pages/Account/index.jsx b/Client/src/Pages/Account/index.jsx index fe7b19655..5543b38ee 100644 --- a/Client/src/Pages/Account/index.jsx +++ b/Client/src/Pages/Account/index.jsx @@ -22,15 +22,20 @@ const Account = ({ open = "profile" }) => { const handleTabChange = (event, newTab) => { navigate(`/account/${newTab}`); }; - - let tabList = ["Profile", "Password", "Team"]; const { user } = useSelector((state) => state.auth); + const requiredRoles = ["superadmin", "admin"]; + let tabList = ["Profile", "Password", "Team"]; const hideTeams = !requiredRoles.some((role) => user.role.includes(role)); if (hideTeams) { tabList = ["Profile", "Password"]; } + // Remove password for demo + if (user.role.includes("demo")) { + tabList = ["Profile"]; + } + return ( { - + {user.role.includes("superadmin") && } {!hideTeams && } 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?{" "} { "& h1": { color: theme.palette.primary.main, fontWeight: 600, - fontSize: 24, + fontSize: 21, }, "& p": { fontSize: 14, @@ -104,8 +105,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/Login.jsx b/Client/src/Pages/Auth/Login.jsx index c8946b7fb..b4566da2f 100644 --- a/Client/src/Pages/Auth/Login.jsx +++ b/Client/src/Pages/Auth/Login.jsx @@ -8,13 +8,14 @@ import { useDispatch, useSelector } from "react-redux"; import { createToast } from "../../Utils/toastUtils"; import { networkService } from "../../main"; import Field from "../../Components/Inputs/Field"; -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"; 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. @@ -278,9 +279,9 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => { textAlign="center" sx={{ position: "absolute", - top: "103%", + top: "104%", left: "50%", - transform: "translateX(-50%)" + transform: "translateX(-50%)", }} > { }; const [form, setForm] = useState({ - email: "", - password: "", + email: DEMO !== undefined ? "uptimedemo@demo.com" : "", + password: DEMO !== undefined ? "Demouser1!" : "", }); const [errors, setErrors] = useState({}); const [step, setStep] = useState(0); @@ -438,18 +439,22 @@ const Login = () => { "& 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 new file mode 100644 index 000000000..a6e4a3343 --- /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: 8, + "& 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/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/CreateMonitor/index.jsx b/Client/src/Pages/Monitors/CreateMonitor/index.jsx index 5147236e2..36a63f0e6 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, Button, 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"; @@ -13,11 +13,13 @@ 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 = () => { 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(); @@ -30,6 +32,7 @@ const CreateMonitor = () => { "notify-email-default": "notification-email", }; + const { monitorId } = useParams(); const [monitor, setMonitor] = useState({ url: "", name: "", @@ -40,6 +43,36 @@ const CreateMonitor = () => { const [https, setHttps] = useState(true); const [errors, setErrors] = useState({}); + useEffect(() => { + 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.", + }); + } + } + }; + fetchMonitor(); + }, [monitorId, authToken, monitors]); + const handleChange = (event, name) => { const { value, id } = event.target; if (!name) name = idMap[id]; diff --git a/Client/src/Pages/Monitors/Details/index.jsx b/Client/src/Pages/Monitors/Details/index.jsx index e31de38d7..41e529eb9 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"; @@ -35,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); @@ -90,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); } @@ -121,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 ? ( @@ -158,11 +148,12 @@ const DetailsPage = ({ isAdmin }) => { { }} > - + {monitor.url?.replace(/^https?:\/\//, "") || "..."} { backgroundColor: theme.palette.text.tertiary, opacity: 0.8, left: -9, - top: "42%", + top: "50%", + transform: "translateY(-50%)", }, }} > @@ -289,27 +280,7 @@ const DetailsPage = ({ isAdmin }) => { - + 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..f055a57a1 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 ( { select: { MenuProps: { keepMounted: true, + disableScrollLock: true, PaperProps: { className: "pagination-dropdown", sx: { diff --git a/Client/src/Pages/Monitors/Home/actionsMenu.jsx b/Client/src/Pages/Monitors/Home/actionsMenu.jsx index efb29402d..7f2eb0d97 100644 --- a/Client/src/Pages/Monitors/Home/actionsMenu.jsx +++ b/Client/src/Pages/Monitors/Home/actionsMenu.jsx @@ -137,6 +137,14 @@ const ActionsMenu = ({ monitor, isAdmin, updateCallback }) => { Configure )} + { + e.stopPropagation(); + navigate(`/monitors/create/${actions.id}`); + }} + > + Clone + {isAdmin && ( { 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 */} diff --git a/Client/src/Pages/Monitors/Home/monitorData.jsx b/Client/src/Pages/Monitors/Home/monitorData.jsx deleted file mode 100644 index afe943f4e..000000000 --- a/Client/src/Pages/Monitors/Home/monitorData.jsx +++ /dev/null @@ -1,110 +0,0 @@ -import Host from "./host"; -import ActionsMenu from "./actionsMenu"; -import BarChart from "../../../Components/Charts/BarChart"; -import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded"; -import { StatusLabel } from "../../../Components/Label"; -import { Box } from "@mui/material"; -import { useTheme } from "@emotion/react"; - -const data = { - cols: [ - { id: 1, name: "Host" }, - { - id: 2, - name: ( - - 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/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, + }, })); 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; diff --git a/Client/src/Utils/Theme/darkTheme.js b/Client/src/Utils/Theme/darkTheme.js index e54d9eaf4..261f8135f 100644 --- a/Client/src/Utils/Theme/darkTheme.js +++ b/Client/src/Utils/Theme/darkTheme.js @@ -18,7 +18,7 @@ const border = { light: "#27272a", dark: "#36363e" }; const fontFamilyDefault = '"Inter","system-ui", "Avenir", "Helvetica", "Arial", sans-serif'; const shadow = - "0px 4px 24px -4px rgba(16, 24, 40, 0.08), 0px 3px 3px -3px rgba(16, 24, 40, 0.03)"; + "0px 4px 24px -4px rgba(255, 255, 255, 0.03), 0px 3px 3px -3px rgba(255, 255, 255, 0.01)"; const darkTheme = createTheme({ typography: { fontFamily: fontFamilyDefault, fontSize: 13 }, @@ -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,16 @@ 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", + paddingLeft: "calc(100vw - 100%)", + }, + }, + }, MuiButton: { defaultProps: { disableRipple: true, @@ -127,7 +137,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 60a8e0b2d..f00ece091 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,16 @@ 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)", + lineHeight: "inherit", + paddingLeft: "calc(100vw - 100%)", + }, + }, + }, MuiButton: { defaultProps: { disableRipple: true, 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 34f6ebce6..000000000 Binary files a/Client/src/assets/Images/background_pattern_decorative.png and /dev/null differ 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/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 @@ - - - - - - - - - - - - - - - - - + + 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 @@ - + 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 @@ + + + + diff --git a/Client/src/index.css b/Client/src/index.css index 1f3b9bee2..4383427a7 100644 --- a/Client/src/index.css +++ b/Client/src/index.css @@ -56,12 +56,6 @@ html { 0px 3px 3px -3px rgba(16, 24, 40, 0.03); } -body { - margin: 0; - min-width: 320px; - height: 100%; -} - .MuiInputBase-root.Mui-disabled input { cursor: not-allowed; } 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..969a320c2 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"] diff --git a/README.md b/README.md index e050b221e..92ee37e35 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 @@ -172,13 +170,21 @@ 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" 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" @@ -189,14 +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" ``` -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.
@@ -214,10 +220,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` \| |
diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index 0cf297a47..fe406a6b6 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -463,13 +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 monitors = await req.db.getMonitorsByUserId({ - params: { userId: _id }, + const result = await req.db.getMonitorsByTeamId({ + params: { teamId: user.teamId }, }); - - if (monitors) { + if (user.role.includes("superadmin") && 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); } }; 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/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); } } 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(),