diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 000000000..c884b2e12 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,9 @@ +## Describe your changes + +## Issue Number if applicable + +## Checklist before requesting a review + +- [ ] I have performed a self-review of my code +- [ ] I have inlcuded the issue # in the PR if applicable +- [ ] I have labelled the PR correctly diff --git a/Client/src/Components/Breadcrumbs/index.css b/Client/src/Components/Breadcrumbs/index.css new file mode 100644 index 000000000..b7d190de1 --- /dev/null +++ b/Client/src/Components/Breadcrumbs/index.css @@ -0,0 +1,31 @@ +.MuiBreadcrumbs-root { + height: 36px; +} +.MuiBreadcrumbs-root svg { + width: 16px; + height: 16px; +} +.MuiBreadcrumbs-root .MuiBreadcrumbs-li a { + font-size: var(--env-var-font-size-medium); + color: var(--env-var-color-2); + opacity: 0.8; + font-weight: 400; +} +.MuiBreadcrumbs-root .MuiBreadcrumbs-li:hover a { + color: var(--env-var-color-2); +} +.MuiBreadcrumbs-root .MuiBreadcrumbs-li:not(:last-child):hover a { + opacity: 1; + background-color: #e3e3e3; +} +.MuiBreadcrumbs-root .MuiBreadcrumbs-li:not(:last-child) { + cursor: pointer; +} +.MuiBreadcrumbs-root .MuiBreadcrumbs-li:last-child a { + font-weight: 500; + opacity: 1; + cursor: default; +} +.MuiBreadcrumbs-root .MuiBreadcrumbs-separator { + margin: 0; +} diff --git a/Client/src/Components/Breadcrumbs/index.jsx b/Client/src/Components/Breadcrumbs/index.jsx new file mode 100644 index 000000000..506263fce --- /dev/null +++ b/Client/src/Components/Breadcrumbs/index.jsx @@ -0,0 +1,63 @@ +import PropTypes from "prop-types"; +import { Box, Breadcrumbs as MUIBreadcrumbs } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import { useNavigate } from "react-router"; +import ArrowRight from "../../assets/icons/right-arrow.svg?react"; + +import "./index.css"; + +/** + * Breadcrumbs component that displays a list of breadcrumb items. + * + * @param {Object} props + * @param {Array} props.list - Array of breadcrumb items. Each item should have `name` and `path` properties. + * @param {string} props.list.name - The name to display for the breadcrumb. + * @param {string} props.list.path - The path to navigate to when the breadcrumb is clicked. + * + * @returns {JSX.Element} The rendered Breadcrumbs component. + */ + +const Breadcrumbs = ({ list }) => { + const theme = useTheme(); + const navigate = useNavigate(); + + return ( + } + aria-label="breadcrumb" + p={theme.gap.small} + width="fit-content" + backgroundColor={theme.palette.otherColors.fillGray} + borderRadius={`${theme.shape.borderRadius}px`} + lineHeight="18px" + > + {list.map((item, index) => { + return ( + navigate(item.path)} + > + {item.name} + + ); + })} + + ); +}; + +Breadcrumbs.propTypes = { + list: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string.isRequired, + path: PropTypes.string.isRequired, + }).isRequired + ).isRequired, +}; + +export default Breadcrumbs; diff --git a/Client/src/Components/Sidebar/index.css b/Client/src/Components/Sidebar/index.css index 36415438c..59924b1a1 100644 --- a/Client/src/Components/Sidebar/index.css +++ b/Client/src/Components/Sidebar/index.css @@ -1,4 +1,5 @@ aside .selected-path, +.sidebar-popup .selected-path, aside .MuiListItemButton-root:hover { background-color: var(--env-var-color-13); } @@ -52,7 +53,7 @@ aside > .MuiListItemButton-root::before { content: ""; position: absolute; - left: -8px; + left: -7px; top: 0; height: 100%; width: 100%; @@ -65,7 +66,8 @@ aside > .MuiListItemButton-root:last-child::before { height: 50%; } -aside .MuiCollapse-wrapperInner .MuiList-root svg { +aside .MuiCollapse-wrapperInner .MuiList-root svg, +aside .MuiList-root .MuiListItemText-root + svg { width: 18px; height: 18px; } @@ -73,11 +75,50 @@ aside .MuiCollapse-wrapperInner .MuiList-root svg { aside .MuiCollapse-wrapperInner .MuiList-root > .MuiListItemButton-root::after { content: ""; position: absolute; - left: -8px; + left: -7px; top: 50%; height: 1px; - width: 7px; + width: 6px; border-radius: 50%; background-color: var(--env-var-color-5); opacity: 0.3; } + +aside { + transition: flex 300ms ease-out; +} + +.sidebar-popup .MuiPaper-root { + box-shadow: var(--env-var-shadow-1); + border: solid 1px var(--env-var-color-6); + border-radius: var(--env-var-radius-1); + gap: 1px; +} +.sidebar-popup .MuiList-root { + padding: 6px 6px; + min-width: 100px; +} +.sidebar-popup li.MuiButtonBase-root { + font-size: var(--env-var-font-size-medium); + color: var(--env-var-color-5); + padding: 5px 8px; + border-radius: var(--env-var-radius-1); +} +.sidebar-popup li.MuiButtonBase-root:hover{ + background-color: var(--env-var-color-13); +} +.sidebar-popup li.MuiButtonBase-root:has(.MuiBox-root):hover{ + background-color: white; +} +.sidebar-popup li.MuiButtonBase-root:has(.MuiBox-root){ + padding-bottom: 0; +} +.sidebar-popup .MuiModal-root p.MuiTypography-root { + font-size: var(--env-var-font-size-medium); + color: var(--env-var-color-2); +} +.sidebar-popup svg{ + width: 16px; + height: 16px; + opacity: 0.9; +} diff --git a/Client/src/Components/Sidebar/index.jsx b/Client/src/Components/Sidebar/index.jsx index 159b01503..38baa33ec 100644 --- a/Client/src/Components/Sidebar/index.jsx +++ b/Client/src/Components/Sidebar/index.jsx @@ -9,6 +9,8 @@ import { ListItemIcon, ListItemText, ListSubheader, + Menu, + MenuItem, Stack, Tooltip, Typography, @@ -24,6 +26,7 @@ import UserSvg from "../../assets/icons/user.svg?react"; import TeamSvg from "../../assets/icons/user-two.svg?react"; import LogoutSvg from "../../assets/icons/logout.svg?react"; import BWULogo from "../../assets/Images/bwl-logo.svg?react"; +import BWULogoAbbreviated from "../../assets/icons/bwu-abbreviated.svg?react"; import Support from "../../assets/icons/support.svg?react"; import Dashboard from "../../assets/icons/dashboard.svg?react"; import Account from "../../assets/icons/user-edit.svg?react"; @@ -36,6 +39,8 @@ import PageSpeed from "../../assets/icons/page-speed.svg?react"; import Settings from "../../assets/icons/settings.svg?react"; import ArrowDown from "../../assets/icons/down-arrow.svg?react"; import ArrowUp from "../../assets/icons/up-arrow.svg?react"; +import ArrowRight from "../../assets/icons/right-arrow.svg?react"; +import ArrowLeft from "../../assets/icons/left-arrow.svg?react"; import "./index.css"; @@ -82,6 +87,17 @@ function Sidebar() { const dispatch = useDispatch(); const authState = useSelector((state) => state.auth); const [open, setOpen] = useState({ Dashboard: false, Account: false }); + const [collapsed, setCollapsed] = useState(false); + const [anchorEl, setAnchorEl] = useState(null); + const [popup, setPopup] = useState(); + + const openPopup = (event, id) => { + setAnchorEl(event.currentTarget); + setPopup(id); + }; + const closePopup = () => { + setAnchorEl(null); + }; /** * Handles logging out the user @@ -105,10 +121,47 @@ function Sidebar() { }, []); return ( - - - - + + + {collapsed ? ( + + ) : ( + + )} + setCollapsed(!collapsed)} + > + {collapsed ? : } + + {/* menu */} Menu } + sx={{ px: collapsed ? theme.gap.xs : theme.gap.ml }} > {menu.map((item) => item.path ? ( - navigate(`/${item.path}`)} - sx={{ - gap: theme.gap.medium, - borderRadius: `${theme.shape.borderRadius}px`, + placement="right" + title={collapsed ? item.name : ""} + slotProps={{ + popper: { + modifiers: [ + { + name: "offset", + options: { + offset: [0, -16], + }, + }, + ], + }, }} > - {item.icon} - {item.name} - + navigate(`/${item.path}`)} + sx={{ + gap: theme.gap.medium, + borderRadius: `${theme.shape.borderRadius}px`, + justifyContent: collapsed ? "center" : "flex-start", + }} + > + {item.icon} + {!collapsed && {item.name}} + + + ) : collapsed ? ( + + + openPopup(event, item.name)} + sx={{ + position: "relative", + gap: theme.gap.medium, + borderRadius: `${theme.shape.borderRadius}px`, + justifyContent: collapsed ? "center" : "flex-start", + }} + > + {item.icon} + {!collapsed && {item.name}} + + + + {item.nested.map((child) => { + if ( + child.name === "Team" && + authState.user?.role && + !authState.user.role.includes("admin") + ) { + return null; + } + + return ( + { + navigate(`/${child.path}`); + closePopup(); + }} + sx={{ + gap: theme.gap.small, + borderRadius: `${theme.shape.borderRadius}px`, + pl: theme.gap.small, + }} + > + {child.icon} + {child.name} + + ); + })} + + ) : ( {item.icon} @@ -210,63 +368,154 @@ function Sidebar() { Other } + sx={{ px: collapsed ? theme.gap.xs : theme.gap.ml }} > {other.map((item) => ( - - item.path === "support" - ? window.open( - "https://github.com/bluewave-labs/bluewave-uptime", - "_blank", - "noreferrer" - ) - : navigate(`/${item.path}`) - } - sx={{ - gap: theme.gap.medium, - borderRadius: `${theme.shape.borderRadius}px`, + placement="right" + title={collapsed ? item.name : ""} + slotProps={{ + popper: { + modifiers: [ + { + name: "offset", + options: { + offset: [0, -16], + }, + }, + ], + }, }} > - {item.icon} - {item.name} - + + item.path === "support" + ? window.open( + "https://github.com/bluewave-labs/bluewave-uptime", + "_blank", + "noreferrer" + ) + : navigate(`/${item.path}`) + } + sx={{ + gap: theme.gap.medium, + borderRadius: `${theme.shape.borderRadius}px`, + justifyContent: collapsed ? "center" : "flex-start", + }} + > + {item.icon} + {!collapsed && {item.name}} + + ))} + - - - - {authState.user?.firstName} {authState.user?.lastName} - - - {authState.user?.role} - - - - - - - + {collapsed ? ( + <> + + openPopup(event, "logout")} + sx={{ p: 0, "&:focus": { outline: "none" } }} + > + + + + + + + + {authState.user?.firstName} {authState.user?.lastName} + + + {authState.user?.role} + + + + + + + Log out + + + + ) : ( + <> + + + + {authState.user?.firstName} {authState.user?.lastName} + + + {authState.user?.role} + + + + + + + + + )} ); diff --git a/Client/src/Layouts/HomeLayout/index.css b/Client/src/Layouts/HomeLayout/index.css index f143b3fd9..dcdcf0535 100644 --- a/Client/src/Layouts/HomeLayout/index.css +++ b/Client/src/Layouts/HomeLayout/index.css @@ -19,7 +19,6 @@ height: calc(100vh - var(--env-var-spacing-2) * 2); max-width: var(--env-var-side-bar-width); - flex: 1; border: 1px solid var(--color-border-0); border-radius: var(--env-var-radius-1); @@ -27,10 +26,6 @@ padding: var(--env-var-spacing-1) 0; } -.home-layout aside > *:not(:first-child) { - padding-left: var(--env-var-spacing-1-plus); - padding-right: var(--env-var-spacing-1-plus); -} .home-layout > div { min-height: calc(100vh - var(--env-var-spacing-2) * 2); diff --git a/Client/src/Pages/Auth/Login.jsx b/Client/src/Pages/Auth/Login.jsx index 12dc52bef..2776866dc 100644 --- a/Client/src/Pages/Auth/Login.jsx +++ b/Client/src/Pages/Auth/Login.jsx @@ -4,7 +4,7 @@ import { Box, Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import { credentials } from "../../Validation/validation"; import { login } from "../../Features/Auth/authSlice"; -import { useDispatch } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { createToast } from "../../Utils/toastUtils"; import Button from "../../Components/Button"; import { networkService } from "../../main"; @@ -13,6 +13,7 @@ import background from "../../assets/Images/background_pattern_decorative.png"; import Logo from "../../assets/icons/bwu-icon.svg?react"; import Mail from "../../assets/icons/mail.svg?react"; import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; +import PropTypes from "prop-types"; import "./index.css"; @@ -63,6 +64,10 @@ const LandingPage = ({ onContinue }) => { ); }; +LandingPage.propTypes = { + onContinue: PropTypes.func.isRequired, +}; + /** * Renders the first step of the login process. * @@ -136,6 +141,14 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => { ); }; +StepOne.propTypes = { + form: PropTypes.object.isRequired, + errors: PropTypes.object.isRequired, + onSubmit: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + onBack: PropTypes.func.isRequired, +}; + /** * Renders the second step of the login process, including a password input field. * @@ -230,11 +243,22 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => { ); }; +StepTwo.propTypes = { + form: PropTypes.object.isRequired, + errors: PropTypes.object.isRequired, + onSubmit: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + onBack: PropTypes.func.isRequired, +}; + const Login = () => { const dispatch = useDispatch(); const navigate = useNavigate(); const theme = useTheme(); + const authState = useSelector((state) => state.auth); + const { authToken } = authState; + const idMap = { "login-email-input": "email", "login-password-input": "password", @@ -248,6 +272,10 @@ const Login = () => { const [step, setStep] = useState(0); useEffect(() => { + if (authToken) { + navigate("/monitors"); + return; + } networkService .doesAdminExist() .then((response) => { @@ -258,7 +286,7 @@ const Login = () => { .catch((error) => { console.log(error); }); - }, [navigate]); + }, [authToken, navigate]); const handleChange = (event) => { const { value, id } = event.target; diff --git a/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx index 187d9c63d..8fff6e4a1 100644 --- a/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx +++ b/Client/src/Pages/Maintenance/CreateMaintenanceWindow/index.jsx @@ -2,7 +2,7 @@ import { Box, Stack, Typography } from "@mui/material"; import "./index.css"; import React, { useState } from "react"; import Button from "../../../Components/Button"; -import Back from "../../../assets/icons/left-arrow.svg?react"; +import Back from "../../../assets/icons/left-arrow-long.svg?react"; import Select from "../../../Components/Inputs/Select"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; diff --git a/Client/src/Pages/Monitors/Configure/index.css b/Client/src/Pages/Monitors/Configure/index.css index ac9cd8a0d..ec140887c 100644 --- a/Client/src/Pages/Monitors/Configure/index.css +++ b/Client/src/Pages/Monitors/Configure/index.css @@ -23,8 +23,7 @@ opacity: 0.6; } .configure-monitor h6.MuiTypography-root, -.configure-monitor p.MuiTypography-root:has(span), -.configure-monitor p.MuiTypography-root span.MuiTypography-root { +.configure-monitor h1.MuiTypography-root + span.MuiTypography-root { font-size: var(--env-var-font-size-small-plus); } .configure-monitor button.MuiButtonBase-root { @@ -33,19 +32,20 @@ .configure-monitor button.MuiButtonBase-root { height: var(--env-var-height-2); } -.configure-monitor button.MuiButtonBase-root:has(svg) { - line-height: 1; -} .configure-monitor .config-box { - padding: var(--env-var-spacing-4) 50px; - padding-bottom: 60px; border: 1px solid var(--env-var-color-16); border-radius: var(--env-var-radius-1); background-color: var(--env-var-color-8); } -.configure-monitor .config-box .MuiBox-root, -.configure-monitor .config-box .MuiStack-root { +.configure-monitor .config-box > .MuiBox-root, +.configure-monitor .config-box > .MuiStack-root { flex: 1; + padding: 24px 30px; + padding-bottom: 40px; +} +.configure-monitor .config-box > *:first-child { + flex: 0.7; + border-right: solid 1px var(--env-var-color-16); } .configure-monitor .MuiStack-root:has(span.MuiTypography-root.input-error) { position: relative; @@ -72,12 +72,6 @@ border-color: var(--env-var-color-29); } -.configure-monitor-form { - display: flex; - flex-direction: column; - gap: var(--env-var-spacing-2); -} - body:has(.configure-monitor) .select-dropdown .MuiMenuItem-root, .configure-monitor .select-wrapper .select-component > .MuiSelect-select { text-transform: lowercase; diff --git a/Client/src/Pages/Monitors/Configure/index.jsx b/Client/src/Pages/Monitors/Configure/index.jsx index 08c8cbe39..c873482a9 100644 --- a/Client/src/Pages/Monitors/Configure/index.jsx +++ b/Client/src/Pages/Monitors/Configure/index.jsx @@ -5,15 +5,11 @@ import { useEffect, useState } from "react"; import Button from "../../../Components/Button"; import Field from "../../../Components/Inputs/Field"; import { Box, Modal, Skeleton, Stack, Typography } from "@mui/material"; -import WestRoundedIcon from "@mui/icons-material/WestRounded"; import GreenCheck from "../../../assets/icons/checkbox-green.svg?react"; import RedCheck from "../../../assets/icons/checkbox-red.svg?react"; import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline"; -import { getLastChecked } from "../../../Utils/monitorUtils"; -import "./index.css"; import { monitorValidation } from "../../../Validation/validation"; import Select from "../../../Components/Inputs/Select"; -import { formatDurationRounded } from "../../../Utils/timeUtils"; import { createToast } from "../../../Utils/toastUtils"; import { updateUptimeMonitor, @@ -21,6 +17,8 @@ import { deleteUptimeMonitor, } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; import Checkbox from "../../../Components/Inputs/Checkbox"; +import Breadcrumbs from "../../../Components/Breadcrumbs"; +import "./index.css"; /** * Parses a URL string and returns a URL object. @@ -96,7 +94,6 @@ const Configure = () => { const { user, authToken } = useSelector((state) => state.auth); const { monitors } = useSelector((state) => state.uptimeMonitors); const [monitor, setMonitor] = useState({}); - const [duration, setDuration] = useState(0); const [errors, setErrors] = useState({}); const { monitorId } = useParams(); @@ -117,7 +114,6 @@ const Configure = () => { setMonitor({ ...data, }); - setDuration(formatDurationRounded(data?.interval)); }, [monitorId, authToken, monitors]); const handleChange = (event, name) => { @@ -210,74 +206,54 @@ const Configure = () => { const parsedUrl = parseUrl(monitor?.url); const protocol = parsedUrl?.protocol?.replace(":", "") || ""; - var lastChecked = formatDurationRounded(getLastChecked(monitor?.checks)); - let loading = Object.keys(monitor).length === 0; return ( - + {loading ? ( ) : ( <> -