diff --git a/Client/src/App.css b/Client/src/App.css index 0d669ffa5..bf3d4ea92 100644 --- a/Client/src/App.css +++ b/Client/src/App.css @@ -1,31 +1,3 @@ -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - .card { padding: 2em; } diff --git a/Client/src/App.jsx b/Client/src/App.jsx index 12db9dab9..783f28330 100644 --- a/Client/src/App.jsx +++ b/Client/src/App.jsx @@ -7,7 +7,7 @@ import Login from "./Pages/Auth/Login"; import Register from "./Pages/Auth/Register/Register"; import HomeLayout from "./Layouts/HomeLayout"; import Account from "./Pages/Account"; -import Monitors from "./Pages/Monitors"; +import Monitors from "./Pages/Monitors/Home"; import CreateMonitor from "./Pages/Monitors/CreateMonitor"; import Incidents from "./Pages/Incidents"; import Status from "./Pages/Status"; @@ -21,27 +21,40 @@ import ProtectedRoute from "./Components/ProtectedRoute"; import Details from "./Pages/Monitors/Details"; import Maintenance from "./Pages/Maintenance"; import withAdminCheck from "./HOC/withAdminCheck"; +import withAdminProp from "./HOC/withAdminProp"; import Configure from "./Pages/Monitors/Configure"; import PageSpeed from "./Pages/PageSpeed"; import CreatePageSpeed from "./Pages/PageSpeed/CreatePageSpeed"; import CreateNewMaintenanceWindow from "./Pages/Maintenance/CreateMaintenanceWindow"; import PageSpeedDetails from "./Pages/PageSpeed/Details"; import PageSpeedConfigure from "./Pages/PageSpeed/Configure"; +import { ThemeProvider } from "@emotion/react"; +import lightTheme from "./Utils/Theme/lightTheme"; +import darkTheme from "./Utils/Theme/darkTheme"; +import { useSelector } from "react-redux"; function App() { const AdminCheckedRegister = withAdminCheck(Register); + const MonitorsWithAdminProp = withAdminProp(Monitors); + const DetailsWithAdminProp = withAdminProp(Details); + const PageSpeedWithAdminProp = withAdminProp(PageSpeed); + const MaintenanceWithAdminProp = withAdminProp(Maintenance); + const SettingsWithAdminProp = withAdminProp(Settings); + + const mode = useSelector((state) => state.ui.mode); + return ( - <> + }> } + element={} /> } + element={} /> } + element={} /> } + element={} /> } + element={} /> } + element={} /> - > + ); } diff --git a/Client/src/Components/Alert/index.jsx b/Client/src/Components/Alert/index.jsx index 61c3b6bc3..eb5fbd13b 100644 --- a/Client/src/Components/Alert/index.jsx +++ b/Client/src/Components/Alert/index.jsx @@ -1,12 +1,11 @@ import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; -import { Box, IconButton, Stack, Typography } from "@mui/material"; +import { Box, Button, IconButton, Stack, Typography } from "@mui/material"; import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined"; import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined"; import CloseIcon from "@mui/icons-material/Close"; import "./index.css"; -import Button from "../Button"; /** * Icons mapping for different alert variants. @@ -32,7 +31,7 @@ const icons = { const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => { const theme = useTheme(); - const { bg, border, color } = theme.alert[variant]; + const { text, light, border } = theme.palette[variant]; const icon = icons[variant]; return ( @@ -41,46 +40,43 @@ const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => { justifyContent="flex-start" alignItems={hasIcon ? "" : "center"} className="alert row-stack" - gap={theme.gap.ml} + gap={theme.spacing(8)} sx={{ - padding: hasIcon ? theme.gap.ml : `${theme.gap.small} ${theme.gap.ml}`, - backgroundColor: bg, + padding: hasIcon + ? theme.spacing(8) + : `${theme.spacing(4)} ${theme.spacing(8)}`, + backgroundColor: light, border: `solid 1px ${border}`, - borderRadius: `${theme.shape.borderRadius}px`, + borderRadius: theme.shape.borderRadius, }} > - {hasIcon && {icon}} + {hasIcon && {icon}} {title && ( - + {title} )} {body && ( - + {body} )} {hasIcon && isToast && ( + > + Dismiss + )} {isToast && ( @@ -95,10 +91,6 @@ const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => { "&:focus": { outline: "none", }, - "& .MuiTouchRipple-root": { - pointerEvents: "none", - display: "none", - }, }} > { } sx={{ fontSize: small ? "16px" : "22px", + color: "white", fontWeight: 400, backgroundColor: stringToColor(`${user?.firstName} ${user?.lastName}`), display: "inline-flex", diff --git a/Client/src/Components/BasicTable/index.css b/Client/src/Components/BasicTable/index.css index 208604021..5defa124d 100644 --- a/Client/src/Components/BasicTable/index.css +++ b/Client/src/Components/BasicTable/index.css @@ -17,8 +17,6 @@ .MuiPaper-root:has(table.MuiTable-root) { box-shadow: none; - border: solid 1px var(--env-var-color-16); - border-radius: var(--env-var-radius-1); } .MuiTable-root .MuiTableBody-root @@ -26,10 +24,7 @@ .MuiTableCell-root { border: none; } -.MuiTable-root .MuiTableHead-root, -.MuiTable-root .MuiTableRow-root:hover { - background-color: var(--env-var-color-13); -} + .MuiTable-root .MuiTableHead-root .MuiTableCell-root, .MuiTable-root .MuiTableBody-root .MuiTableCell-root { font-size: var(--env-var-font-size-medium); @@ -37,7 +32,6 @@ .MuiTable-root .MuiTableHead-root .MuiTableCell-root { padding: var(--env-var-spacing-1) var(--env-var-spacing-2); font-weight: 500; - color: var(--env-var-color-2); } .MuiTable-root .MuiTableHead-root span { display: inline-block; @@ -52,7 +46,6 @@ height: 20px; } .MuiTable-root .MuiTableBody-root .MuiTableCell-root { - color: var(--env-var-color-5); padding: 6px var(--env-var-spacing-2); } .MuiTable-root .MuiTableBody-root .MuiTableRow-root { @@ -60,21 +53,14 @@ } .MuiPaper-root + .MuiPagination-root { - margin-top: 24px; - border: solid 1px var(--env-var-color-16); border-radius: var(--env-var-radius-1); padding: var(--env-var-spacing-1-plus) var(--env-var-spacing-2); } .MuiPaper-root + .MuiPagination-root ul { justify-content: center; } -.MuiPaper-root - + .MuiPagination-root - ul - li:not(:first-child):not(:last-child) - button { +.MuiPaper-root + .MuiPagination-root button { font-size: var(--env-var-font-size-medium); - color: var(--env-var-color-5); font-weight: 500; } .MuiPaper-root + .MuiPagination-root ul li:first-child { @@ -83,11 +69,6 @@ .MuiPaper-root + .MuiPagination-root ul li:last-child { margin-left: auto; } -.MuiPaper-root + .MuiPagination-root ul li:first-child button, -.MuiPaper-root + .MuiPagination-root ul li:last-child button { - border: solid 1px var(--env-var-color-16); - border-radius: var(--env-var-radius-1); -} .MuiPaper-root + .MuiPagination-root ul li:first-child button { padding: 0 var(--env-var-spacing-1) 0 var(--env-var-spacing-1-plus); } @@ -107,23 +88,14 @@ content: "Next"; margin-right: 15px; } -.MuiPaper-root + .MuiPagination-root .MuiPaginationItem-root.Mui-selected { - background-color: var(--env-var-color-15); -} .MuiPaper-root + .MuiPagination-root div.MuiPaginationItem-root { user-select: none; } .MuiTablePagination-root p { - color: var(--env-var-color-2); font-weight: 500; font-size: var(--env-var-font-size-small-plus); } -.MuiTablePagination-root button, -.MuiTablePagination-root .MuiSelect-select { - border: solid 1px #ebebeb; - border-radius: var(--env-var-radius-1); -} .MuiTablePagination-root .MuiTablePagination-select.MuiSelect-select { text-align: left; text-align-last: left; @@ -137,10 +109,6 @@ width: 22px; height: 22px; } -.MuiTablePagination-root svg path { - stroke: var(--env-var-color-2); - stroke-width: 1.3; -} .MuiTablePagination-root .MuiSelect-icon { width: 16px; height: 16px; @@ -151,38 +119,9 @@ .MuiTablePagination-root button.Mui-disabled { opacity: 0.4; } -.MuiTablePagination-root button:not(.Mui-disabled):hover { - background-color: #f4f4f4; - border-color: #f4f4f4; -} - -.pagination-dropdown.MuiPaper-root { - box-shadow: var(--env-var-shadow-1); - border: solid 1px var(--env-var-color-6); - border-radius: var(--env-var-radius-1); -} -.pagination-dropdown ul { - padding: 4px; - min-width: 100px; -} -.pagination-dropdown li, -body:has(.pagination-dropdown) p:has(+ .MuiTablePagination-root) { - font-size: var(--env-var-font-size-medium); - color: var(--env-var-color-2); - padding: 4px; -} -.pagination-dropdown li.Mui-selected, -.pagination-dropdown li.Mui-selected.Mui-focusVisible, -.pagination-dropdown li.Mui-selected:hover { - background-color: #f4f4f4; -} -body:has(.pagination-dropdown) p:has(+ .MuiTablePagination-root) { - font-size: var(--env-var-font-size-small-plus); -} .monitors .MuiTable-root .MuiTableHead-root .MuiTableCell-root { text-transform: uppercase; - color: var(--env-var-color-5); opacity: 0.8; font-size: var(--env-var-font-size-small-plus); font-weight: 400; diff --git a/Client/src/Components/BasicTable/index.jsx b/Client/src/Components/BasicTable/index.jsx index c723d2bec..e9fbbadfd 100644 --- a/Client/src/Components/BasicTable/index.jsx +++ b/Client/src/Components/BasicTable/index.jsx @@ -13,6 +13,7 @@ import { Box, Typography, Stack, + Button, } from "@mui/material"; import { useDispatch, useSelector } from "react-redux"; import { setRowsPerPage } from "../../Features/UI/uiSlice"; @@ -21,7 +22,6 @@ import RightArrowDouble from "../../assets/icons/right-arrow-double.svg?react"; 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 Button from "../Button"; import "./index.css"; /** * Component for pagination actions (first, previous, next, last). @@ -54,37 +54,37 @@ const TablePaginationActions = (props) => { return ( } + variant="group" onClick={handleFirstPageButtonClick} disabled={page === 0} aria-label="first page" - /> + > + + } + variant="group" onClick={handleBackButtonClick} disabled={page === 0} aria-label="previous page" - /> + > + + } + variant="group" onClick={handleNextButtonClick} disabled={page >= Math.ceil(count / rowsPerPage) - 1} aria-label="next page" - /> + > + + } + variant="group" onClick={handleLastPageButtonClick} disabled={page >= Math.ceil(count / rowsPerPage) - 1} aria-label="last page" - /> + > + + ); }; @@ -215,6 +215,9 @@ const BasicTable = ({ data, paginated, reversed, table }) => { { direction="row" alignItems="center" justifyContent="space-between" - px={theme.gap.small} + px={theme.spacing(4)} + sx={{ + "& p": { + color: theme.palette.text.tertiary, + }, + }} > - + Showing {getRange()} of {data.rows.length} monitor(s) { keepMounted: true, PaperProps: { className: "pagination-dropdown", + sx: { + mt: 0, + mb: theme.spacing(2), + }, }, transformOrigin: { vertical: "bottom", horizontal: "left" }, anchorOrigin: { vertical: "top", horizontal: "left" }, - sx: { mt: "-4px" }, + sx: { mt: theme.spacing(-2) }, }, inputProps: { id: "pagination-dropdown" }, IconComponent: SelectorVertical, sx: { - ml: theme.gap.small, - mr: theme.gap.large, - minWidth: theme.gap.xl, + ml: theme.spacing(4), + mr: theme.spacing(12), + minWidth: theme.spacing(20), textAlign: "left", "&.Mui-focused > div": { - backgroundColor: theme.palette.otherColors.white, + backgroundColor: theme.palette.background.main, }, }, }, }} sx={{ - mt: theme.gap.medium, - color: theme.palette.otherColors.bluishGray, + mt: theme.spacing(6), + color: theme.palette.text.secondary, + "& svg path": { + stroke: theme.palette.text.tertiary, + strokeWidth: 1.3, + }, + "& .MuiSelect-select": { + border: 1, + borderColor: theme.palette.border.light, + borderRadius: theme.shape.borderRadius, + }, }} /> diff --git a/Client/src/Components/Breadcrumbs/index.css b/Client/src/Components/Breadcrumbs/index.css index b7d190de1..a425946e8 100644 --- a/Client/src/Components/Breadcrumbs/index.css +++ b/Client/src/Components/Breadcrumbs/index.css @@ -1,5 +1,5 @@ .MuiBreadcrumbs-root { - height: 36px; + height: 34px; } .MuiBreadcrumbs-root svg { width: 16px; @@ -7,17 +7,8 @@ } .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; } diff --git a/Client/src/Components/Breadcrumbs/index.jsx b/Client/src/Components/Breadcrumbs/index.jsx index 506263fce..f2ca2089f 100644 --- a/Client/src/Components/Breadcrumbs/index.jsx +++ b/Client/src/Components/Breadcrumbs/index.jsx @@ -25,23 +25,36 @@ const Breadcrumbs = ({ list }) => { } aria-label="breadcrumb" - p={theme.gap.small} + px={theme.spacing(2)} + py={theme.spacing(3.5)} width="fit-content" - backgroundColor={theme.palette.otherColors.fillGray} - borderRadius={`${theme.shape.borderRadius}px`} + backgroundColor={theme.palette.background.fill} + borderRadius={theme.shape.borderRadius} lineHeight="18px" + sx={{ + "& .MuiBreadcrumbs-li:not(:last-of-type):hover a": { + backgroundColor: theme.palette.other.fill, + opacity: 1, + }, + }} > {list.map((item, index) => { return ( navigate(item.path)} + sx={{ + opacity: 0.8, + textTransform: "capitalize", + "&, &:hover": { + color: theme.palette.text.tertiary, + }, + }} > {item.name} diff --git a/Client/src/Components/Button/index.css b/Client/src/Components/Button/index.css deleted file mode 100644 index 63791d46b..000000000 --- a/Client/src/Components/Button/index.css +++ /dev/null @@ -1,33 +0,0 @@ -.MuiButtonBase-root.button-rotate90 svg { - transition: transform 500ms ease; -} -.MuiButtonBase-root.button-rotate90:hover svg { - transform: rotate(90deg); - transform-origin: center; -} - -.MuiButtonBase-root.button-rotate180 svg { - transition: transform 700ms ease; -} -.MuiButtonBase-root.button-rotate180:hover svg { - transform: rotate(180deg); - transform-origin: center; -} - -.MuiButtonBase-root.button-slideLeft { - padding-left: 45px; -} -.MuiButtonBase-root.button-slideLeft svg { - position: absolute; - left: 15px; - transition: left 250ms cubic-bezier(0.65, 0, 0.076, 1); -} -.MuiButtonBase-root.button-slideLeft:hover svg { - left: 11px; -} - -.MuiButtonBase-root.button-slideLeft:hover, -.MuiButtonBase-root.button-rotate180:hover, -.MuiButtonBase-root.button-rotate90:hover { - background-color: rgba(71, 84, 103, 0.08); -} diff --git a/Client/src/Components/Button/index.jsx b/Client/src/Components/Button/index.jsx deleted file mode 100644 index 485973498..000000000 --- a/Client/src/Components/Button/index.jsx +++ /dev/null @@ -1,107 +0,0 @@ -import { Button as MuiButton } from "@mui/material"; -import PropTypes from "prop-types"; -import "./index.css"; - -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: "text", - }, - imageSecondary: { - color: "secondary", - variant: "text", - }, - imageTertiary: { - color: "tertiary", - variant: "text", - }, -}; - -/** - * @component - * @param {Object} props - * @param {'primary' | 'secondary' | 'tertiary' | 'error' | 'imagePrimary' | 'imageSecondary' | 'imageTertiary'} props.level - The level of the button - * @param {string} props.type - The type of the button - * @param {string} props.label - The label of the button - * @param {React.ReactNode} props.img - Image for button - * @param {boolean} [props.disabled] - Whether the button is disabled - * @param {function} props.onClick - Function to run when the button is clicked - * @param {Object} props.sx - Styles for the button - * @returns {JSX.Element} - * @example - * // Render an error button - * - */ - -const Button = ({ - id, - animate, - type, - level, - label, - disabled, - img, - onClick, - props, - sx, -}) => { - const { variant, color } = levelConfig[level]; - return ( - - {img && img} - {label} - - ); -}; - -Button.propTypes = { - id: PropTypes.string, - type: PropTypes.string, - level: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - img: PropTypes.node, - onClick: PropTypes.func, - sx: PropTypes.object, - disabled: PropTypes.bool, -}; - -export default Button; diff --git a/Client/src/Components/ButtonSpinner/index.jsx b/Client/src/Components/ButtonSpinner/index.jsx deleted file mode 100644 index 62cfae477..000000000 --- a/Client/src/Components/ButtonSpinner/index.jsx +++ /dev/null @@ -1,104 +0,0 @@ -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", - }, -}; - -/** - * @component - * @param {Object} props - * @param {'primary' | 'secondary' | 'tertiary' | 'error'} 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/Components/Charts/BarChart/index.css b/Client/src/Components/Charts/BarChart/index.css index e386aef1c..e69de29bb 100644 --- a/Client/src/Components/Charts/BarChart/index.css +++ b/Client/src/Components/Charts/BarChart/index.css @@ -1,19 +0,0 @@ -.bar-tooltip .MuiTooltip-tooltip { - background-color: white; - border: solid 1px black; - box-shadow: var(--env-var-shadow-1); - border: solid 1px var(--env-var-color-6); - border-radius: var(--env-var-radius-1); - padding: 4px 8px; -} - -.bar-tooltip .MuiTooltip-tooltip p { - font-size: var(--env-var-font-size-small-plus); - color: var(--env-var-color-2); - font-weight: 500; -} -.bar-tooltip .MuiTooltip-tooltip span { - font-size: var(--env-var-font-size-small); - color: var(--env-var-color-2); - font-weight: 600; -} diff --git a/Client/src/Components/Charts/BarChart/index.jsx b/Client/src/Components/Charts/BarChart/index.jsx index 9238374c2..54a9524af 100644 --- a/Client/src/Components/Charts/BarChart/index.jsx +++ b/Client/src/Components/Charts/BarChart/index.jsx @@ -26,7 +26,7 @@ const BarChart = ({ checks = [] }) => { event.stopPropagation()} @@ -39,11 +39,11 @@ const BarChart = ({ checks = [] }) => { ) : ( @@ -53,15 +53,15 @@ const BarChart = ({ checks = [] }) => { {formatDate(new Date(check.createdAt), { year: undefined })} - + @@ -69,8 +69,8 @@ const BarChart = ({ checks = [] }) => { display="inline-flex" direction="row" justifyContent="space-between" - ml={theme.gap.xs} - gap={theme.gap.large} + ml={theme.spacing(2)} + gap={theme.spacing(12)} > Response Time @@ -99,6 +99,27 @@ const BarChart = ({ checks = [] }) => { }, }, ], + sx: { + "& .MuiTooltip-tooltip": { + backgroundColor: theme.palette.background.main, + border: 1, + borderColor: theme.palette.border.dark, + borderRadius: theme.shape.borderRadius, + boxShadow: theme.shape.boxShadow, + px: theme.spacing(4), + py: theme.spacing(2), + }, + "& .MuiTooltip-tooltip p": { + fontSize: 12, + color: theme.palette.text.tertiary, + fontWeight: 500, + }, + "& .MuiTooltip-tooltip span": { + fontSize: 11, + color: theme.palette.text.tertiary, + fontWeight: 600, + }, + }, }, }} > @@ -107,10 +128,10 @@ const BarChart = ({ checks = [] }) => { width="9px" height="100%" backgroundColor={ - check.status ? theme.label.up.bgColor : theme.label.down.bgColor + check.status ? theme.palette.success.bg : theme.palette.error.bg } sx={{ - borderRadius: "3px", + borderRadius: theme.spacing(1.5), "&:hover > .MuiBox-root": { filter: "brightness(0.8)", }, @@ -123,11 +144,11 @@ const BarChart = ({ checks = [] }) => { height={`${animate ? check.responseTime : 0}%`} backgroundColor={ check.status - ? theme.label.up.dotColor - : theme.label.down.dotColor + ? theme.palette.success.main + : theme.palette.error.text } sx={{ - borderRadius: "3px", + borderRadius: theme.spacing(1.5), transition: "height 600ms cubic-bezier(0.4, 0, 0.2, 1)", }} /> diff --git a/Client/src/Components/Charts/MonitorDetails60MinChart/index.jsx b/Client/src/Components/Charts/MonitorDetails60MinChart/index.jsx index f7fc0d3d8..57cb344e7 100644 --- a/Client/src/Components/Charts/MonitorDetails60MinChart/index.jsx +++ b/Client/src/Components/Charts/MonitorDetails60MinChart/index.jsx @@ -1,17 +1,20 @@ import { BarChart, Bar, Cell, ReferenceLine, Label } from "recharts"; import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; const MonitorDetails60MinChart = ({ data }) => { + const theme = useTheme(); + const labelStyle = { fontSize: "10px", - fill: "#475467", + fill: theme.palette.text.tertiary, }; const color = { - true: "var(--env-var-color-23)", - false: "var(--env-var-color-24)", - undefined: "var(--env-var-color-33)", + true: theme.palette.success.main, + false: theme.palette.error.text, + undefined: theme.palette.unresolved.main, }; return ( { + const theme = useTheme(); + if (active && payload && payload.length) { return ( - - + + {new Date(label).toLocaleDateString("en-US", { weekday: "short", // Mon month: "long", // July @@ -18,10 +37,18 @@ const CustomToolTip = ({ active, payload, label }) => { minute: "2-digit", // 15 hour12: true, // AM/PM format })} - - Response Time (ms): {payload[0].payload.originalResponseTime}{" "} + + + Response Time (ms): {payload[0].payload.originalResponseTime} + {" "} {/* Display original value */} - + ); } return null; diff --git a/Client/src/Components/Charts/ResponseTimeChart/index.css b/Client/src/Components/Charts/ResponseTimeChart/index.css index 86b7a1d1b..e69de29bb 100644 --- a/Client/src/Components/Charts/ResponseTimeChart/index.css +++ b/Client/src/Components/Charts/ResponseTimeChart/index.css @@ -1,7 +0,0 @@ -.chart-container { - display: flex; - justify-content: space-around; - align-items: flex-end; - height: 50px; - width: 300px; -} diff --git a/Client/src/Components/Charts/ResponseTimeChart/index.jsx b/Client/src/Components/Charts/ResponseTimeChart/index.jsx index 66ef6b3eb..fd6c0f41e 100644 --- a/Client/src/Components/Charts/ResponseTimeChart/index.jsx +++ b/Client/src/Components/Charts/ResponseTimeChart/index.jsx @@ -1,10 +1,17 @@ -import "./index.css"; import PropTypes from "prop-types"; +import { Stack } from "@mui/material"; import { BarChart, Bar, ResponsiveContainer, Cell } from "recharts"; +import "./index.css"; const ResponseTimeChart = ({ checks = [] }) => { return ( - + { key={`cell-${index}`} fill={ check.status === true - ? "var(--env-var-color-23)" - : "var(--env-var-color-24)" + ? "var(--success-color)" + : "var(--error-color)" } /> ))} - + ); }; diff --git a/Client/src/Components/Charts/Statistics/Statistic.jsx b/Client/src/Components/Charts/Statistics/Statistic.jsx deleted file mode 100644 index 99ad2066f..000000000 --- a/Client/src/Components/Charts/Statistics/Statistic.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from "react"; -import "./statistic.css"; -import PropTypes from "prop-types"; -import { useTheme } from "@mui/material"; - -/** - * @component - * @param {Object} props - * @param {string} props.title - The title text for the statistic (required) - * @param {string} props.value - The numerical or textual value for the statistic (required) - * @returns {JSX.Element} - Renders the statistic component - */ -const Statistic = ({ title, value }) => { - const theme = useTheme(); - - const fontLookup = { - default: theme.font.default.font, - }; - - const fontFamily = fontLookup["default"]; - - return ( - - {title} - {value} - - ); -}; - -Statistic.propTypes = { - title: PropTypes.string.isRequired, - value: PropTypes.string.isRequired, -}; - -export default Statistic; diff --git a/Client/src/Components/Charts/Statistics/statistic.css b/Client/src/Components/Charts/Statistics/statistic.css deleted file mode 100644 index a8834199d..000000000 --- a/Client/src/Components/Charts/Statistics/statistic.css +++ /dev/null @@ -1,31 +0,0 @@ -:root { - --stats-spacing-general-0: 5px 10px; - --color-border-0: #eaecf0; - --border-radius-0: 4px; - --font-size-0: 1rem; - --font-size-1: 13px; - --font-color-0: #344054; - --margin-bottom-0: 10px; -} - -.statistic-tile { - margin: var(--stats-spacing-general-0); - padding: var(--stats-spacing-general-0); - border: 1px solid var(--color-border-0); - border-radius: var(--border-radius-0); - - width: 180px; -} - -.statistic-tile-title { - font-size: var(--font-size-0); - color: var(--font-color-0); - margin-bottom: var(--margin-bottom-0); -} - -.statistic-tile-value { - font-size: var(--font-size-1); - color: var(--font-color-0); - font-weight: 700; - margin-bottom: var(--margin-bottom-0); -} diff --git a/Client/src/Components/Check/Check.jsx b/Client/src/Components/Check/Check.jsx index 645e0b5e2..9eaba846e 100644 --- a/Client/src/Components/Check/Check.jsx +++ b/Client/src/Components/Check/Check.jsx @@ -2,7 +2,7 @@ import "./check.css"; import PropTypes from "prop-types"; import CheckGrey from "../../assets/icons/check.svg?react"; import CheckOutlined from "../../assets/icons/check-outlined.svg?react"; -import { Stack, Typography } from "@mui/material"; +import { Box, Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; /** @@ -22,25 +22,41 @@ import { useTheme } from "@emotion/react"; */ const Check = ({ text, variant = "info", outlined = false }) => { const theme = useTheme(); + const colors = { + success: theme.palette.success.main, + error: theme.palette.error.text, + info: theme.palette.info.border, + }; + return ( {outlined ? ( ) : ( - + path": { fill: colors[variant] }, + }} + > + + )} - {text} + + {text} + ); }; diff --git a/Client/src/Components/Check/check.css b/Client/src/Components/Check/check.css index f1432d2bc..8b1378917 100644 --- a/Client/src/Components/Check/check.css +++ b/Client/src/Components/Check/check.css @@ -1,17 +1 @@ -.check > span.MuiTypography-root { - font-size: var(--env-var-font-size-small-plus); - color: var(--env-var-color-2); - opacity: 0.8; -} -.check-error span.MuiTypography-root { - color: var(--env-var-color-24); -} -.check-error svg > path { - fill: var(--env-var-color-19); -} -.check-success span.MuiTypography-root { - color: var(--env-var-color-17); -} -.check-success svg > path { - fill: var(--env-var-color-23); -} + diff --git a/Client/src/Components/Fallback/index.css b/Client/src/Components/Fallback/index.css index 226c7bc47..293eb13b3 100644 --- a/Client/src/Components/Fallback/index.css +++ b/Client/src/Components/Fallback/index.css @@ -7,10 +7,6 @@ font-size: var(--env-var-font-size-large); font-weight: 600; } -[class*="fallback__"] .check span.MuiTypography-root, -[class*="fallback__"] h1.MuiTypography-root { - color: var(--env-var-color-5); -} [class*="fallback__"] button.MuiButtonBase-root, [class*="fallback__"] .check { width: max-content; diff --git a/Client/src/Components/Fallback/index.jsx b/Client/src/Components/Fallback/index.jsx index 976440f7d..8d6b21f73 100644 --- a/Client/src/Components/Fallback/index.jsx +++ b/Client/src/Components/Fallback/index.jsx @@ -1,11 +1,12 @@ import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; -import { Box, Stack, Typography } from "@mui/material"; +import { Box, Button, Stack, Typography } from "@mui/material"; import Skeleton from "../../assets/Images/create-placeholder.svg?react"; -import background from "../../assets/Images/background_pattern_decorative.png"; -import Button from "../Button"; +import SkeletonDark from "../../assets/Images/create-placeholder-dark.svg?react"; +import Background from "../../assets/Images/background-grid.svg?react"; import Check from "../Check/Check"; import { useNavigate } from "react-router-dom"; +import { useSelector } from "react-redux"; import "./index.css"; /** @@ -19,23 +20,38 @@ import "./index.css"; * @returns {JSX.Element} The rendered fallback UI. */ -const Fallback = ({ title, checks, link = "/" }) => { +const Fallback = ({ title, checks, link = "/", isAdmin }) => { const theme = useTheme(); const navigate = useNavigate(); + const mode = useSelector((state) => state.ui.mode); return ( - + {mode === "light" ? ( + + ) : ( + + )} - - + sx={{ + "& svg g g:last-of-type path": { + stroke: theme.palette.border.light, + }, + }} + > + + + + A {title} is used to: {checks.map((check, index) => ( @@ -46,12 +62,17 @@ const Fallback = ({ title, checks, link = "/" }) => { /> ))} - navigate(link)} - /> + {/* TODO - display a different fallback if user is not an admin*/} + {isAdmin && ( + navigate(link)} + > + Let's create your {title} + + )} ); }; @@ -60,6 +81,7 @@ Fallback.propTypes = { title: PropTypes.string.isRequired, checks: PropTypes.arrayOf(PropTypes.string).isRequired, link: PropTypes.string, + isAdmin: PropTypes.bool, }; export default Fallback; diff --git a/Client/src/Components/Inputs/Checkbox/index.css b/Client/src/Components/Inputs/Checkbox/index.css index 0d2285955..e69de29bb 100644 --- a/Client/src/Components/Inputs/Checkbox/index.css +++ b/Client/src/Components/Inputs/Checkbox/index.css @@ -1,14 +0,0 @@ -.checkbox-wrapper { - border-radius: var(--env-var-radius-1); -} -.checkbox-wrapper:not(:has(.Mui-disabled)):hover { - background-color: var(--env-var-color-15); -} -.checkbox-wrapper span.MuiTypography-root { - font-size: var(--env-var-font-size-medium); - color: var(--env-var-color-2); -} -.checkbox-wrapper .MuiButtonBase-root { - margin-right: var(--env-var-spacing-1); - padding: 0; -} diff --git a/Client/src/Components/Inputs/Checkbox/index.jsx b/Client/src/Components/Inputs/Checkbox/index.jsx index e454bbc6e..e7af04a64 100644 --- a/Client/src/Components/Inputs/Checkbox/index.jsx +++ b/Client/src/Components/Inputs/Checkbox/index.jsx @@ -1,5 +1,6 @@ import PropTypes from "prop-types"; import { FormControlLabel, Checkbox as MuiCheckbox } from "@mui/material"; +import { useTheme } from "@emotion/react"; import CheckboxOutline from "../../../assets/icons/checkbox-outline.svg?react"; import CheckboxFilled from "../../../assets/icons/checkbox-filled.svg?react"; @@ -16,7 +17,7 @@ import "./index.css"; * @param {boolean} [props.isDisabled] - Whether the checkbox is disabled or not. * * @returns {JSX.Element} - * + * * @example * { const sizes = { small: "14px", medium: "16px", large: "18px" }; + const theme = useTheme(); return ( diff --git a/Client/src/Components/Inputs/Field/index.css b/Client/src/Components/Inputs/Field/index.css index 3e2435694..ab1812013 100644 --- a/Client/src/Components/Inputs/Field/index.css +++ b/Client/src/Components/Inputs/Field/index.css @@ -8,24 +8,6 @@ .field .input-error { font-size: var(--env-var-font-size-medium); } -.field h3.MuiTypography-root, -.field h5.MuiTypography-root, -.field input, -.field textarea { - color: var(--env-var-color-5); -} -.field h3.MuiTypography-root { - font-weight: 500; -} -.field h3.MuiTypography-root span.field-required { - color: var(--env-var-color-19); - margin-left: 2px; -} -.field h3.MuiTypography-root span.field-optional { - opacity: 0.6; - margin-left: 4px; - font-weight: 400; -} .field h5.MuiTypography-root { position: relative; opacity: 0.8; @@ -37,24 +19,7 @@ .field .MuiInputBase-root:has(.MuiInputAdornment-root) { padding-right: var(--env-var-spacing-1-minus); } -.field - .MuiInputBase-root:has(.copy.MuiInputAdornment-root > .MuiButtonBase-root) { - padding-right: 0; -} -.field .MuiInputBase-root .copy.MuiInputAdornment-root .MuiButtonBase-root { - padding: 0 var(--env-var-spacing-1-minus); - font-size: var(--env-var-font-size-medium); - border-radius: 0; - height: 34px; -} -.field .MuiInputBase-root .copy.MuiInputAdornment-root .MuiButtonBase-root svg { - width: 20px; - height: 20px; - margin-right: 5px; -} -.field .MuiInputBase-root .MuiInputAdornment-root .MuiButtonBase-root:hover { - border-color: var(--env-var-color-29); -} + .field input { height: 100%; padding: 0 var(--env-var-spacing-1-minus); @@ -62,22 +27,6 @@ .field .MuiInputBase-root:has(textarea) { padding: var(--env-var-spacing-1-minus); } -.field .MuiOutlinedInput-root fieldset { - border-color: var(--env-var-color-29); - border-radius: var(--env-var-radius-1); -} -.field:not(:has(.Mui-disabled)):not(:has(.input-error)) - .MuiOutlinedInput-root:hover:not(:has(input:focus)):not(:has(textarea:focus)) - fieldset { - border-color: var(--env-var-color-29); -} -.field .input-error { - color: var(--env-var-color-24); - opacity: 0.8; -} -.field:has(.input-error) .MuiOutlinedInput-root fieldset { - border-color: var(--env-var-color-19); -} .register-page .field .MuiOutlinedInput-root fieldset, .register-page .field input, diff --git a/Client/src/Components/Inputs/Field/index.jsx b/Client/src/Components/Inputs/Field/index.jsx index 850598d3e..62c50eeff 100644 --- a/Client/src/Components/Inputs/Field/index.jsx +++ b/Client/src/Components/Inputs/Field/index.jsx @@ -1,4 +1,5 @@ import PropTypes from "prop-types"; +import { forwardRef, useState } from "react"; import { useTheme } from "@emotion/react"; import { IconButton, @@ -9,10 +10,7 @@ import { } from "@mui/material"; import VisibilityOff from "@mui/icons-material/VisibilityOff"; import Visibility from "@mui/icons-material/Visibility"; -import ContentCopyIcon from "@mui/icons-material/ContentCopy"; -import Button from "../../Button"; import "./index.css"; -import { forwardRef, useState } from "react"; /** * @param {Object} props @@ -23,7 +21,6 @@ import { forwardRef, useState } from "react"; * @param {boolean} [props.isRequired] - Indicates if the field is required, will display a red asterisk. * @param {boolean} [props.isOptional] - Indicates if the field is optional, will display optional text. * @param {string} [props.optionalLabel] - Optional label for the input field. - * @param {boolean} [props.hasCopy] - Indicates if the field supports copying. * @param {string} [props.autoComplete] - Autocomplete value for the input field. * @param {string} [props.placeholder] - Placeholder text for the input field. * @param {string} props.value - Value of the input field. @@ -43,7 +40,6 @@ const Field = forwardRef( isRequired, isOptional, optionalLabel, - hasCopy, autoComplete, placeholder, value, @@ -56,25 +52,54 @@ const Field = forwardRef( ) => { const theme = useTheme(); - // TODO - are we using this feature anywhere ? - const [copy, setCopy] = useState(false); - const handleCopy = () => { - setCopy(true); - setTimeout(() => setCopy(false), 1000); - }; - const [isVisible, setVisible] = useState(false); return ( - + {label && ( - + {label} - {isRequired ? * : ""} + {isRequired ? ( + + * + + ) : ( + "" + )} {isOptional ? ( - + {optionalLabel || "(optional)"} - + ) : ( "" )} @@ -92,13 +117,22 @@ const Field = forwardRef( onChange={onChange} disabled={disabled} inputRef={ref} + inputProps={{ + sx: { + color: theme.palette.text.secondary, + "&:-webkit-autofill": { + WebkitBoxShadow: `0 0 0 100px ${theme.palette.background.fill} inset`, + WebkitTextFillColor: theme.palette.text.secondary, + }, + }, + }} sx={ type === "url" ? { "& .MuiInputBase-root": { padding: 0 }, "& .MuiStack-root": { - borderTopLeftRadius: `${theme.shape.borderRadius}px`, - borderBottomLeftRadius: `${theme.shape.borderRadius}px`, + borderTopLeftRadius: theme.shape.borderRadius, + borderBottomLeftRadius: theme.shape.borderRadius, }, } : {} @@ -110,65 +144,53 @@ const Field = forwardRef( alignItems="center" height="100%" sx={{ - borderRight: `solid 1px ${theme.palette.section.borderColor}`, - backgroundColor: "#f9f9fa", - pl: theme.gap.medium, + borderRight: `solid 1px ${theme.palette.border.dark}`, + backgroundColor: theme.palette.background.accent, + pl: theme.spacing(6), }} > - + {https ? "https" : "http"}:// ), - endAdornment: - type === "password" ? ( - - setVisible((show) => !show)} - tabIndex={-1} - sx={{ - color: theme.palette.section.borderColor, - padding: `calc(${theme.gap.xs} / 2)`, - "&:focus": { - outline: "none", - }, - "& .MuiTouchRipple-root": { - pointerEvents: "none", - display: "none", - }, - }} - > - {!isVisible ? : } - - - ) : ( - hasCopy && ( - - } - onClick={handleCopy} - sx={{ - borderLeft: `solid 1px ${theme.palette.section.borderColor}`, - lineHeight: 0, - "& .MuiTouchRipple-root": { - pointerEvents: "none", - display: "none", - }, - }} - /> - - ) - ), + endAdornment: type === "password" && ( + + setVisible((show) => !show)} + tabIndex={-1} + sx={{ + color: theme.palette.border.dark, + padding: theme.spacing(1), + "&:focus": { + outline: "none", + }, + "& .MuiTouchRipple-root": { + pointerEvents: "none", + display: "none", + }, + }} + > + {!isVisible ? : } + + + ), }} /> {error && ( {error} @@ -195,7 +217,6 @@ Field.propTypes = { isRequired: PropTypes.bool, isOptional: PropTypes.bool, optionalLabel: PropTypes.string, - hasCopy: PropTypes.bool, autoComplete: PropTypes.string, placeholder: PropTypes.string, value: PropTypes.string.isRequired, diff --git a/Client/src/Components/Inputs/Image/index.css b/Client/src/Components/Inputs/Image/index.css index 96a730bf0..649012e1c 100644 --- a/Client/src/Components/Inputs/Image/index.css +++ b/Client/src/Components/Inputs/Image/index.css @@ -1,9 +1,3 @@ -.image-field-wrapper h2.MuiTypography-root, -.image-field-wrapper p.MuiTypography-root, -.image-field-wrapper + p.MuiTypography-root, -.MuiStack-root:has(#modal-update-picture) h1.MuiTypography-root { - color: var(--env-var-color-2); -} .MuiStack-root:has(#modal-update-picture) h1.MuiTypography-root { font-weight: 600; } @@ -20,10 +14,5 @@ } .image-field-wrapper + p.MuiTypography-root, .image-field-wrapper p.MuiTypography-root { - opacity: 0.6; font-size: var(--env-var-font-size-small-plus); } -.image-field-wrapper h2.MuiTypography-root span { - color: var(--env-var-color-3); - font-weight: 500; -} diff --git a/Client/src/Components/Inputs/Image/index.jsx b/Client/src/Components/Inputs/Image/index.jsx index 771f461b4..cdc7d8c7f 100644 --- a/Client/src/Components/Inputs/Image/index.jsx +++ b/Client/src/Components/Inputs/Image/index.jsx @@ -31,15 +31,15 @@ const ImageField = ({ id, src, loading, onChange }) => { <> { - - Click to upload or drag and drop + + + Click to upload + {" "} + or drag and drop + + + (maximum size: 3MB) - (maximum size: 3MB) - Supported formats: JPG, PNG + + Supported formats: JPG, PNG + > ) : ( diff --git a/Client/src/Components/Inputs/Radio/index.css b/Client/src/Components/Inputs/Radio/index.css new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/Client/src/Components/Inputs/Radio/index.css @@ -0,0 +1 @@ + diff --git a/Client/src/Components/RadioButton/index.jsx b/Client/src/Components/Inputs/Radio/index.jsx similarity index 51% rename from Client/src/Components/RadioButton/index.jsx rename to Client/src/Components/Inputs/Radio/index.jsx index acc87c992..1772912ac 100644 --- a/Client/src/Components/RadioButton/index.jsx +++ b/Client/src/Components/Inputs/Radio/index.jsx @@ -1,10 +1,16 @@ +import PropTypes from "prop-types"; +import { FormControlLabel, Radio as MUIRadio, Typography } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import RadioChecked from "../../../assets/icons/radio-checked.svg?react"; +import "./index.css"; + /** - * RadioButton component. + * Radio component. * * @component * @example * // Usage: - * { const theme = useTheme(); + return ( } + sx={{ + color: "transparent", + width: 16, + height: 16, + boxShadow: "inset 0 0 0 1px #656a74", + mt: theme.spacing(0.5), + }} /> } onChange={props.onChange} label={ <> {props.title} - + {props.desc} > } labelPlacement="end" - sx={{ margin: 0, alignItems: "flex-start" }} + sx={{ + alignItems: "flex-start", + p: theme.spacing(2.5), + m: theme.spacing(-2.5), + borderRadius: theme.shape.borderRadius, + "&:hover": { + backgroundColor: theme.palette.background.accent, + }, + "& .MuiButtonBase-root": { + p: 0, + mr: theme.spacing(6), + }, + }} /> ); -} +}; -RadioButton.propTypes = { +Radio.propTypes = { title: PropTypes.string.isRequired, desc: PropTypes.string, size: PropTypes.string, }; -export default RadioButton; +export default Radio; diff --git a/Client/src/Components/Inputs/Select/index.css b/Client/src/Components/Inputs/Select/index.css index be1a92503..329cdd7d2 100644 --- a/Client/src/Components/Inputs/Select/index.css +++ b/Client/src/Components/Inputs/Select/index.css @@ -1,46 +1,7 @@ .select-wrapper .select-component > .MuiSelect-select { padding: 0 10px; - height: 34px !important; - display: flex !important; + height: 34px; + display: flex; align-items: center; - text-transform: capitalize; line-height: 1; } -.select-wrapper .MuiInputBase-root:has(> .MuiSelect-select) { - min-width: 150px; -} -.select-wrapper .MuiInputBase-root:has(> .MuiSelect-select), -.MuiMenu-List .MuiMenuItem-root, -.select-wrapper h3.MuiTypography-root { - font-size: var(--env-var-font-size-medium); - color: var(--env-var-color-5); -} -.select-wrapper h3.MuiTypography-root { - font-weight: 500; -} -.MuiInputBase-root:has(> .MuiSelect-select) fieldset { - border-color: var(--env-var-color-29); -} -.MuiInputBase-root:not(.Mui-focused):has(> .MuiSelect-select):hover fieldset { - border-color: var(--env-var-color-29); -} - -.select-dropdown.MuiPaper-root { - box-shadow: var(--env-var-shadow-1); - border: solid 1px var(--env-var-color-6); - border-radius: var(--env-var-radius-1); -} -.select-dropdown .MuiList-root { - min-width: 100px; -} -.select-dropdown li { - font-size: var(--env-var-font-size-medium); - color: var(--env-var-color-5); - padding: 5px 8px; - border-radius: var(--env-var-radius-1); -} -.select-dropdown li.Mui-selected, -.select-dropdown li.Mui-selected.Mui-focusVisible, -.select-dropdown li.Mui-selected:hover { - background-color: #f4f4f4; -} diff --git a/Client/src/Components/Inputs/Select/index.jsx b/Client/src/Components/Inputs/Select/index.jsx index 99240bc2b..e30b5d951 100644 --- a/Client/src/Components/Inputs/Select/index.jsx +++ b/Client/src/Components/Inputs/Select/index.jsx @@ -55,34 +55,45 @@ const Select = ({ const theme = useTheme(); const itemStyles = { fontSize: "var(--env-var-font-size-medium)", - color: theme.palette.otherColors.bluishGray, - textTransform: "capitalize", - borderRadius: `${theme.shape.borderRadius}px`, - margin: theme.gap.xs, + color: theme.palette.text.tertiary, + borderRadius: theme.shape.borderRadius, + margin: theme.spacing(2), }; return ( - - {label && {label}} + + {label && ( + + {label} + + )} {placeholder && ( { { typeof color !== "string" || !/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(color) ) { - color = theme.palette.labelGray.color; + color = theme.palette.border.light; } // Calculate lighter shades for border and bg @@ -127,8 +127,26 @@ ColoredLabel.propTypes = { const StatusLabel = ({ status, text, customStyles }) => { const theme = useTheme(); + const colors = { + up: { + dotColor: theme.palette.success.main, + bgColor: theme.palette.success.bg, + borderColor: theme.palette.success.light, + }, + down: { + dotColor: theme.palette.error.text, + bgColor: theme.palette.error.bg, + borderColor: theme.palette.error.light, + }, + "cannot resolve": { + dotColor: theme.palette.unresolved.main, + bgColor: theme.palette.unresolved.bg, + borderColor: theme.palette.unresolved.light, + }, + }; + // Look up the color for the status - const { borderColor, bgColor, dotColor } = theme.label[status]; + const { borderColor, bgColor, dotColor } = colors[status]; return ( { const levelConfig = { primary: {}, secondary: { - color: theme.palette.otherColors.bluishGray, + color: theme.palette.text.secondary, sx: { ":hover": { - color: theme.palette.otherColors.bluishGray, + color: theme.palette.text.secondary, }, }, }, tertiary: { - color: theme.palette.tertiary.main, + color: theme.palette.text.tertiary, sx: { textDecoration: "underline", textDecorationStyle: "dashed", textDecorationColor: theme.palette.primary.main, textUnderlineOffset: "1px", ":hover": { - color: theme.palette.tertiary.main, + color: theme.palette.text.tertiary, textDecorationColor: theme.palette.primary.main, - backgroundColor: theme.palette.tertiary.linkHover, + backgroundColor: theme.palette.background.fill, }, }, }, diff --git a/Client/src/Components/ProgressBars/index.css b/Client/src/Components/ProgressBars/index.css index 5ba1939da..09e4915da 100644 --- a/Client/src/Components/ProgressBars/index.css +++ b/Client/src/Components/ProgressBars/index.css @@ -1,32 +1,10 @@ -.progress-bar-container { - background-color: #fafafa; - padding: var(--env-var-spacing-1-plus); -} .progress-bar-container h2.MuiTypography-root, .progress-bar-container p.MuiTypography-root { - color: var(--env-var-color-2); font-size: var(--env-var-font-size-medium); } -.progress-bar-container p.MuiTypography-root { - opacity: 0.6; -} .progress-bar-container p.MuiTypography-root:has(span) { font-size: 12px; } .progress-bar-container p.MuiTypography-root span { padding-left: 2px; } -.progress-bar-container:has(p.input-error){ - border-color: var(--env-var-color-19); - background-color: var(--env-var-color-21); - padding: 8px var(--env-var-spacing-1-plus); -} -.progress-bar-container p.input-error{ - color: var(--env-var-color-24); - opacity: 0.8; -} -.progress-bar-container:has(p.input-error)>.MuiStack-root>svg{ - fill: var(--env-var-color-24); - width: 20px; - height: 20px; -} diff --git a/Client/src/Components/ProgressBars/index.jsx b/Client/src/Components/ProgressBars/index.jsx index 40e1a5a2c..51d70bb2c 100644 --- a/Client/src/Components/ProgressBars/index.jsx +++ b/Client/src/Components/ProgressBars/index.jsx @@ -32,21 +32,36 @@ const ProgressUpload = ({ error, }) => { const theme = useTheme(); + return ( .MuiStack-root > svg": { + fill: theme.palette.error.text, + width: "20px", + height: "20px", + }, + }, }} > {error ? ( @@ -54,10 +69,11 @@ const ProgressUpload = ({ ) : icon ? ( @@ -67,15 +83,21 @@ const ProgressUpload = ({ "" )} {error ? ( - + {error} ) : ( - - + + {error ? error : label} - {!error && size} + + {!error && size} + )} {!error ? ( - + - + {progress} % diff --git a/Client/src/Components/RadioButton/index.css b/Client/src/Components/RadioButton/index.css deleted file mode 100644 index 353bbdf55..000000000 --- a/Client/src/Components/RadioButton/index.css +++ /dev/null @@ -1,15 +0,0 @@ -.create-monitor-form .custom-radio-button.MuiFormControlLabel-root, -.configure-monitor .custom-radio-button.MuiFormControlLabel-root { - padding: 5px; - margin: -5px; - border-radius: var(--env-var-radius-1); -} -.create-monitor-form .custom-radio-button.MuiFormControlLabel-root:hover, -.configure-monitor .custom-radio-button.MuiFormControlLabel-root:hover { - background-color: var(--env-var-color-15); -} - -.custom-radio-button.MuiFormControlLabel-root .MuiButtonBase-root { - margin-right: var(--env-var-spacing-1); - padding: 0; -} diff --git a/Client/src/Components/Sidebar/index.css b/Client/src/Components/Sidebar/index.css index 4c253c781..b01562ac4 100644 --- a/Client/src/Components/Sidebar/index.css +++ b/Client/src/Components/Sidebar/index.css @@ -1,16 +1,8 @@ -aside .selected-path, -.sidebar-popup .selected-path, -aside .MuiListItemButton-root:hover { - background-color: var(--env-var-color-13); -} aside .MuiList-root svg { width: 20px; height: 20px; opacity: 0.9; } -aside .MuiList-root svg path { - stroke: var(--env-var-color-2); -} aside span.MuiTypography-root { font-size: var(--env-var-font-size-medium); line-height: 1; @@ -26,11 +18,6 @@ aside .MuiListSubheader-root { margin-bottom: 2px; opacity: 0.6; } -aside span.MuiTypography-root, -aside p.MuiTypography-root, -aside .MuiListSubheader-root { - color: var(--env-var-color-5); -} aside p.MuiTypography-root { font-size: var(--env-var-font-size-small); opacity: 0.8; @@ -47,82 +34,15 @@ aside .selected-path span.MuiTypography-root { aside .MuiCollapse-wrapperInner .MuiList-root > .MuiListItemButton-root { position: relative; } -aside .MuiDivider-root { - border-color: var(--env-var-color-6); -} -aside - .MuiCollapse-wrapperInner - .MuiList-root - > .MuiListItemButton-root::before { - content: ""; - position: absolute; - left: -7px; - top: 0; - height: 100%; - width: 100%; - border-left: solid 1px var(--env-var-color-5); - opacity: 0.2; -} -aside - .MuiCollapse-wrapperInner - .MuiList-root - > .MuiListItemButton-root:last-child::before { - height: 50%; -} aside .MuiCollapse-wrapperInner .MuiList-root svg, aside .MuiList-root .MuiListItemText-root + svg { width: 18px; height: 18px; } -aside .MuiCollapse-wrapperInner .MuiList-root > .MuiListItemButton-root::after { - content: ""; - position: absolute; - left: -8px; - top: 45%; - height: 3px; - width: 3px; - border-radius: 50%; - background-color: #d6d9dd; -} - -aside - .MuiCollapse-wrapperInner - .MuiList-root - > .selected-path.MuiListItemButton-root::after { - background-color: var(--env-var-color-2); - transform: scale(1.2); -} - -.sidebar-popup .MuiPaper-root { - 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: 3px; - 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; @@ -155,7 +75,7 @@ aside.collapsed .MuiAvatar-root + .MuiBox-root + .MuiIconButton-root { } aside .MuiListSubheader-root { - transition: all 200ms ease; + transition: padding 200ms ease; } @keyframes fadeIn { diff --git a/Client/src/Components/Sidebar/index.jsx b/Client/src/Components/Sidebar/index.jsx index 873ec8212..c372f8c66 100644 --- a/Client/src/Components/Sidebar/index.jsx +++ b/Client/src/Components/Sidebar/index.jsx @@ -19,7 +19,7 @@ import { useLocation, useNavigate } from "react-router"; import { useTheme } from "@emotion/react"; import { useDispatch, useSelector } from "react-redux"; import { clearAuthState } from "../../Features/Auth/authSlice"; -import { toggleSidebar } from "../../Features/UI/uiSlice"; +import { setMode, toggleSidebar } from "../../Features/UI/uiSlice"; import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice"; import Avatar from "../Avatar"; import LockSvg from "../../assets/icons/lock.svg?react"; @@ -40,6 +40,7 @@ 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 DotsVertical from "../../assets/icons/dots-vertical.svg?react"; import "./index.css"; @@ -85,8 +86,7 @@ function Sidebar() { const location = useLocation(); const dispatch = useDispatch(); const authState = useSelector((state) => state.auth); - const { sidebar } = useSelector((state) => state.ui); - let collapsed = sidebar.collapsed; + const collapsed = useSelector((state) => state.ui.sidebar.collapsed); const [open, setOpen] = useState({ Dashboard: false, Account: false }); const [anchorEl, setAnchorEl] = useState(null); const [popup, setPopup] = useState(); @@ -124,21 +124,38 @@ function Sidebar() { - - + + @@ -146,7 +163,7 @@ function Sidebar() { BlueWave Uptime @@ -158,20 +175,21 @@ function Sidebar() { top: 60, right: 0, transform: `translate(50%, 0)`, - backgroundColor: theme.palette.otherColors.fillGray, - border: `solid 1px ${theme.palette.otherColors.graishWhite}`, - p: "5px", + backgroundColor: theme.palette.background.fill, + border: 1, + borderColor: theme.palette.border.light, + p: theme.spacing(2.5), "& svg": { - width: theme.gap.ml, - height: theme.gap.ml, + width: theme.spacing(8), + height: theme.spacing(8), "& path": { - stroke: theme.palette.otherColors.bluishGray, + stroke: theme.palette.text.secondary, }, }, "&:focus": { outline: "none" }, "&:hover": { - backgroundColor: "#e3e3e3", - borderColor: theme.palette.otherColors.graishWhite, + backgroundColor: theme.palette.border.light, + borderColor: theme.palette.border.light, }, }} onClick={() => { @@ -192,15 +210,15 @@ function Sidebar() { component="div" id="nested-menu-subheader" sx={{ - pt: theme.gap.small, - px: collapsed ? theme.gap.xs : theme.gap.small, - backgroundColor: "transparent" + pt: theme.spacing(4), + px: collapsed ? theme.spacing(2) : theme.spacing(4), + backgroundColor: "transparent", }} > Menu } - sx={{ px: theme.gap.medium }} + sx={{ px: theme.spacing(6) }} > {menu.map((item) => item.path ? ( @@ -229,9 +247,9 @@ function Sidebar() { onClick={() => navigate(`/${item.path}`)} sx={{ height: "37px", - gap: theme.gap.small, - borderRadius: `${theme.shape.borderRadius}px`, - px: theme.gap.small, + gap: theme.spacing(4), + borderRadius: theme.shape.borderRadius, + px: theme.spacing(4), }} > {item.icon} @@ -266,9 +284,9 @@ function Sidebar() { onClick={(event) => openPopup(event, item.name)} sx={{ position: "relative", - gap: theme.gap.small, - borderRadius: `${theme.shape.borderRadius}px`, - px: theme.gap.small, + gap: theme.spacing(4), + borderRadius: theme.shape.borderRadius, + px: theme.spacing(4), }} > {item.icon} @@ -284,13 +302,27 @@ function Sidebar() { vertical: "top", horizontal: "right", }} - sx={{ ml: theme.gap.ml }} + slotProps={{ + paper: { + sx: { + mt: theme.spacing(-2), + ml: theme.spacing(1), + }, + }, + }} + MenuListProps={{ sx: { px: 1, py: 2 } }} + sx={{ + ml: theme.spacing(8), + "& .selected-path": { + backgroundColor: theme.palette.background.accent, + }, + }} > {item.nested.map((child) => { if ( child.name === "Team" && authState.user?.role && - !authState.user.role.includes("admin") + !authState.user.role.includes("superadmin") ) { return null; } @@ -308,10 +340,14 @@ function Sidebar() { closePopup(); }} sx={{ - gap: theme.gap.small, - borderRadius: `${theme.shape.borderRadius}px`, - pl: theme.gap.small, - mb: "1px", + gap: theme.spacing(4), + opacity: 0.9, + "& svg": { + "& path": { + stroke: theme.palette.other.icon, + strokeWidth: 1.1, + }, + }, }} > {child.icon} @@ -331,9 +367,9 @@ function Sidebar() { })) } sx={{ - gap: theme.gap.small, - borderRadius: `${theme.shape.borderRadius}px`, - px: theme.gap.small, + gap: theme.spacing(4), + borderRadius: theme.shape.borderRadius, + px: theme.spacing(4), }} > {item.icon} @@ -344,13 +380,13 @@ function Sidebar() { {item.nested.map((child) => { if ( child.name === "Team" && authState.user?.role && - !authState.user.role.includes("admin") + !authState.user.role.includes("superadmin") ) { return null; } @@ -365,9 +401,35 @@ function Sidebar() { key={child.path} onClick={() => navigate(`/${child.path}`)} sx={{ - gap: theme.gap.small, - borderRadius: `${theme.shape.borderRadius}px`, - pl: theme.gap.small, + gap: theme.spacing(4), + borderRadius: theme.shape.borderRadius, + pl: theme.spacing(4), + "&::before": { + content: `""`, + position: "absolute", + top: 0, + left: "-7px", + height: "100%", + borderLeft: 1, + borderLeftColor: theme.palette.other.line, + }, + "&:last-child::before": { + height: "50%", + }, + "&::after": { + content: `""`, + position: "absolute", + top: "45%", + left: "-8px", + height: "3px", + width: "3px", + borderRadius: "50%", + backgroundColor: theme.palette.other.line, + }, + "&.selected-path::after": { + backgroundColor: theme.palette.text.tertiary, + transform: "scale(1.2)", + }, }} > @@ -383,7 +445,7 @@ function Sidebar() { ) )} - + {/* other */} Other } - sx={{ px: theme.gap.medium }} + sx={{ px: theme.spacing(6) }} > {other.map((item) => ( {item.icon} @@ -453,10 +515,10 @@ function Sidebar() { direction="row" height="50px" alignItems="center" - py={theme.gap.small} - px={theme.gap.ml} - gap={theme.gap.xs} - borderRadius={`${theme.shape.borderRadius}px`} + py={theme.spacing(4)} + px={theme.spacing(8)} + gap={theme.spacing(2)} + borderRadius={theme.shape.borderRadius} > {collapsed ? ( <> @@ -483,47 +545,11 @@ function Sidebar() { - - - - - {authState.user?.firstName} {authState.user?.lastName} - - - {authState.user?.role} - - - - - - - Log out - - > ) : ( <> - + {authState.user?.firstName} {authState.user?.lastName} @@ -531,16 +557,99 @@ function Sidebar() { {authState.user?.role} - + openPopup(event, "logout")} > - + > )} + + {collapsed && ( + + + + {authState.user?.firstName} {authState.user?.lastName} + + + {authState.user?.role} + + + + )} + {collapsed && } + { + dispatch(setMode("light")); + closePopup(); + }} + > + Light + + { + dispatch(setMode("dark")); + closePopup(); + }} + > + Dark + + + + + Log out + + ); diff --git a/Client/src/Components/TabPanels/Account/PasswordPanel.jsx b/Client/src/Components/TabPanels/Account/PasswordPanel.jsx index 602816e6c..2de15450d 100644 --- a/Client/src/Components/TabPanels/Account/PasswordPanel.jsx +++ b/Client/src/Components/TabPanels/Account/PasswordPanel.jsx @@ -1,8 +1,8 @@ import TabPanel from "@mui/lab/TabPanel"; import React, { useState } from "react"; import { useTheme } from "@emotion/react"; -import { Box, Divider, Stack, Typography } from "@mui/material"; -import ButtonSpinner from "../../ButtonSpinner"; +import { Box, Stack, Typography } from "@mui/material"; +import LoadingButton from "@mui/lab/LoadingButton"; import Field from "../../Inputs/Field"; import { credentials } from "../../../Validation/validation"; import Alert from "../../Alert"; @@ -97,13 +97,20 @@ const PasswordPanel = () => { }; return ( - + @@ -157,19 +164,20 @@ const PasswordPanel = () => { - + > + Save + diff --git a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx index 7d18b48c5..885c795b1 100644 --- a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx +++ b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx @@ -1,9 +1,7 @@ import { useTheme } from "@emotion/react"; import { useRef, useState } from "react"; import TabPanel from "@mui/lab/TabPanel"; -import { Box, Divider, Modal, Stack, Typography } from "@mui/material"; -import ButtonSpinner from "../../ButtonSpinner"; -import Button from "../../Button"; +import { Box, Button, Divider, Modal, Stack, Typography } from "@mui/material"; import Avatar from "../../Avatar"; import Field from "../../Inputs/Field"; import ImageField from "../../Inputs/Image"; @@ -20,6 +18,7 @@ import { formatBytes } from "../../../Utils/fileUtils"; import { clearUptimeMonitorState } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; import { createToast } from "../../../Utils/toastUtils"; import { logger } from "../../../Utils/Logger"; +import LoadingButton from "@mui/lab/LoadingButton"; /** * ProfilePanel component displays a form for editing user profile information @@ -216,13 +215,20 @@ const ProfilePanel = () => { const isModalOpen = (name) => isOpen === name; return ( - + @@ -285,52 +291,48 @@ const ProfilePanel = () => { } sx={{ mr: "8px" }} /> - - + + Delete + + + Update + - + sx={{ px: theme.spacing(12) }} + > + Save + - - - + + + Delete account Note that deleting your account will remove all data from our @@ -338,12 +340,13 @@ const ProfilePanel = () => { setIsOpen("delete")} - sx={{ width: "fit-content", mt: theme.gap.small }} - /> - + > + Delete account + + {/* TODO - Update ModalPopup Component with @mui for reusability */} { disablePortal > { and all of your data will be deleted. Deleting your account is permanent and non-recoverable action. - - setIsOpen("")} - /> - + setIsOpen("")}> + Cancel + + + loading={isLoading} + > + Delete account + @@ -406,17 +415,22 @@ const ProfilePanel = () => { left: "50%", transform: "translate(-50%, -50%)", width: 450, - bgcolor: "white", - border: "solid 1px #f2f2f2", - borderRadius: `${theme.shape.borderRadius}px`, + bgcolor: theme.palette.background.main, + border: 1, + borderColor: theme.palette.border.light, + borderRadius: theme.shape.borderRadius, boxShadow: 24, - p: "30px", + p: theme.spacing(15), "&:focus": { outline: "none", }, }} > - + Upload Image { ) : ( "" )} - + - + > + Edit + + + Remove + { ? true : false } - /> + > + Update + diff --git a/Client/src/Components/TabPanels/Account/TeamPanel.jsx b/Client/src/Components/TabPanels/Account/TeamPanel.jsx index a0393977d..8c4630b19 100644 --- a/Client/src/Components/TabPanels/Account/TeamPanel.jsx +++ b/Client/src/Components/TabPanels/Account/TeamPanel.jsx @@ -2,6 +2,7 @@ import { useTheme } from "@emotion/react"; import TabPanel from "@mui/lab/TabPanel"; import { Box, + Button, ButtonGroup, Divider, IconButton, @@ -10,8 +11,6 @@ import { TextField, Typography, } from "@mui/material"; -import ButtonSpinner from "../../ButtonSpinner"; -import Button from "../../Button"; import { useEffect, useState } from "react"; import EditSvg from "../../../assets/icons/edit.svg?react"; import Field from "../../Inputs/Field"; @@ -22,6 +21,7 @@ import { useSelector } from "react-redux"; import BasicTable from "../../BasicTable"; import Remove from "../../../assets/icons/trash-bin.svg?react"; import Select from "../../Inputs/Select"; +import LoadingButton from "@mui/lab/LoadingButton"; /** * TeamPanel component manages the organization and team members, @@ -32,6 +32,12 @@ import Select from "../../Inputs/Select"; */ const TeamPanel = () => { + const roleMap = { + superadmin: "Super admin", + admin: "Admin", + user: "Team member", + }; + const theme = useTheme(); const { authToken, user } = useSelector((state) => state.auth); @@ -67,7 +73,14 @@ const TeamPanel = () => { useEffect(() => { let team = members; if (filter !== "all") - team = members.filter((member) => member.role.includes(filter)); + team = members.filter((member) => { + if (filter === "admin") { + return ( + member.role.includes("admin") || member.role.includes("superadmin") + ); + } + return member.role.includes(filter); + }); const data = { cols: [ @@ -78,6 +91,7 @@ const TeamPanel = () => { // { id: 4, name: "ACTION" }, ], rows: team?.map((member, idx) => { + const roles = member.role.map((role) => roleMap[role]).join(","); return { id: member._id, data: [ @@ -85,12 +99,10 @@ const TeamPanel = () => { id: idx, data: ( - + {member.firstName + " " + member.lastName} - + Created {new Date(member.createdAt).toLocaleDateString()} @@ -100,7 +112,7 @@ const TeamPanel = () => { { // TODO - Add select dropdown id: idx + 2, - data: member.role.includes("admin") ? "Administrator" : "Member", + data: roles, }, // FEATURE STILL TO BE IMPLEMENTED // { @@ -198,7 +210,18 @@ const TeamPanel = () => { }; return ( - + {/* FEATURE STILL TO BE IMPLEMENTED */} {/* @@ -229,7 +252,7 @@ const TeamPanel = () => { }, }} inputProps={{ - sx: { textAlign: "end", padding: theme.gap.small }, + sx: { textAlign: "end", padding: theme.spacing(4) }, }} /> { onClick={() => toggleEdit()} sx={{ minWidth: 0, - paddingX: theme.gap.small, - ml: orgStates.isEdit ? theme.gap.small : 0, + paddingX: theme.spacing(4), + ml: orgStates.isEdit ? theme.spacing(4) : 0, }} /> @@ -250,52 +273,47 @@ const TeamPanel = () => { component="form" noValidate spellCheck="false" - gap={theme.gap.large} + gap={theme.spacing(12)} > Team members setFilter("all")} - sx={{ - backgroundColor: - filter === "all" && theme.palette.otherColors.fillGray, - }} - /> + > + All + setFilter("admin")} - sx={{ - backgroundColor: - filter === "admin" && theme.palette.otherColors.fillGray, - }} - /> + > + Administrator + setFilter("user")} - sx={{ - backgroundColor: - filter === "user" && theme.palette.otherColors.fillGray, - }} - /> + > + Member + setIsOpen(true)} - /> + > + Invite a team member + { disablePortal > { When you add a new team member, they will get access to all monitors. @@ -360,28 +379,28 @@ const TeamPanel = () => { })) } items={[ - { _id: "admin", name: "admin" }, - { _id: "user", name: "user" }, + { _id: "admin", name: "Admin" }, + { _id: "user", name: "User" }, ]} /> - - + Cancel + + + > + Send invite + diff --git a/Client/src/Components/Tooltips/TooltipWithTail/TooltipWithTail.jsx b/Client/src/Components/Tooltips/TooltipWithTail/TooltipWithTail.jsx deleted file mode 100644 index 1b1fb8b4d..000000000 --- a/Client/src/Components/Tooltips/TooltipWithTail/TooltipWithTail.jsx +++ /dev/null @@ -1,45 +0,0 @@ -import "./tooltipWithTail.css"; -import React from "react"; -import { styled } from "@mui/material/styles"; -import Tooltip, { tooltipClasses } from "@mui/material/Tooltip"; -import HelpOutlineIcon from "@mui/icons-material/HelpOutline"; - -const CustomizedTooltip = styled( - ({ className, placement, arrow, ...props }) => ( - - ) -)(({ theme }) => ({ - [`& .${tooltipClasses.tooltip}`]: { - backgroundColor: "black", - }, - [`& .${tooltipClasses.arrow}`]: { - color: "black", - }, -})); - -function TooltipWithTail({ placement, arrow = false, title, text }) { - return ( - - - {title && {title}} - {text} - - } - className="tooltip-holder" - > - - - - ); -} - -export default TooltipWithTail; diff --git a/Client/src/Components/Tooltips/TooltipWithTail/tooltipWithTail.css b/Client/src/Components/Tooltips/TooltipWithTail/tooltipWithTail.css deleted file mode 100644 index 3efe67643..000000000 --- a/Client/src/Components/Tooltips/TooltipWithTail/tooltipWithTail.css +++ /dev/null @@ -1,49 +0,0 @@ -:root { - --env-var-color-1: #101828; - --env-var-color-2: #475467; - --env-var-color-3: #1570ef; - - --env-var-color-4: #d0d5dd; - --env-var-color-5: #344054; - --env-var-color-6: #eaecf0; - --env-var-color-7: #7f56d9; - --env-var-color-8: #fff; - --env-var-color-9: #f2f2f2; - --env-var-color-10: #175cd3; - - --env-var-radius-1: 4px; - --env-var-radius-2: 8px; - - --env-var-width-1: 100vw; - --env-var-width-2: 360px; - - --env-var-height-1: 100vh; - - --env-var-spacing-1: 12px; - --env-var-spacing-2: 24px; - --env-var-spacing-3: 32px; - --env-var-spacing-4: 40px; - - --env-var-font-size-small: 11px; - --env-var-font-size-medium: 13px; - --env-var-font-size-medium-plus: 14px; - --env-var-font-size-large: 16px; - --env-var-font-size-xlarge: 30px; - - --env-var-img-width-1: 20px; - - --env-var-default-1: 24px; - --env-var-default-2: 40px; -} - -.tooltip-title { - font-size: var(--env-var-font-size-medium); - font-weight: bold; - margin: 5px; -} - -.tooltip-description { - font-size: var(--env-var-font-size-medium); - color: var(--env-var-color-4); - margin: 5px; -} diff --git a/Client/src/Features/Auth/authSlice.js b/Client/src/Features/Auth/authSlice.js index fa1f0471e..f12758715 100644 --- a/Client/src/Features/Auth/authSlice.js +++ b/Client/src/Features/Auth/authSlice.js @@ -1,6 +1,7 @@ import { networkService } from "../../main"; import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import { jwtDecode } from "jwt-decode"; +import axios from "axios"; const initialState = { isLoading: false, @@ -57,9 +58,8 @@ export const update = createAsyncThunk( form.password && fd.append("password", form.password); form.newPassword && fd.append("newPassword", form.newPassword); if (form.file && form.file !== "") { - const imageResult = await networkService.get(form.file, { + const imageResult = await axios.get(form.file, { responseType: "blob", - baseURL: "", }); fd.append("profileImage", imageResult.data); } diff --git a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js index 22e2b3324..e32d673cc 100644 --- a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js +++ b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js @@ -28,14 +28,14 @@ export const createPageSpeed = createAsyncThunk( } ); -export const getPageSpeedByUserId = createAsyncThunk( - "pageSpeedMonitors/getPageSpeedByUserId", +export const getPageSpeedByTeamId = createAsyncThunk( + "pageSpeedMonitors/getPageSpeedByTeamId", async (token, thunkApi) => { const user = jwtDecode(token); try { - const res = await networkService.getMonitorsByUserId( + const res = await networkService.getMonitorsByTeamId( token, - user._id, + user.teamId, 25, ["pagespeed"], null, @@ -127,15 +127,15 @@ const pageSpeedMonitorSlice = createSlice({ // Monitors by userId // ***************************************************** - .addCase(getPageSpeedByUserId.pending, (state) => { + .addCase(getPageSpeedByTeamId.pending, (state) => { state.isLoading = true; }) - .addCase(getPageSpeedByUserId.fulfilled, (state, action) => { + .addCase(getPageSpeedByTeamId.fulfilled, (state, action) => { state.isLoading = false; state.success = action.payload.msg; state.monitors = action.payload.data; }) - .addCase(getPageSpeedByUserId.rejected, (state, action) => { + .addCase(getPageSpeedByTeamId.rejected, (state, action) => { state.isLoading = false; state.success = false; state.msg = action.payload diff --git a/Client/src/Features/UI/uiSlice.js b/Client/src/Features/UI/uiSlice.js index ebea20bf1..947055ddc 100644 --- a/Client/src/Features/UI/uiSlice.js +++ b/Client/src/Features/UI/uiSlice.js @@ -4,7 +4,7 @@ import { createSlice } from "@reduxjs/toolkit"; // Add more settings as needed (e.g., theme preferences, user settings) const initialState = { monitors: { - rowsPerPage: 5, + rowsPerPage: 10, }, team: { rowsPerPage: 5, @@ -12,6 +12,8 @@ const initialState = { sidebar: { collapsed: false, }, + mode: "light", + greeting: { index: 0, lastUpdate: null }, }; const uiSlice = createSlice({ @@ -19,13 +21,24 @@ const uiSlice = createSlice({ initialState, reducers: { setRowsPerPage: (state, action) => { - state[action.payload.table].rowsPerPage = action.payload.value; + const { table, value } = action.payload; + if (state[table]) { + state[table].rowsPerPage = value; + } }, toggleSidebar: (state) => { state.sidebar.collapsed = !state.sidebar.collapsed; }, + setMode: (state, action) => { + state.mode = action.payload; + }, + setGreeting(state, action) { + state.greeting.index = action.payload.index; + state.greeting.lastUpdate = action.payload.lastUpdate; + }, }, }); export default uiSlice.reducer; -export const { setRowsPerPage, toggleSidebar } = uiSlice.actions; +export const { setRowsPerPage, toggleSidebar, setMode, setGreeting } = + uiSlice.actions; diff --git a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js index 9ab7d385b..2c381812c 100644 --- a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js +++ b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js @@ -28,14 +28,34 @@ export const createUptimeMonitor = createAsyncThunk( } ); -export const getUptimeMonitorsByUserId = createAsyncThunk( - "montiors/getMonitorsByUserId", +export const getUptimeMonitorById = createAsyncThunk( + "monitors/getMonitorById", + async (data, thunkApi) => { + try { + const { authToken, monitorId } = data; + const res = await networkService.getMonitorByid(authToken, monitorId); + return res.data; + } catch (error) { + if (error.response && error.response.data) { + return thunkApi.rejectWithValue(error.response.data); + } + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); + } + } +); + +export const getUptimeMonitorsByTeamId = createAsyncThunk( + "monitors/getMonitorsByTeamId", async (token, thunkApi) => { const user = jwtDecode(token); try { - const res = await networkService.getMonitorsByUserId( + const res = await networkService.getMonitorsByTeamId( token, - user._id, + user.teamId, 25, ["http", "ping"], null, @@ -109,6 +129,46 @@ export const deleteUptimeMonitor = createAsyncThunk( } ); +export const pauseUptimeMonitor = createAsyncThunk( + "monitors/pauseMonitor", + async (data, thunkApi) => { + try { + const { authToken, monitorId } = data; + const res = await networkService.pauseMonitorById(authToken, monitorId); + return res.data; + } catch (error) { + if (error.response && error.response.data) { + return thunkApi.rejectWithValue(error.response.data); + } + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); + } + } +); + +export const deleteMonitorChecksByTeamId = createAsyncThunk( + "monitors/deleteChecksByTeamId", + async (data, thunkApi) => { + try { + const { authToken, teamId } = data; + const res = await networkService.deleteChecksByTeamId(authToken, teamId); + return res.data; + } catch (error) { + if (error.response && error.response.data) { + return thunkApi.rejectWithValue(error.response.data); + } + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); + } + } +); + const uptimeMonitorsSlice = createSlice({ name: "uptimeMonitors", initialState, @@ -126,15 +186,15 @@ const uptimeMonitorsSlice = createSlice({ // Monitors by userId // ***************************************************** - .addCase(getUptimeMonitorsByUserId.pending, (state) => { + .addCase(getUptimeMonitorsByTeamId.pending, (state) => { state.isLoading = true; }) - .addCase(getUptimeMonitorsByUserId.fulfilled, (state, action) => { + .addCase(getUptimeMonitorsByTeamId.fulfilled, (state, action) => { state.isLoading = false; state.success = action.payload.msg; state.monitors = action.payload.data; }) - .addCase(getUptimeMonitorsByUserId.rejected, (state, action) => { + .addCase(getUptimeMonitorsByTeamId.rejected, (state, action) => { state.isLoading = false; state.success = false; state.msg = action.payload @@ -160,7 +220,24 @@ const uptimeMonitorsSlice = createSlice({ ? action.payload.msg : "Failed to create uptime monitor"; }) - + // ***************************************************** + // Get Monitor By Id + // ***************************************************** + .addCase(getUptimeMonitorById.pending, (state) => { + state.isLoading = true; + }) + .addCase(getUptimeMonitorById.fulfilled, (state, action) => { + state.isLoading = false; + state.success = action.payload.success; + state.msg = action.payload.msg; + }) + .addCase(getUptimeMonitorById.rejected, (state, action) => { + state.isLoading = false; + state.success = false; + state.msg = action.payload + ? action.payload.msg + : "Failed to pause uptime monitor"; + }) // ***************************************************** // update Monitor // ***************************************************** @@ -197,6 +274,42 @@ const uptimeMonitorsSlice = createSlice({ state.msg = action.payload ? action.payload.msg : "Failed to delete uptime monitor"; + }) + // ***************************************************** + // Delete Monitor checks by Team ID + // ***************************************************** + .addCase(deleteMonitorChecksByTeamId.pending, (state) => { + state.isLoading = true; + }) + .addCase(deleteMonitorChecksByTeamId.fulfilled, (state, action) => { + state.isLoading = false; + state.success = action.payload.success; + state.msg = action.payload.msg; + }) + .addCase(deleteMonitorChecksByTeamId.rejected, (state, action) => { + state.isLoading = false; + state.success = false; + state.msg = action.payload + ? action.payload.msg + : "Failed to delete monitor checks"; + }) + // ***************************************************** + // Pause Monitor + // ***************************************************** + .addCase(pauseUptimeMonitor.pending, (state) => { + state.isLoading = true; + }) + .addCase(pauseUptimeMonitor.fulfilled, (state, action) => { + state.isLoading = false; + state.success = action.payload.success; + state.msg = action.payload.msg; + }) + .addCase(pauseUptimeMonitor.rejected, (state, action) => { + state.isLoading = false; + state.success = false; + state.msg = action.payload + ? action.payload.msg + : "Failed to pause uptime monitor"; }); }, }); diff --git a/Client/src/HOC/withAdminCheck.jsx b/Client/src/HOC/withAdminCheck.jsx index 30a767f36..097f224e9 100644 --- a/Client/src/HOC/withAdminCheck.jsx +++ b/Client/src/HOC/withAdminCheck.jsx @@ -10,7 +10,7 @@ const withAdminCheck = (WrappedComponent) => { useEffect(() => { networkService - .doesAdminExist() + .doesSuperAdminExist() .then((response) => { if (response.data.data === true) { navigate("/login"); @@ -20,7 +20,7 @@ const withAdminCheck = (WrappedComponent) => { logger.error(error); }); }, [navigate]); - return ; + return ; }; const wrappedComponentName = WrappedComponent.displayName || WrappedComponent.name || "Component"; diff --git a/Client/src/HOC/withAdminProp.jsx b/Client/src/HOC/withAdminProp.jsx new file mode 100644 index 000000000..6910ba5ea --- /dev/null +++ b/Client/src/HOC/withAdminProp.jsx @@ -0,0 +1,20 @@ +import { useSelector } from "react-redux"; + +const withAdminProp = (WrappedComponent) => { + const WithAdminProp = (props) => { + const { user } = useSelector((state) => state.auth); + const isAdmin = + (user?.role?.includes("admin") ?? false) || + (user?.role?.includes("superadmin") ?? false); + + return ; + }; + + const wrappedComponentName = + WrappedComponent.displayName || WrappedComponent.name || "Component"; + WithAdminProp.displayName = `WithAdminProp(${wrappedComponentName})`; + + return WithAdminProp; +}; + +export default withAdminProp; diff --git a/Client/src/Layouts/HomeLayout/index.css b/Client/src/Layouts/HomeLayout/index.css index e85a6362b..1434ff9d9 100644 --- a/Client/src/Layouts/HomeLayout/index.css +++ b/Client/src/Layouts/HomeLayout/index.css @@ -1,5 +1,5 @@ #root:has(.home-layout) { - background-color: var(--env-var-color-30); + background-color: var(--secondary-bg); } .home-layout { @@ -19,12 +19,6 @@ height: calc(100vh - var(--env-var-spacing-2) * 2); max-width: var(--env-var-side-bar-width); - - border: 1px solid var(--env-var-color-6); - border-radius: var(--env-var-radius-1); - background-color: var(--env-var-color-8); - - padding: var(--env-var-spacing-1) 0; } .home-layout > div { @@ -32,19 +26,11 @@ flex: 1; } -.home-layout > div:has(> [class*="fallback__"]) { - border: solid 1px var(--env-var-color-6); - border-radius: var(--env-var-radius-1); - border-style: dashed; - background-color: var(--env-var-color-8); - overflow: hidden; - position: relative; -} .home-layout > div:has(> [class*="fallback__"]) .background-pattern-svg { position: absolute; top: 0; left: 50%; - transform: translate(-50%, -33%); + transform: translate(-50%, -50%); z-index: 0; width: 100%; diff --git a/Client/src/Layouts/HomeLayout/index.jsx b/Client/src/Layouts/HomeLayout/index.jsx index 81cb2fd9c..92d763f0c 100644 --- a/Client/src/Layouts/HomeLayout/index.jsx +++ b/Client/src/Layouts/HomeLayout/index.jsx @@ -1,14 +1,19 @@ import Sidebar from "../../Components/Sidebar"; import { Outlet } from "react-router"; import { Box } from "@mui/material"; +import { useTheme } from "@emotion/react"; import "./index.css"; const HomeLayout = () => { + const theme = useTheme(); + return ( - - - + + + + + ); }; diff --git a/Client/src/Pages/Account/index.css b/Client/src/Pages/Account/index.css index 594b7b839..24b47d3e9 100644 --- a/Client/src/Pages/Account/index.css +++ b/Client/src/Pages/Account/index.css @@ -6,12 +6,6 @@ .account .MuiSelect-select { font-size: var(--env-var-font-size-medium); } -.account h1.MuiTypography-root, -.account p.MuiTypography-root, -.account input, -.account .MuiSelect-select { - color: var(--env-var-color-2); -} .account h1.MuiTypography-root { font-weight: 600; } @@ -19,10 +13,6 @@ padding: 0; margin-top: 50px; } -.account .MuiDivider-root, -.account .MuiButtonGroup-root button { - border-color: var(--env-var-color-16); -} .account button:not(.MuiIconButton-root) { height: 34px; } @@ -43,9 +33,6 @@ top: 100%; } -.account .MuiTable-root .MuiTableBody-root .MuiTableCell-root { - color: var(--env-var-color-25); -} .account .MuiTableBody-root .MuiTableCell-root { padding: var(--env-var-spacing-1-plus) var(--env-var-spacing-2); } diff --git a/Client/src/Pages/Account/index.jsx b/Client/src/Pages/Account/index.jsx index 2fed926b8..fe7b19655 100644 --- a/Client/src/Pages/Account/index.jsx +++ b/Client/src/Pages/Account/index.jsx @@ -25,23 +25,27 @@ const Account = ({ open = "profile" }) => { let tabList = ["Profile", "Password", "Team"]; const { user } = useSelector((state) => state.auth); - if (!user.role.includes("admin")) tabList = ["Profile", "Password"]; + const requiredRoles = ["superadmin", "admin"]; + const hideTeams = !requiredRoles.some((role) => user.role.includes(role)); + if (hideTeams) { + tabList = ["Profile", "Password"]; + } return ( @@ -52,15 +56,15 @@ const Account = ({ open = "profile" }) => { key={index} value={label.toLowerCase()} sx={{ - fontSize: "13px", - color: theme.palette.secondary.main, + fontSize: 13, + color: theme.palette.text.tertiary, textTransform: "none", minWidth: "fit-content", minHeight: 0, - paddingLeft: "0", - paddingY: theme.gap.small, + paddingLeft: 0, + paddingY: theme.spacing(4), fontWeight: 400, - marginRight: theme.gap.ml, + marginRight: theme.spacing(8), "&:focus": { outline: "none", }, @@ -71,7 +75,7 @@ const Account = ({ open = "profile" }) => { - {user.role.includes("admin") && } + {!hideTeams && } ); diff --git a/Client/src/Pages/Auth/CheckEmail.jsx b/Client/src/Pages/Auth/CheckEmail.jsx index 4a0daf68a..88bc1983c 100644 --- a/Client/src/Pages/Auth/CheckEmail.jsx +++ b/Client/src/Pages/Auth/CheckEmail.jsx @@ -1,11 +1,10 @@ -import { Box, Stack, Typography } from "@mui/material"; +import { Box, Button, Stack, Typography } from "@mui/material"; import { useEffect, useState } from "react"; import { useTheme } from "@emotion/react"; import { useDispatch } from "react-redux"; import { useNavigate } from "react-router"; import { createToast } from "../../Utils/toastUtils"; import { forgotPassword } from "../../Features/Auth/authSlice"; -import Button from "../../Components/Button"; import background from "../../assets/Images/background_pattern_decorative.png"; import EmailIcon from "../../assets/icons/email.svg?react"; import Logo from "../../assets/icons/bwu-icon.svg?react"; @@ -85,7 +84,21 @@ const CheckEmail = () => { }; return ( - + { BlueWave Uptime @@ -104,31 +117,31 @@ const CheckEmail = () => { maxWidth={600} flex={1} justifyContent="center" - px={{ xs: theme.gap.large, lg: theme.gap.xl }} - pb={theme.gap.xl} + px={{ xs: theme.spacing(12), lg: theme.spacing(20) }} + pb={theme.spacing(20)} mx="auto" sx={{ "& > .MuiStack-root": { border: 1, - borderRadius: theme.shape.borderRadius, - borderColor: theme.palette.otherColors.graishWhite, - backgroundColor: theme.palette.otherColors.white, + borderRadius: theme.spacing(5), + borderColor: theme.palette.border.light, + backgroundColor: theme.palette.background.main, padding: { - xs: theme.gap.large, - sm: theme.gap.xl, + xs: theme.spacing(12), + sm: theme.spacing(20), }, }, }} > Check your email - + We sent a password reset link to{" "} {email || "username@email.com"} @@ -136,15 +149,17 @@ const CheckEmail = () => { - + > + Open email app + + Didn't receive the email?{" "} { - + Go back to — diff --git a/Client/src/Pages/Auth/ForgotPassword.jsx b/Client/src/Pages/Auth/ForgotPassword.jsx index 06afb05fa..338da645c 100644 --- a/Client/src/Pages/Auth/ForgotPassword.jsx +++ b/Client/src/Pages/Auth/ForgotPassword.jsx @@ -7,10 +7,10 @@ import { useEffect, useState } from "react"; import { credentials } from "../../Validation/validation"; import { useNavigate } from "react-router-dom"; import Field from "../../Components/Inputs/Field"; -import ButtonSpinner from "../../Components/ButtonSpinner"; import Logo from "../../assets/icons/bwu-icon.svg?react"; import Key from "../../assets/icons/key.svg?react"; import background from "../../assets/Images/background_pattern_decorative.png"; +import LoadingButton from "@mui/lab/LoadingButton"; import "./index.css"; const ForgotPassword = () => { @@ -87,7 +87,21 @@ const ForgotPassword = () => { }; return ( - + { BlueWave Uptime @@ -106,24 +120,24 @@ const ForgotPassword = () => { maxWidth={600} flex={1} justifyContent="center" - px={{ xs: theme.gap.large, lg: theme.gap.xl }} - pb={theme.gap.xl} + px={{ xs: theme.spacing(12), lg: theme.spacing(20) }} + pb={theme.spacing(20)} mx="auto" sx={{ "& > .MuiStack-root": { border: 1, - borderRadius: theme.shape.borderRadius, - borderColor: theme.palette.otherColors.graishWhite, - backgroundColor: theme.palette.otherColors.white, + borderRadius: theme.spacing(5), + borderColor: theme.palette.border.light, + backgroundColor: theme.palette.background.main, padding: { - xs: theme.gap.large, - sm: theme.gap.xl, + xs: theme.spacing(12), + sm: theme.spacing(20), }, }, }} > @@ -134,39 +148,46 @@ const ForgotPassword = () => { No worries, we'll send you reset instructions. - - - - - + + + + Send instructions + - + Go back to — diff --git a/Client/src/Pages/Auth/Login.jsx b/Client/src/Pages/Auth/Login.jsx index b962fbb9a..c8946b7fb 100644 --- a/Client/src/Pages/Auth/Login.jsx +++ b/Client/src/Pages/Auth/Login.jsx @@ -1,12 +1,11 @@ import { useState, useEffect, useRef } from "react"; import { useNavigate } from "react-router-dom"; -import { Box, Stack, Typography } from "@mui/material"; +import { Box, Button, Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import { credentials } from "../../Validation/validation"; import { login } from "../../Features/Auth/authSlice"; import { useDispatch, useSelector } from "react-redux"; import { createToast } from "../../Utils/toastUtils"; -import Button from "../../Components/Button"; import { networkService } from "../../main"; import Field from "../../Components/Inputs/Field"; import background from "../../assets/Images/background_pattern_decorative.png"; @@ -30,7 +29,7 @@ const LandingPage = ({ onContinue }) => { return ( <> @@ -40,17 +39,22 @@ const LandingPage = ({ onContinue }) => { } + variant="outlined" + color="info" onClick={onContinue} sx={{ width: "100%", "& svg": { - mr: theme.gap.small, + mr: theme.spacing(4), + "& path": { + stroke: theme.palette.other.icon, + }, }, }} - /> + > + + Continue with Email + @@ -64,6 +68,11 @@ const LandingPage = ({ onContinue }) => { "noreferrer" ); }} + sx={{ + "&:hover": { + color: theme.palette.text.tertiary, + }, + }} > Terms of Service {" "} @@ -77,6 +86,11 @@ const LandingPage = ({ onContinue }) => { "noreferrer" ); }} + sx={{ + "&:hover": { + color: theme.palette.text.tertiary, + }, + }} > Privacy Policy. @@ -114,12 +128,15 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => { return ( <> - + Log In Enter your email address - + { } + variant="outlined" + color="info" onClick={onBack} sx={{ - mb: theme.gap.medium, - px: theme.gap.ml, + px: theme.spacing(5), "& svg.MuiSvgIcon-root": { - mr: theme.gap.xs, + mr: theme.spacing(3), }, }} props={{ tabIndex: -1 }} - /> + > + + Back + + > + Continue + > @@ -204,12 +223,16 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => { return ( <> - + Log In Enter your password - + { } + variant="outlined" + color="info" onClick={onBack} sx={{ - mb: theme.gap.medium, - px: theme.gap.ml, + px: theme.spacing(5), "& svg.MuiSvgIcon-root": { - mr: theme.gap.xs, + mr: theme.spacing(3), }, }} props={{ tabIndex: -1 }} - /> + > + + Back + + > + Continue + - - + + Forgot password? @@ -301,7 +339,7 @@ const Login = () => { return; } networkService - .doesAdminExist() + .doesSuperAdminExist() .then((response) => { if (response.data.data === false) { navigate("/register"); @@ -393,7 +431,21 @@ const Login = () => { }; return ( - + { BlueWave Uptime @@ -412,18 +464,18 @@ const Login = () => { maxWidth={600} flex={1} justifyContent="center" - px={{ xs: theme.gap.large, lg: theme.gap.xl }} - pb={theme.gap.xl} + px={{ xs: theme.spacing(12), lg: theme.spacing(20) }} + pb={theme.spacing(20)} mx="auto" sx={{ "& > .MuiStack-root": { border: 1, - borderRadius: theme.shape.borderRadius, - borderColor: theme.palette.otherColors.graishWhite, - backgroundColor: theme.palette.otherColors.white, + borderRadius: theme.spacing(5), + borderColor: theme.palette.border.light, + backgroundColor: theme.palette.background.main, padding: { - xs: theme.gap.large, - sm: theme.gap.xl, + xs: theme.spacing(12), + sm: theme.spacing(20), }, }, }} @@ -450,13 +502,14 @@ const Login = () => { ) )} - + Don't have an account? — { navigate("/register"); }} diff --git a/Client/src/Pages/Auth/NewPasswordConfirmed.jsx b/Client/src/Pages/Auth/NewPasswordConfirmed.jsx index be6aeb9b5..7be6d325d 100644 --- a/Client/src/Pages/Auth/NewPasswordConfirmed.jsx +++ b/Client/src/Pages/Auth/NewPasswordConfirmed.jsx @@ -1,10 +1,9 @@ -import { Box, Stack, Typography } from "@mui/material"; +import { Box, Button, Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router"; import { useDispatch } from "react-redux"; import { clearAuthState } from "../../Features/Auth/authSlice"; import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice"; -import Button from "../../Components/Button"; import background from "../../assets/Images/background_pattern_decorative.png"; import ConfirmIcon from "../../assets/icons/confirm-icon.svg?react"; import Logo from "../../assets/icons/bwu-icon.svg?react"; @@ -22,7 +21,21 @@ const NewPasswordConfirmed = () => { }; return ( - + { BlueWave Uptime @@ -41,51 +54,54 @@ const NewPasswordConfirmed = () => { maxWidth={600} flex={1} justifyContent="center" - px={{ xs: theme.gap.large, lg: theme.gap.xl }} - pb={theme.gap.xl} + px={{ xs: theme.spacing(12), lg: theme.spacing(20) }} + pb={theme.spacing(20)} mx="auto" sx={{ "& > .MuiStack-root": { border: 1, - borderRadius: theme.shape.borderRadius, - borderColor: theme.palette.otherColors.graishWhite, - backgroundColor: theme.palette.otherColors.white, + borderRadius: theme.spacing(5), + borderColor: theme.palette.border.light, + backgroundColor: theme.palette.background.main, padding: { - xs: theme.gap.large, - sm: theme.gap.xl, + xs: theme.spacing(12), + sm: theme.spacing(20), }, }, }} > Password reset - + Your password has been successfully reset. Click below to log in magically. navigate("/monitors")} sx={{ width: "100%", maxWidth: 400, }} - /> + > + Continue + - + Go back to — diff --git a/Client/src/Pages/Auth/Register/Register.jsx b/Client/src/Pages/Auth/Register/Register.jsx index 4680c1821..e454b355f 100644 --- a/Client/src/Pages/Auth/Register/Register.jsx +++ b/Client/src/Pages/Auth/Register/Register.jsx @@ -2,7 +2,7 @@ import PropTypes from "prop-types"; import { useState, useEffect, useRef } from "react"; import { useNavigate } from "react-router-dom"; import { useTheme } from "@emotion/react"; -import { Box, Stack, Typography } from "@mui/material"; +import { Box, Button, Stack, Typography } from "@mui/material"; import { useDispatch } from "react-redux"; import { credentials } from "../../../Validation/validation"; import { createToast } from "../../../Utils/toastUtils"; @@ -13,49 +13,53 @@ 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 Check from "../../../Components/Check/Check"; -import Button from "../../../Components/Button"; import Field from "../../../Components/Inputs/Field"; import { networkService } from "../../../main"; import "../index.css"; -import { logger } from "../../../Utils/Logger"; /** * Displays the initial landing page. * * @param {Object} props - * @param {boolean} props.isAdmin - Whether the user is creating and admin account + * @param {boolean} props.isSuperAdmin - Whether the user is creating and admin account * @param {Function} props.onContinue - Callback function to handle "Continue with Email" button click. * @returns {JSX.Element} */ -const LandingPage = ({ isAdmin, onSignup }) => { +const LandingPage = ({ isSuperAdmin, onSignup }) => { const theme = useTheme(); return ( <> Sign Up - Create your {isAdmin ? "admin " : ""}account to get started. + Create your {isSuperAdmin ? "Super admin " : ""}account to get + started. } + variant="outlined" + color="info" onClick={onSignup} sx={{ width: "100%", "& svg": { - mr: theme.gap.small, + mr: theme.spacing(4), + "& path": { + stroke: theme.palette.other.icon, + }, }, }} - /> + > + + Sign up with Email + @@ -69,6 +73,11 @@ const LandingPage = ({ isAdmin, onSignup }) => { "noreferrer" ); }} + sx={{ + "&:hover": { + color: theme.palette.text.tertiary, + }, + }} > Terms of Service {" "} @@ -82,6 +91,11 @@ const LandingPage = ({ isAdmin, onSignup }) => { "noreferrer" ); }} + sx={{ + "&:hover": { + color: theme.palette.text.tertiary, + }, + }} > Privacy Policy. @@ -93,7 +107,7 @@ const LandingPage = ({ isAdmin, onSignup }) => { }; LandingPage.propTypes = { - isAdmin: PropTypes.bool, + isSuperAdmin: PropTypes.bool, onSignup: PropTypes.func, }; @@ -120,13 +134,22 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => { return ( <> - + Sign Up Enter your personal details - + { error={errors.firstName} ref={inputRef} /> - - + + { onChange={onChange} error={errors.lastName} /> - + } + variant="outlined" + color="info" onClick={onBack} sx={{ - px: theme.gap.ml, + px: theme.spacing(5), "& svg.MuiSvgIcon-root": { - mr: theme.gap.xs, + mr: theme.spacing(3), }, }} props={{ tabIndex: -1 }} - /> + > + + Back + + > + Continue + > @@ -211,13 +243,22 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => { return ( <> - + Sign Up Enter your email address - + { error={errors.email} ref={inputRef} /> - + } + variant="outlined" + color="info" onClick={onBack} sx={{ - px: theme.gap.ml, + px: theme.spacing(5), "& svg.MuiSvgIcon-root": { - mr: theme.gap.xs, + mr: theme.spacing(3), }, }} props={{ tabIndex: -1 }} - /> + > + + Back + + > + Continue + > @@ -292,13 +336,28 @@ const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => { return ( <> - + Sign Up Create your password - - + + { error={errors.password} ref={inputRef} /> - - + + { onChange={onChange} error={errors.confirm} /> - + { } + variant="outlined" + color="info" onClick={onBack} sx={{ - px: theme.gap.ml, + px: theme.spacing(5), "& svg.MuiSvgIcon-root": { - mr: theme.gap.xs, + mr: theme.spacing(3), }, }} props={{ tabIndex: -1 }} - /> + > + + Back + + > + Continue + > @@ -422,7 +489,7 @@ StepThree.propTypes = { onBack: PropTypes.func, }; -const Register = ({ isAdmin }) => { +const Register = ({ isSuperAdmin }) => { const dispatch = useDispatch(); const navigate = useNavigate(); const { token } = useParams(); @@ -443,6 +510,7 @@ const Register = ({ isAdmin }) => { password: "", confirm: "", role: [], + teamId: "", }); const [errors, setErrors] = useState({}); const [step, setStep] = useState(0); @@ -452,15 +520,16 @@ const Register = ({ isAdmin }) => { if (token !== undefined) { try { const res = await networkService.verifyInvitationToken(token); - const { role, email } = res.data.data; - setForm({ ...form, email, role }); + const invite = res.data.data; + const { role, email, teamId } = invite; + setForm({ ...form, email, role, teamId }); } catch (error) { - logger.error(error); + navigate("/register", { replace: true }); } } }; fetchInvite(); - }, [token, form]); + }, []); /** * Validates the form data against the validation schema. @@ -493,7 +562,6 @@ const Register = ({ isAdmin }) => { const handleStepOne = async (e) => { e.preventDefault(); - let error = validateForm({ firstName: form.firstName, lastName: form.lastName, @@ -525,7 +593,10 @@ const Register = ({ isAdmin }) => { const handleStepThree = async (e) => { e.preventDefault(); - let registerForm = { ...form, role: isAdmin ? ["admin"] : form.role }; + let registerForm = { + ...form, + role: isSuperAdmin ? ["superadmin"] : form.role, + }; let error = validateForm(registerForm, { context: { password: form.password }, }); @@ -580,7 +651,21 @@ const Register = ({ isAdmin }) => { }; return ( - + { BlueWave Uptime @@ -599,24 +684,27 @@ const Register = ({ isAdmin }) => { maxWidth={600} flex={1} justifyContent="center" - px={{ xs: theme.gap.large, lg: theme.gap.xl }} - pb={theme.gap.xl} + px={{ xs: theme.spacing(12), lg: theme.spacing(20) }} + pb={theme.spacing(20)} mx="auto" sx={{ "& > .MuiStack-root": { border: 1, - borderRadius: theme.shape.borderRadius, - borderColor: theme.palette.otherColors.graishWhite, - backgroundColor: theme.palette.otherColors.white, + borderRadius: theme.spacing(5), + borderColor: theme.palette.border.light, + backgroundColor: theme.palette.background.main, padding: { - xs: theme.gap.large, - sm: theme.gap.xl, + xs: theme.spacing(12), + sm: theme.spacing(20), }, }, }} > {step === 0 ? ( - setStep(1)} /> + setStep(1)} + /> ) : step === 1 ? ( { "" )} - + Already have an account? — { navigate("/login"); }} - sx={{ userSelect: "none" }} + sx={{ userSelect: "none", color: theme.palette.primary.main }} > Log In @@ -664,6 +752,6 @@ const Register = ({ isAdmin }) => { ); }; Register.propTypes = { - isAdmin: PropTypes.bool, + isSuperAdmin: PropTypes.bool, }; export default Register; diff --git a/Client/src/Pages/Auth/SetNewPassword.jsx b/Client/src/Pages/Auth/SetNewPassword.jsx index fab448ffa..a01e9bd51 100644 --- a/Client/src/Pages/Auth/SetNewPassword.jsx +++ b/Client/src/Pages/Auth/SetNewPassword.jsx @@ -8,11 +8,11 @@ import { useState } from "react"; import { credentials } from "../../Validation/validation"; import { useNavigate } from "react-router-dom"; import Check from "../../Components/Check/Check"; -import ButtonSpinner from "../../Components/ButtonSpinner"; import Field from "../../Components/Inputs/Field"; import LockIcon from "../../assets/icons/lock-button-icon.svg?react"; import background from "../../assets/Images/background_pattern_decorative.png"; import Logo from "../../assets/icons/bwu-icon.svg?react"; +import LoadingButton from "@mui/lab/LoadingButton"; import "./index.css"; const SetNewPassword = () => { @@ -104,7 +104,21 @@ const SetNewPassword = () => { }; return ( - + { BlueWave Uptime @@ -123,24 +137,24 @@ const SetNewPassword = () => { maxWidth={600} flex={1} justifyContent="center" - px={{ xs: theme.gap.large, lg: theme.gap.xl }} - pb={theme.gap.large} + px={{ xs: theme.spacing(12), lg: theme.spacing(20) }} + pb={theme.spacing(12)} mx="auto" sx={{ "& > .MuiStack-root": { border: 1, - borderRadius: theme.shape.borderRadius, - borderColor: theme.palette.otherColors.graishWhite, - backgroundColor: theme.palette.otherColors.white, + borderRadius: theme.spacing(5), + borderColor: theme.palette.border.light, + backgroundColor: theme.palette.background.main, padding: { - xs: theme.gap.large, - sm: theme.gap.xl, + xs: theme.spacing(12), + sm: theme.spacing(20), }, }, }} > @@ -151,8 +165,21 @@ const SetNewPassword = () => { Your new password must be different to previously used passwords. - - + + { onChange={handleChange} error={errors.password} /> - - + + { onChange={handleChange} error={errors.confirm} /> - - + + @@ -234,21 +266,24 @@ const SetNewPassword = () => { /> - + > + Reset password + - + Go back to — navigate("/login")} sx={{ userSelect: "none" }} > diff --git a/Client/src/Pages/Auth/index.css b/Client/src/Pages/Auth/index.css index 979ae191c..33280e8f4 100644 --- a/Client/src/Pages/Auth/index.css +++ b/Client/src/Pages/Auth/index.css @@ -5,24 +5,14 @@ .auth h1 { font-size: var(--env-var-font-size-xlarge); font-weight: 600; - color: var(--env-var-color-3); } .auth button:not(.MuiIconButton-root), .auth p, -.auth .field input, .auth span { font-size: var(--env-var-font-size-medium-plus); } -.auth p { - color: var(--env-var-color-2-light); -} -.auth .field h3.MuiTypography-root, -.auth .field input { - color: var(--env-var-color-5); -} .auth p + span { - color: var(--env-var-color-3); opacity: 0.8; cursor: pointer; transition: opacity 300ms ease-in; @@ -34,7 +24,6 @@ transition: all 200ms; } .auth p > span:not(.email-sent-to):hover { - color: var(--env-var-color-2); text-underline-offset: 4px; } .auth p + span:hover { @@ -86,11 +75,12 @@ background-repeat: no-repeat; } -.forgot-password-page h1, -.check-email-page h1, -.password-confirmed-page h1, -.set-new-password-page h1 { - font-size: 22px; +.auth .field { + position: relative; +} +.auth .input-error { + position: absolute; + top: calc(100% - 2px); } @media (max-width: 800px) { diff --git a/Client/src/Pages/Incidents/IncidentTable/index.jsx b/Client/src/Pages/Incidents/IncidentTable/index.jsx index 7a77208f8..aa80b35ce 100644 --- a/Client/src/Pages/Incidents/IncidentTable/index.jsx +++ b/Client/src/Pages/Incidents/IncidentTable/index.jsx @@ -21,14 +21,16 @@ import { useSelector } from "react-redux"; import { networkService } from "../../../main"; import { StatusLabel } from "../../../Components/Label"; import { logger } from "../../../Utils/Logger"; +import { useTheme } from "@emotion/react"; const IncidentTable = ({ monitors, selectedMonitor, filter }) => { + const theme = useTheme(); const { authToken, user } = useSelector((state) => state.auth); const [checks, setChecks] = useState([]); const [checksCount, setChecksCount] = useState(0); const [paginationController, setPaginationController] = useState({ page: 0, - rowsPerPage: 12, + rowsPerPage: 14, }); useEffect(() => { @@ -46,9 +48,9 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { try { let res; if (selectedMonitor === "0") { - res = await networkService.getChecksByUser( + res = await networkService.getChecksByTeam( authToken, - user._id, + user.teamId, "desc", null, null, @@ -107,26 +109,31 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { next: ArrowForwardRoundedIcon, }} {...item} - sx={{ - "&:focus": { - outline: "none", - }, - }} /> )} /> ); } + let sharedStyles = { + border: 1, + borderColor: theme.palette.border.light, + borderRadius: theme.shape.borderRadius, + backgroundColor: theme.palette.background.main, + p: theme.spacing(30), + }; + return ( <> {checks?.length === 0 && selectedMonitor === "0" ? ( - - No incidents recorded yet. + + + No incidents recorded yet. + ) : checks?.length === 0 ? ( - - + + The monitor you have selected has no recorded incidents yet. @@ -139,6 +146,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { Monitor Name Status Date & Time + Status Code Message @@ -159,7 +167,10 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => { {new Date(check.createdAt).toLocaleString()} - {check.statusCode} + + {check.statusCode ? check.statusCode : "N/A"} + + {check.message} ); })} diff --git a/Client/src/Pages/Incidents/index.css b/Client/src/Pages/Incidents/index.css index 286552d22..73c7b93e0 100644 --- a/Client/src/Pages/Incidents/index.css +++ b/Client/src/Pages/Incidents/index.css @@ -1,7 +1,3 @@ -.incidents h1.MuiTypography-root, -.incidents p.MuiTypography-root { - color: var(--env-var-color-5); -} .incidents h1.MuiTypography-root { font-size: var(--env-var-font-size-large); font-weight: 600; @@ -12,19 +8,4 @@ } .incidents button.MuiButtonBase-root { height: 34px; - border-color: var(--env-var-color-16); -} -.incidents .MuiPagination-root { - background-color: var(--env-var-color-8); -} -.incidents > .MuiBox-root { - background-color: var(--env-var-color-8); - border: solid 1px var(--env-var-color-6); - border-radius: var(--env-var-radius-1); - padding: 60px; -} - -/* remove later */ -.incidents .MuiSelect-select { - padding: 0 10px; } diff --git a/Client/src/Pages/Incidents/index.jsx b/Client/src/Pages/Incidents/index.jsx index 34768f7f9..5d756298e 100644 --- a/Client/src/Pages/Incidents/index.jsx +++ b/Client/src/Pages/Incidents/index.jsx @@ -1,7 +1,12 @@ import { useState, useEffect } from "react"; import { useSelector } from "react-redux"; -import { ButtonGroup, Stack, Skeleton, Typography } from "@mui/material"; -import Button from "../../Components/Button"; +import { + ButtonGroup, + Stack, + Skeleton, + Typography, + Button, +} from "@mui/material"; import { networkService } from "../../main"; import { useTheme } from "@emotion/react"; import Select from "../../Components/Inputs/Select"; @@ -18,7 +23,7 @@ const SkeletonLayout = () => { return ( <> - + { useEffect(() => { const fetchMonitors = async () => { setLoading(true); - const res = await networkService.getMonitorsByUserId( + const res = await networkService.getMonitorsByTeamId( authState.authToken, - authState.user._id, + authState.user.teamId, 1, null, null, null, null ); - // Reduce to a lookup object for 0(1) lookup if (res.data && res.data.data.length > 0) { const monitorLookup = res.data.data.reduce((acc, monitor) => { @@ -79,13 +83,17 @@ const Incidents = () => { }; return ( - + {loading ? ( ) : ( <> - - + + Incidents for { value={selectedMonitor} onChange={handleSelect} items={Object.values(monitors)} + sx={{ + backgroundColor: theme.palette.background.main, + }} /> - + setFilter("all")} - sx={{ - backgroundColor: - filter === "all" && theme.palette.otherColors.fillGray, - }} - /> + > + All + setFilter("down")} - sx={{ - backgroundColor: - filter === "down" && theme.palette.otherColors.fillGray, - }} - /> + > + Down + setFilter("resolve")} - sx={{ - backgroundColor: - filter === "resolve" && theme.palette.otherColors.fillGray, - }} - /> + > + Cannot Resolve + .MuiStack-root { - border: solid 1px var(--env-var-color-6); - border-radius: var(--env-var-radius-1); - background-color: var(--env-var-color-8); -} diff --git a/Client/src/Pages/Integrations/index.jsx b/Client/src/Pages/Integrations/index.jsx index 278754fea..d292d1aa2 100644 --- a/Client/src/Pages/Integrations/index.jsx +++ b/Client/src/Pages/Integrations/index.jsx @@ -1,6 +1,5 @@ import PropTypes from "prop-types"; -import { Stack, Typography, Grid } from "@mui/material"; -import Button from "../../Components/Button"; +import { Stack, Typography, Grid, Button } from "@mui/material"; import { useTheme } from "@emotion/react"; import Discord from "../../assets/icons/discord-icon.svg?react"; import Slack from "../../assets/icons/slack-icon.svg?react"; @@ -25,13 +24,17 @@ const IntegrationsComponent = ({ icon, header, info, onClick }) => { {icon} - + {header} { + > + Add + ); @@ -108,12 +113,21 @@ const Integrations = () => { ]; return ( - + Integrations - + Connect BlueWave Uptime to your favorite service. - + {integrations.map((integration, index) => ( { + const theme = useTheme(); + const [values, setValues] = useState({ repeat: 1, date: dayjs(), startTime: dayjs(), duration: "60", unit: "minutes", - friendlyName: "", + displayName: "", AddMonitors: "", }); const [errors, setErrors] = useState({}); @@ -82,7 +84,7 @@ const CreateNewMaintenanceWindow = () => { startTime: values.startTime.format("HH:mm"), duration: values.duration, unit: values.unit, - friendlyName: values.friendlyName, + displayName: values.displayName, addMonitors: values.AddMonitors, }; @@ -159,7 +161,11 @@ const CreateNewMaintenanceWindow = () => { { title: "Duration", component: ( - + { ), }, { - title: "Friendly name", + title: "Display name", component: ( handleChange(e, "friendlyName")} - error={errors.friendlyName} + value={values.displayName} + onChange={(e) => handleChange(e, "displayName")} + error={errors.displayName} /> ), }, @@ -195,7 +201,7 @@ const CreateNewMaintenanceWindow = () => { { sx={{ width: "fit-content", fontSize: "var(--env-var-font-size-small)", - borderBottom: "1px dashed var(--env-var-color-3)", + borderBottom: `1px dashed ${theme.palette.primary.main}`, paddingBottom: "4px", }} > @@ -221,26 +227,25 @@ const CreateNewMaintenanceWindow = () => { return ( - + } - /> + > + + Back + {directory.title} @@ -252,14 +257,14 @@ const CreateNewMaintenanceWindow = () => { {configOptions.map((item, index) => ( @@ -271,26 +276,19 @@ const CreateNewMaintenanceWindow = () => { - + > + Cancel + + + Create + diff --git a/Client/src/Pages/Maintenance/index.css b/Client/src/Pages/Maintenance/index.css index f26a48767..46ba8b0fa 100644 --- a/Client/src/Pages/Maintenance/index.css +++ b/Client/src/Pages/Maintenance/index.css @@ -13,7 +13,7 @@ } .maintenance-title { - color: var(--env-var-color-2); + color: var(--secondary-color-light); font-size: 16px; font-weight: bold; margin-top: 40px; @@ -22,7 +22,7 @@ .checklist-item { font-size: 13px; - color: var(--env-var-color-2); + color: var(--secondary-color-light); display: flex; margin-bottom: 10px; } diff --git a/Client/src/Pages/Maintenance/index.jsx b/Client/src/Pages/Maintenance/index.jsx index e9c28d905..5c23a789c 100644 --- a/Client/src/Pages/Maintenance/index.jsx +++ b/Client/src/Pages/Maintenance/index.jsx @@ -1,9 +1,26 @@ +import { Box } from "@mui/material"; +import { useTheme } from "@emotion/react"; import Fallback from "../../Components/Fallback"; import "./index.css"; -const Maintenance = () => { +const Maintenance = ({ isAdmin }) => { + const theme = useTheme(); + return ( - + [class*="fallback__"])': { + position: "relative", + border: 1, + borderColor: theme.palette.border.light, + borderRadius: theme.shape.borderRadius, + borderStyle: "dashed", + backgroundColor: theme.palette.background.main, + overflow: "hidden", + }, + }} + > { "Stop sending alerts in maintenance windows", ]} link="/maintenance/create" + isAdmin={isAdmin} /> - + ); }; diff --git a/Client/src/Pages/Monitors/Configure/index.css b/Client/src/Pages/Monitors/Configure/index.css index ec140887c..23b6af57a 100644 --- a/Client/src/Pages/Monitors/Configure/index.css +++ b/Client/src/Pages/Monitors/Configure/index.css @@ -1,52 +1,17 @@ .configure-monitor h1.MuiTypography-root { font-size: var(--env-var-font-size-large-plus); - color: var(--env-var-color-1); font-weight: 600; } -.configure-monitor h2.MuiTypography-root { - font-size: var(--env-var-font-size-large); - font-weight: 600; -} -.configure-monitor h2.MuiTypography-root, -.configure-monitor .config-box p.MuiTypography-root, -.configure-monitor .MuiSelect-select { - color: var(--env-var-color-5); -} -.configure-monitor .MuiModal-root h2.MuiTypography-root { - color: var(--env-var-color-1); -} -.configure-monitor p.MuiTypography-root, -.configure-monitor .MuiSelect-select { +.configure-monitor p.MuiTypography-root { font-size: var(--env-var-font-size-medium); } -.configure-monitor h6.MuiTypography-root { - opacity: 0.6; -} -.configure-monitor h6.MuiTypography-root, .configure-monitor h1.MuiTypography-root + span.MuiTypography-root { font-size: var(--env-var-font-size-small-plus); } -.configure-monitor button.MuiButtonBase-root { - font-size: var(--env-var-font-size-medium); -} .configure-monitor button.MuiButtonBase-root { height: var(--env-var-height-2); } -.configure-monitor .config-box { - 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 { - 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; } @@ -54,25 +19,3 @@ position: absolute; top: 100%; } -.configure-monitor .MuiInputBase-root:has(> .Mui-disabled) { - background-color: var(--env-var-color-13); -} - -.MuiInputBase-root:has(#monitor-interval) { - height: 34px; - width: 100%; -} -#monitor-interval { - padding: 0 10px; - height: 100%; - display: flex; - align-items: center; -} -.MuiInputBase-root:not(.Mui-focused):has(#monitor-interval):hover fieldset { - border-color: var(--env-var-color-29); -} - -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 bd2a26df0..483a2fd79 100644 --- a/Client/src/Pages/Monitors/Configure/index.jsx +++ b/Client/src/Pages/Monitors/Configure/index.jsx @@ -2,22 +2,27 @@ import { useNavigate, useParams } from "react-router"; import { useTheme } from "@emotion/react"; import { useDispatch, useSelector } from "react-redux"; import { useEffect, useState } from "react"; -import { Box, Modal, Skeleton, Stack, Typography } from "@mui/material"; +import { Box, Button, Modal, Stack, Typography } from "@mui/material"; import { monitorValidation } from "../../../Validation/validation"; import { createToast } from "../../../Utils/toastUtils"; import { logger } from "../../../Utils/Logger"; +import { ConfigBox } from "../styled"; import { updateUptimeMonitor, - getUptimeMonitorsByUserId, + pauseUptimeMonitor, + getUptimeMonitorById, + getUptimeMonitorsByTeamId, deleteUptimeMonitor, } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; -import Button from "../../../Components/Button"; import Field from "../../../Components/Inputs/Field"; import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline"; +import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded"; import Select from "../../../Components/Inputs/Select"; import Checkbox from "../../../Components/Inputs/Checkbox"; import Breadcrumbs from "../../../Components/Breadcrumbs"; import PulseDot from "../../../Components/Animated/PulseDot"; +import SkeletonLayout from "./skeleton"; +import LoadingButton from "@mui/lab/LoadingButton"; import "./index.css"; /** @@ -34,54 +39,6 @@ const parseUrl = (url) => { } }; -/** - * Renders a skeleton layout. - * - * @returns {JSX.Element} - */ -const SkeletonLayout = () => { - const theme = useTheme(); - - return ( - <> - - - - - - - - - - - - - - - - - - - - > - ); -}; - /** * Configure page displays monitor configurations and allows for editing actions. * @component @@ -92,11 +49,11 @@ const Configure = () => { const theme = useTheme(); const dispatch = useDispatch(); const { user, authToken } = useSelector((state) => state.auth); - const { monitors } = useSelector((state) => state.uptimeMonitors); + const { isLoading } = useSelector((state) => state.uptimeMonitors); const [monitor, setMonitor] = useState({}); const [errors, setErrors] = useState({}); const { monitorId } = useParams(); - + console.log(monitor); const idMap = { "monitor-url": "url", "monitor-name": "name", @@ -106,15 +63,25 @@ const Configure = () => { }; useEffect(() => { - const data = monitors.find((monitor) => monitor._id === monitorId); - if (!data) { - logger.error("Error fetching monitor of id: " + monitorId); - navigate("/not-found", { replace: true }); - } - setMonitor({ - ...data, - }); - }, [monitorId, authToken, monitors, navigate]); + const fetchMonitor = async () => { + try { + const action = await dispatch( + getUptimeMonitorById({ authToken, monitorId }) + ); + + if (getUptimeMonitorById.fulfilled.match(action)) { + const monitor = action.payload.data; + setMonitor(monitor); + } else if (getUptimeMonitorById.rejected.match(action)) { + throw new Error(action.error.message); + } + } catch (error) { + logger.error("Error fetching monitor of id: " + monitorId); + navigate("/not-found", { replace: true }); + } + }; + fetchMonitor(); + }, [monitorId, authToken, navigate]); const handleChange = (event, name) => { let { value, id } = event.target; @@ -170,6 +137,23 @@ const Configure = () => { } }; + const handlePause = async () => { + try { + const action = await dispatch( + pauseUptimeMonitor({ authToken, monitorId }) + ); + if (pauseUptimeMonitor.fulfilled.match(action)) { + const monitor = action.payload.data; + setMonitor(monitor); + } else if (pauseUptimeMonitor.rejected.match(action)) { + throw new Error(action.error.message); + } + } catch (error) { + logger.error("Error pausing monitor: " + monitorId); + createToast({ body: "Failed to pause monitor" }); + } + }; + const handleSubmit = async (event) => { event.preventDefault(); const action = await dispatch( @@ -177,7 +161,7 @@ const Configure = () => { ); if (action.meta.requestStatus === "fulfilled") { createToast({ body: "Monitor updated successfully!" }); - dispatch(getUptimeMonitorsByUserId(authToken)); + dispatch(getUptimeMonitorsByTeamId(authToken)); } else { createToast({ body: "Failed to update monitor." }); } @@ -206,11 +190,9 @@ const Configure = () => { const parsedUrl = parseUrl(monitor?.url); const protocol = parsedUrl?.protocol?.replace(":", "") || ""; - let loading = Object.keys(monitor).length === 0; - return ( - - {loading ? ( + + {Object.keys(monitor).length === 0 ? ( ) : ( <> @@ -225,20 +207,25 @@ const Configure = () => { component="form" noValidate spellCheck="false" - gap={theme.gap.large} + gap={theme.spacing(12)} flex={1} > - + {parsedUrl?.host ? ( - + {parsedUrl.host || "..."} ) : ( @@ -246,11 +233,11 @@ const Configure = () => { )} Your site is {monitor?.status ? "up" : "down"}. @@ -262,45 +249,51 @@ const Configure = () => { ml: "auto", }} > - } + + onClick={handlePause} + > + {monitor?.isActive ? ( + <> + + Pause + > + ) : ( + <> + + Resume + > + )} + setIsOpen(true)} - /> + > + Remove + - + General settings - + Here you can select the URL of the host, together with the type of monitor. - + { { error={errors["name"]} /> - - + + Incident notifications - + When there is an incident, notify users. - - + + When there is a new incident, { {monitor?.notifications?.some( (notification) => notification.type === "emails" ) ? ( - + { value="" onChange={() => logger.warn("disabled")} /> - + You can separate multiple emails with a comma @@ -384,16 +373,12 @@ const Configure = () => { "" )} - - + + Advanced settings - + { items={frequencies} /> - + - + > + Save + > @@ -422,41 +410,60 @@ const Configure = () => { disablePortal > - + Do you really want to delete this monitor? - + Once deleted, this monitor cannot be retrieved. setIsOpen(false)} - /> - + > + Cancel + + + Delete + diff --git a/Client/src/Pages/Monitors/Configure/skeleton.jsx b/Client/src/Pages/Monitors/Configure/skeleton.jsx new file mode 100644 index 000000000..08eb071cf --- /dev/null +++ b/Client/src/Pages/Monitors/Configure/skeleton.jsx @@ -0,0 +1,52 @@ +import { Box, Skeleton, Stack } from "@mui/material"; +import { useTheme } from "@emotion/react"; + +/** + * Renders a skeleton layout. + * + * @returns {JSX.Element} + */ +const SkeletonLayout = () => { + const theme = useTheme(); + + return ( + <> + + + + + + + + + + + + + + + + + + + + > + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.css b/Client/src/Pages/Monitors/CreateMonitor/index.css index 2a1b5a2a6..ca74e83e1 100644 --- a/Client/src/Pages/Monitors/CreateMonitor/index.css +++ b/Client/src/Pages/Monitors/CreateMonitor/index.css @@ -1,23 +1,11 @@ .create-monitor h1.MuiTypography-root { font-size: var(--env-var-font-size-large-plus); - color: var(--env-var-color-1); font-weight: 500; } -.create-monitor h2.MuiTypography-root { - font-size: var(--env-var-font-size-large); - font-weight: 600; -} .create-monitor p.MuiTypography-root, .create-monitor button.MuiButtonBase-root { font-size: var(--env-var-font-size-medium); } -.create-monitor h2.MuiTypography-root { - color: var(--env-var-color-5); -} -.create-monitor h6.MuiTypography-root, -.create-monitor p.MuiTypography-root { - color: var(--env-var-color-2); -} .create-monitor h6.MuiTypography-root, .create-monitor .MuiBox-root .field + p.MuiTypography-root { font-size: var(--env-var-font-size-small-plus); @@ -26,11 +14,11 @@ .create-monitor button.MuiButtonBase-root { height: 34px; } + .create-monitor .error-container { position: relative; } .create-monitor .error-container p.MuiTypography-root.input-error { - color: var(--env-var-color-24); opacity: 0.8; position: absolute; top: 0; @@ -42,30 +30,9 @@ position: absolute; top: 100%; } -.create-monitor .config-box { - flex-direction: row; - border: 1px solid var(--env-var-color-16); - border-radius: var(--env-var-radius-1); - background-color: var(--env-var-color-8); -} -.create-monitor .config-box > .MuiBox-root, -.create-monitor .config-box > .MuiStack-root { - flex: 1; - padding: 24px 30px; - padding-bottom: 40px; -} -.create-monitor .config-box > *:first-child { - flex: 0.7; - border-right: solid 1px var(--env-var-color-16); -} .create-monitor .MuiStack-root .MuiButtonGroup-root button { font-size: var(--env-var-font-size-small); height: 28px; padding: 8px; } - -body:has(.create-monitor) .select-dropdown .MuiMenuItem-root, -.create-monitor .select-wrapper .select-component > .MuiSelect-select { - text-transform: lowercase; -} diff --git a/Client/src/Pages/Monitors/CreateMonitor/index.jsx b/Client/src/Pages/Monitors/CreateMonitor/index.jsx index 05e343e77..5147236e2 100644 --- a/Client/src/Pages/Monitors/CreateMonitor/index.jsx +++ b/Client/src/Pages/Monitors/CreateMonitor/index.jsx @@ -1,19 +1,19 @@ -import "./index.css"; import { useState } from "react"; -import RadioButton from "../../../Components/RadioButton"; -import Button from "../../../Components/Button"; -import { Box, ButtonGroup, Stack, Typography } from "@mui/material"; +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 { useTheme } from "@emotion/react"; +import { createToast } from "../../../Utils/toastUtils"; +import { logger } from "../../../Utils/Logger"; +import { ConfigBox } from "../styled"; +import Radio from "../../../Components/Inputs/Radio"; import Field from "../../../Components/Inputs/Field"; import Select from "../../../Components/Inputs/Select"; import Checkbox from "../../../Components/Inputs/Checkbox"; -import { createToast } from "../../../Utils/toastUtils"; import Breadcrumbs from "../../../Components/Breadcrumbs"; -import { logger } from "../../../Utils/Logger"; +import "./index.css"; const CreateMonitor = () => { const MS_PER_MINUTE = 60000; @@ -33,7 +33,7 @@ const CreateMonitor = () => { const [monitor, setMonitor] = useState({ url: "", name: "", - type: "", + type: "http", notifications: [], interval: 1, }); @@ -118,6 +118,7 @@ const CreateMonitor = () => { form = { ...form, description: form.name, + teamId: user.teamId, userId: user._id, notifications: monitor.notifications, }; @@ -143,7 +144,14 @@ const CreateMonitor = () => { ]; return ( - + { onSubmit={handleCreateMonitor} noValidate spellCheck="false" - gap={theme.gap.large} - mt={theme.gap.medium} + gap={theme.spacing(12)} + mt={theme.spacing(6)} > Create your{" "} @@ -171,15 +179,15 @@ const CreateMonitor = () => { monitor - + General settings - + Here you can select the URL of the host, together with the type of monitor. - + { { error={errors["name"]} /> - - + + Checks to perform - + You can always add or remove checks after adding your site. - - - + + { {monitor.type === "http" ? ( setHttps(true)} - sx={{ - backgroundColor: - https && theme.palette.otherColors.fillGray, - }} - /> + > + HTTPS + setHttps(false)} - sx={{ - backgroundColor: - !https && theme.palette.otherColors.fillGray, - }} - /> + > + HTTP + ) : ( "" )} - { /> {errors["type"] ? ( - + {errors["type"]} @@ -264,15 +272,15 @@ const CreateMonitor = () => { "" )} - - + + Incident notifications - + When there is an incident, notify users. - + When there is a new incident, { {monitor.notifications.some( (notification) => notification.type === "emails" ) ? ( - + { value="" onChange={() => logger.warn("disabled")} /> - + You can separate multiple emails with a comma @@ -318,12 +326,12 @@ const CreateMonitor = () => { "" )} - - + + Advanced settings - + { onChange={(event) => handleChange(event, "interval")} items={frequencies} /> - {/* TODO */} - {/* - handleChange(event, "retries", setAdvancedSettings) - } - /> - - handleChange(event, "codes", setAdvancedSettings) - } - /> - - handleChange(event, "redirects", setAdvancedSettings) - } - /> */} - - {/* TODO */} - {/* - - Proxy settings - - } - rightLayout={ - - handleCheck("enabled", setProxy)} - /> - handleChange(event, "protocol", setProxy)} - /> - handleChange(event, "address", setProxy)} - /> - handleChange(event, "proxy_port", setProxy)} - /> - - } - /> */} + + > + Create monitor + diff --git a/Client/src/Pages/Monitors/Details/PaginationTable/index.jsx b/Client/src/Pages/Monitors/Details/PaginationTable/index.jsx index ad8248ab0..d7b91d09b 100644 --- a/Client/src/Pages/Monitors/Details/PaginationTable/index.jsx +++ b/Client/src/Pages/Monitors/Details/PaginationTable/index.jsx @@ -18,8 +18,10 @@ import { StatusLabel } from "../../../../Components/Label"; import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded"; import { logger } from "../../../../Utils/Logger"; +import { useTheme } from "@emotion/react"; const PaginationTable = ({ monitorId, dateRange }) => { + const theme = useTheme(); const { authToken } = useSelector((state) => state.auth); const [checks, setChecks] = useState([]); const [checksCount, setChecksCount] = useState(0); @@ -85,15 +87,6 @@ const PaginationTable = ({ monitorId, dateRange }) => { next: ArrowForwardRoundedIcon, }} {...item} - sx={{ - "&:focus": { - outline: "none", - }, - "& .MuiTouchRipple-root": { - pointerEvents: "none", - display: "none", - }, - }} /> )} /> @@ -108,6 +101,7 @@ const PaginationTable = ({ monitorId, dateRange }) => { Status Date & Time + Status Code Message @@ -127,7 +121,10 @@ const PaginationTable = ({ monitorId, dateRange }) => { {new Date(check.createdAt).toLocaleString()} - {check.statusCode} + + {check.statusCode ? check.statusCode : "N/A"} + + {check.message} ); })} diff --git a/Client/src/Pages/Monitors/Details/index.css b/Client/src/Pages/Monitors/Details/index.css index 127b7377a..59ffc5d85 100644 --- a/Client/src/Pages/Monitors/Details/index.css +++ b/Client/src/Pages/Monitors/Details/index.css @@ -1,25 +1,13 @@ .monitor-details h1.MuiTypography-root { font-size: var(--env-var-font-size-large-plus); - color: var(--env-var-color-1); font-weight: 600; } .monitor-details h2.MuiTypography-root { font-size: var(--env-var-font-size-large); } -.monitor-details h2.MuiTypography-root{ +.monitor-details h2.MuiTypography-root { font-weight: 600; } -.monitor-details h2.MuiTypography-root, -.monitor-details h4.MuiTypography-root { - color: var(--env-var-color-5); -} -.monitor-details h6.MuiTypography-root { - color: var(--env-var-color-3); -} -.monitor-details button.MuiButtonBase-root, -.monitor-details h4.MuiTypography-root { - font-size: var(--env-var-font-size-medium); -} .monitor-details button.MuiButtonBase-root { height: var(--env-var-height-2); line-height: 1; @@ -28,28 +16,3 @@ .monitor-details p.MuiTypography-root span.MuiTypography-root { font-size: var(--env-var-font-size-small-plus); } -.monitor-details p.MuiTypography-root { - color: var(--env-var-color-2); -} - -.monitor-details .stat-box { - flex: 1 20%; - min-width: 100px; - border: solid 1px; - border-radius: var(--env-var-radius-1); - padding: var(--env-var-spacing-1) 16px; - background-color: var(--env-var-color-8); -} -.monitor-details h6.MuiTypography-root { - font-size: var(--env-var-font-size-medium-plus); -} -.monitor-details h6.MuiTypography-root span.MuiTypography-root { - font-style: italic; - font-size: var(--env-var-font-size-medium); - color: var(--env-var-color-5); - opacity: 0.7; -} -.monitor-details button.MuiButtonBase-root, -.monitor-details .stat-box { - border-color: var(--env-var-color-16); -} diff --git a/Client/src/Pages/Monitors/Details/index.jsx b/Client/src/Pages/Monitors/Details/index.jsx index 98904587e..616f18657 100644 --- a/Client/src/Pages/Monitors/Details/index.jsx +++ b/Client/src/Pages/Monitors/Details/index.jsx @@ -1,6 +1,13 @@ -import { useEffect, useState, useCallback } from "react"; import PropTypes from "prop-types"; -import { Box, Skeleton, Stack, Typography, useTheme } from "@mui/material"; +import { useEffect, useState, useCallback } from "react"; +import { + Box, + Button, + Skeleton, + Stack, + Typography, + useTheme, +} from "@mui/material"; import { useSelector } from "react-redux"; import { useNavigate, useParams } from "react-router-dom"; import { networkService } from "../../../main"; @@ -11,7 +18,6 @@ import { } from "../../../Utils/timeUtils"; import MonitorDetailsAreaChart from "../../../Components/Charts/MonitorDetailsAreaChart"; import ButtonGroup from "@mui/material/ButtonGroup"; -import Button from "../../../Components/Button"; import SettingsIcon from "../../../assets/icons/settings-bold.svg?react"; import PaginationTable from "./PaginationTable"; import Breadcrumbs from "../../../Components/Breadcrumbs"; @@ -19,12 +25,41 @@ import PulseDot from "../../../Components/Animated/PulseDot"; import "./index.css"; const StatBox = ({ title, value }) => { + const theme = useTheme(); return ( - - + + {title} - + {value} @@ -47,8 +82,8 @@ const SkeletonLayout = () => { return ( <> - - + + { variant="rounded" width="50%" height={18} - sx={{ mt: theme.gap.small }} + sx={{ mt: theme.spacing(4) }} /> { @@ -82,7 +117,7 @@ const SkeletonLayout = () => { { - + @@ -110,7 +145,8 @@ const SkeletonLayout = () => { * Details page component displaying monitor details and related information. * @component */ -const DetailsPage = () => { +const DetailsPage = ({ isAdmin }) => { + const theme = useTheme(); const [monitor, setMonitor] = useState({}); const { monitorId } = useParams(); const { authToken } = useSelector((state) => state.auth); @@ -142,6 +178,9 @@ const DetailsPage = () => { useEffect(() => { const fetchCertificate = async () => { + if (monitor?.type !== "http") { + return; + } try { const res = await networkService.getCertificateExpiry( authToken, @@ -153,9 +192,8 @@ const DetailsPage = () => { } }; fetchCertificate(); - }, [authToken, monitorId]); + }, [authToken, monitorId, monitor]); - const theme = useTheme(); let loading = Object.keys(monitor).length === 0; return ( @@ -169,26 +207,33 @@ const DetailsPage = () => { { name: "details", path: `/monitors/${monitorId}` }, ]} /> - - + + - + {monitor.url?.replace(/^https?:\/\//, "") || "..."} - + Your site is {monitor?.status ? "up" : "down"}. @@ -196,34 +241,31 @@ const DetailsPage = () => { Checking every {formatDurationRounded(monitor?.interval)}. - - } - onClick={() => navigate(`/monitors/configure/${monitorId}`)} - sx={{ - ml: "auto", - alignSelf: "flex-end", - backgroundColor: "#f4f4f4", - px: theme.gap.medium, - "& svg": { - mr: "6px", - }, - }} - /> + {isAdmin && ( + navigate(`/monitors/configure/${monitorId}`)} + sx={{ + ml: "auto", + alignSelf: "flex-end", + px: theme.spacing(5), + "& svg": { + mr: theme.spacing(3), + "& path": { + stroke: theme.palette.other.icon, + }, + }, + }} + > + Configure + + )} { - + Response Times setDateRange("day")} - sx={{ - backgroundColor: - dateRange === "day" && - theme.palette.otherColors.fillGray, - }} - /> + > + Day + setDateRange("week")} - sx={{ - backgroundColor: - dateRange === "week" && - theme.palette.otherColors.fillGray, - }} - /> + > + Week + setDateRange("month")} - sx={{ - backgroundColor: - dateRange === "month" && - theme.palette.otherColors.fillGray, - }} - /> + > + Month + { /> - - History + + + History + @@ -339,4 +378,7 @@ const DetailsPage = () => { ); }; +DetailsPage.propTypes = { + isAdmin: PropTypes.bool, +}; export default DetailsPage; diff --git a/Client/src/Pages/Monitors/Home/StatusBox.jsx b/Client/src/Pages/Monitors/Home/StatusBox.jsx new file mode 100644 index 000000000..08b2e88d9 --- /dev/null +++ b/Client/src/Pages/Monitors/Home/StatusBox.jsx @@ -0,0 +1,112 @@ +import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; +import { Box, Stack, Typography } from "@mui/material"; +import Arrow from "../../../assets/icons/top-right-arrow.svg?react"; +import Background from "../../../assets/Images/background-grid.svg?react"; +import ClockSnooze from "../../../assets/icons/clock-snooze.svg?react"; + +const StatusBox = ({ title, value }) => { + const theme = useTheme(); + + let sharedStyles = { + position: "absolute", + right: 8, + opacity: 0.5, + "& svg path": { stroke: theme.palette.other.icon }, + }; + + let color; + let icon; + if (title === "up") { + color = theme.palette.success.main; + icon = ( + + + + ); + } else if (title === "down") { + color = theme.palette.error.text; + icon = ( + + + + ); + } else if (title === "paused") { + color = theme.palette.warning.main; + icon = ( + + + + ); + } + + return ( + + + + + + {title} + + {icon} + + {value} + + # + + + + ); +}; + +StatusBox.propTypes = { + title: PropTypes.oneOf(["up", "down", "paused"]).isRequired, + value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, +}; + +export default StatusBox; diff --git a/Client/src/Pages/Monitors/Home/actionsMenu.jsx b/Client/src/Pages/Monitors/Home/actionsMenu.jsx new file mode 100644 index 000000000..7d342f5d7 --- /dev/null +++ b/Client/src/Pages/Monitors/Home/actionsMenu.jsx @@ -0,0 +1,227 @@ +import { useState } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { useTheme } from "@emotion/react"; +import { useNavigate } from "react-router-dom"; +import { createToast } from "../../../Utils/toastUtils"; +import { + Button, + IconButton, + Menu, + MenuItem, + Modal, + Stack, + Typography, +} from "@mui/material"; +import { + deleteUptimeMonitor, + getUptimeMonitorsByTeamId, +} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; +import Settings from "../../../assets/icons/settings-bold.svg?react"; +import PropTypes from "prop-types"; + +const ActionsMenu = ({ monitor, isAdmin }) => { + const [anchorEl, setAnchorEl] = useState(null); + const [actions, setActions] = useState({}); + const [isOpen, setIsOpen] = useState(false); + const dispatch = useDispatch(); + const theme = useTheme(); + const authState = useSelector((state) => state.auth); + + const handleRemove = async (event) => { + event.preventDefault(); + event.stopPropagation(); + let monitor = { _id: actions.id }; + const action = await dispatch( + deleteUptimeMonitor({ authToken: authState.authToken, monitor }) + ); + if (action.meta.requestStatus === "fulfilled") { + setIsOpen(false); // close modal + dispatch(getUptimeMonitorsByTeamId(authState.authToken)); + createToast({ body: "Monitor deleted successfully." }); + } else { + createToast({ body: "Failed to delete monitor." }); + } + }; + + const openMenu = (event, id, url) => { + event.preventDefault(); + event.stopPropagation(); + setAnchorEl(event.currentTarget); + setActions({ id: id, url: url }); + }; + + const openRemove = (e) => { + closeMenu(e); + setIsOpen(true); + }; + + const closeMenu = (e) => { + e.stopPropagation(); + setAnchorEl(null); + }; + + const navigate = useNavigate(); + return ( + <> + { + event.stopPropagation(); + openMenu( + event, + monitor._id, + monitor.type === "ping" ? null : monitor.url + ); + }} + sx={{ + "&:focus": { + outline: "none", + }, + "& svg path": { + stroke: theme.palette.other.icon, + }, + }} + > + + + + closeMenu(e)} + slotProps={{ + paper: { + sx: { + "& ul": { p: theme.spacing(2.5) }, + "& li": { m: 0 }, + "& li:last-of-type": { + color: theme.palette.error.text, + }, + }, + }, + }} + > + {actions.url !== null ? ( + { + closeMenu(e); + e.stopPropagation(); + window.open(actions.url, "_blank", "noreferrer"); + }} + > + Open site + + ) : ( + "" + )} + { + e.stopPropagation(); + navigate(`/monitors/${actions.id}`); + }} + > + Details + + {/* TODO - pass monitor id to Incidents page */} + Incidents + {isAdmin && ( + { + e.stopPropagation(); + + navigate(`/monitors/configure/${actions.id}`); + }} + > + Configure + + )} + {isAdmin && ( + { + e.stopPropagation(); + openRemove(e); + }} + > + Remove + + )} + + { + e.stopPropagation(); + setIsOpen(false); + }} + > + + + Do you really want to delete this monitor? + + + Once deleted, this monitor cannot be retrieved. + + + { + e.stopPropagation(); + setIsOpen(false); + }} + > + Cancel + + { + e.stopPropagation(e); + handleRemove(e); + }} + > + Delete + + + + + > + ); +}; + +ActionsMenu.propTypes = { + monitor: PropTypes.shape({ + _id: PropTypes.string, + url: PropTypes.string, + type: PropTypes.string, + }).isRequired, + isAdmin: PropTypes.bool, +}; + +export default ActionsMenu; diff --git a/Client/src/Pages/Monitors/Home/fallback.jsx b/Client/src/Pages/Monitors/Home/fallback.jsx new file mode 100644 index 000000000..270f23f29 --- /dev/null +++ b/Client/src/Pages/Monitors/Home/fallback.jsx @@ -0,0 +1,45 @@ +import { Button, Stack, Typography } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import { useNavigate } from "react-router-dom"; +import PropTypes from "prop-types"; + +const Fallback = ({ isAdmin }) => { + const theme = useTheme(); + const navigate = useNavigate(); + + return ( + + No monitors found + + It looks like you don’t have any monitors set up yet. + + {isAdmin && ( + { + navigate("/monitors/create"); + }} + sx={{ mt: theme.spacing(12) }} + > + Create your first monitor + + )} + + ); +}; + +Fallback.propTypes = { + isAdmin: PropTypes.bool, +}; + +export default Fallback; diff --git a/Client/src/Pages/Monitors/Home/host.jsx b/Client/src/Pages/Monitors/Home/host.jsx new file mode 100644 index 000000000..d450a53f4 --- /dev/null +++ b/Client/src/Pages/Monitors/Home/host.jsx @@ -0,0 +1,60 @@ +import { Box, Typography } from "@mui/material"; +import PropTypes from "prop-types"; +/** + * Host component. + * This subcomponent receives a params object and displays the host details. + * + * @component + * @param {Object} params - An object containing the following properties: + * @param {string} params.title - The name of the host. + * @param {string} params.percentageColor - The color of the percentage text. + * @param {number} params.percentage - The percentage to display. + * @returns {React.ElementType} Returns a div element with the host details. + */ +const Host = ({ params }) => { + return ( + + + {params.title} + + + {params.percentage}% + + {params.url} + + ); +}; + +Host.propTypes = { + params: PropTypes.shape({ + title: PropTypes.string, + percentageColor: PropTypes.string, + percentage: PropTypes.number, + }).isRequired, +}; + +export default Host; diff --git a/Client/src/Pages/Monitors/Home/index.css b/Client/src/Pages/Monitors/Home/index.css new file mode 100644 index 000000000..42ba2e270 --- /dev/null +++ b/Client/src/Pages/Monitors/Home/index.css @@ -0,0 +1,30 @@ +.monitors h1.MuiTypography-root { + font-size: var(--env-var-font-size-large-plus); + font-weight: 500; +} +.monitors h2.MuiTypography-root { + font-size: var(--env-var-font-size-large); + font-weight: 500; +} +.monitors h1.MuiTypography-root + p.MuiTypography-root { + font-size: var(--env-var-font-size-medium-plus); +} +.monitors .MuiStack-root > button:not(.MuiIconButton-root) { + font-size: var(--env-var-font-size-medium); + height: var(--env-var-height-2); + min-width: fit-content; +} + +.current-monitors-counter { + display: flex; + justify-content: center; + align-items: center; + padding: 5px; + min-width: 25px; + min-height: 25px; + border-radius: 50%; + font-size: var(--env-var-font-size-medium); + font-weight: 500; + margin-left: 10px; + line-height: 0.8; +} diff --git a/Client/src/Pages/Monitors/Home/index.jsx b/Client/src/Pages/Monitors/Home/index.jsx new file mode 100644 index 000000000..318a9da20 --- /dev/null +++ b/Client/src/Pages/Monitors/Home/index.jsx @@ -0,0 +1,151 @@ +import "./index.css"; +import { useEffect } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { getUptimeMonitorsByTeamId } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; +import { useNavigate } from "react-router-dom"; +import { useTheme } from "@emotion/react"; +import BasicTable from "../../../Components/BasicTable"; +import { Box, Button, Stack, Typography } from "@mui/material"; +import PropTypes from "prop-types"; +import SkeletonLayout from "./skeleton"; +import Fallback from "./fallback"; +import StatusBox from "./StatusBox"; +import { buildData } from "./monitorData"; +import Breadcrumbs from "../../../Components/Breadcrumbs"; +import Greeting from "../../../Utils/greeting"; + +const Monitors = ({ isAdmin }) => { + const theme = useTheme(); + const navigate = useNavigate(); + const monitorState = useSelector((state) => state.uptimeMonitors); + const authState = useSelector((state) => state.auth); + const dispatch = useDispatch({}); + + useEffect(() => { + dispatch(getUptimeMonitorsByTeamId(authState.authToken)); + }, [authState.authToken, dispatch]); + + const monitorStats = monitorState.monitors.reduce( + (acc, monitor) => { + if (monitor.isActive === false) { + acc["paused"] += 1; + } else if (monitor.status === true) { + acc["up"] += 1; + } else { + acc["down"] += 1; + } + return acc; + }, + { paused: 0, up: 0, down: 0 } + ); + + const data = buildData(monitorState.monitors, isAdmin, navigate); + + let loading = monitorState.isLoading && monitorState.monitors.length === 0; + + const now = new Date(); + const hour = now.getHours(); + + let greeting = ""; + let emoji = ""; + if (hour < 12) { + greeting = "morning"; + emoji = "🌅"; + } else if (hour < 18) { + greeting = "afternoon"; + emoji = "🌞"; + } else { + greeting = "evening"; + emoji = "🌙"; + } + + return ( + + {loading ? ( + + ) : ( + <> + + + + + {monitorState.monitors?.length !== 0 && ( + { + navigate("/monitors/create"); + }} + sx={{ fontWeight: 500 }} + > + Create monitor + + )} + + + {isAdmin && monitorState.monitors?.length === 0 && ( + + )} + + {monitorState.monitors?.length !== 0 && ( + <> + + + + + + + + + Actively monitoring + + + {monitorState.monitors.length} + + {/* TODO - add search bar */} + + + + > + )} + > + )} + + ); +}; + +Monitors.propTypes = { + isAdmin: PropTypes.bool, +}; +export default Monitors; diff --git a/Client/src/Pages/Monitors/Home/monitorData.jsx b/Client/src/Pages/Monitors/Home/monitorData.jsx new file mode 100644 index 000000000..d3fd66958 --- /dev/null +++ b/Client/src/Pages/Monitors/Home/monitorData.jsx @@ -0,0 +1,91 @@ +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: [], +}; + +export const buildData = (monitors, isAdmin, navigate) => { + const theme = useTheme(); + + data.rows = monitors.map((monitor, idx) => { + let uptimePercentage = ""; + if (monitor.uptimePercentage !== undefined) { + uptimePercentage = + monitor.uptimePercentage === 0 + ? "0" + : (monitor.uptimePercentage * 100).toFixed(2); + } + + const params = { + url: monitor.url, + title: monitor.name, + percentage: uptimePercentage, + percentageColor: + monitor.status === true + ? theme.palette.success.main + : theme.palette.error.text, + status: monitor.status === true ? "up" : "down", + }; + + // Reverse checks so 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/Home/skeleton.jsx b/Client/src/Pages/Monitors/Home/skeleton.jsx new file mode 100644 index 000000000..7d798d12c --- /dev/null +++ b/Client/src/Pages/Monitors/Home/skeleton.jsx @@ -0,0 +1,40 @@ +import { Skeleton, Stack } from "@mui/material"; +import { useTheme } from "@emotion/react"; + +/** + * Renders a skeleton layout. + * + * @returns {JSX.Element} + */ +const SkeletonLayout = () => { + const theme = useTheme(); + + return ( + <> + + + + + + + + + + + + + + + > + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Pages/Monitors/index.css b/Client/src/Pages/Monitors/index.css deleted file mode 100644 index 71eb2c755..000000000 --- a/Client/src/Pages/Monitors/index.css +++ /dev/null @@ -1,63 +0,0 @@ -.monitors h1.MuiTypography-root { - font-size: var(--env-var-font-size-large-plus); - color: var(--env-var-color-1); - font-weight: 500; -} -.monitors h2.MuiTypography-root { - font-size: var(--env-var-font-size-large); - color: var(--env-var-color-5); - font-weight: 500; -} -.monitors p.MuiTypography-root { - font-size: var(--env-var-font-size-medium); - color: var(--env-var-color-5); -} -.monitors h1.MuiTypography-root + p.MuiTypography-root { - font-size: var(--env-var-font-size-medium-plus); -} -.monitors .MuiStack-root > button:not(.MuiIconButton-root) { - font-size: var(--env-var-font-size-medium); - height: var(--env-var-height-2); - min-width: fit-content; -} - -.current-monitors-counter { - display: flex; - justify-content: center; - align-items: center; - padding: 5px; - min-width: 25px; - min-height: 25px; - background-color: var(--env-var-color-15); - border: 1px solid var(--env-var-color-6); - border-radius: 50%; - font-size: var(--env-var-font-size-medium); - font-weight: 500; - color: var(--env-var-color-1); - margin-left: 10px; - line-height: 0.8; -} - -body:has(.monitors) .actions-menu .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; -} -body:has(.monitors) .actions-menu .MuiList-root { - padding: 6px 8px; - min-width: 100px; -} -body:has(.monitors) .actions-menu 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); -} -body:has(.monitors) .actions-menu li.MuiButtonBase-root:last-of-type { - color: var(--env-var-color-26); -} -body:has(.monitors) .actions-menu .MuiModal-root p.MuiTypography-root { - font-size: var(--env-var-font-size-medium); - color: var(--env-var-color-2); -} diff --git a/Client/src/Pages/Monitors/index.jsx b/Client/src/Pages/Monitors/index.jsx deleted file mode 100644 index b935231d6..000000000 --- a/Client/src/Pages/Monitors/index.jsx +++ /dev/null @@ -1,625 +0,0 @@ -import { useEffect, useState } from "react"; -import { useSelector, useDispatch } from "react-redux"; -import { - deleteUptimeMonitor, - getUptimeMonitorsByUserId, -} from "../../Features/UptimeMonitors/uptimeMonitorsSlice"; -import { useNavigate } from "react-router-dom"; -import { useTheme } from "@emotion/react"; -import { StatusLabel } from "../../Components/Label"; -import { createToast } from "../../Utils/toastUtils"; -import { - Box, - IconButton, - Menu, - MenuItem, - Modal, - Skeleton, - Stack, - Typography, -} from "@mui/material"; -import BasicTable from "../../Components/BasicTable"; -import Button from "../../Components/Button"; -import Settings from "../../assets/icons/settings-bold.svg?react"; -import PropTypes from "prop-types"; -import BarChart from "../../Components/Charts/BarChart"; -import Breadcrumbs from "../../Components/Breadcrumbs"; -import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded"; -import background from "../../assets/Images/background-grid.svg"; -import Arrow from "../../assets/icons/top-right-arrow.svg?react"; -import ClockSnooze from "../../assets/icons/clock-snooze.svg?react"; -import "./index.css"; - -const ActionsMenu = ({ monitor }) => { - const [anchorEl, setAnchorEl] = useState(null); - const [actions, setActions] = useState({}); - const [isOpen, setIsOpen] = useState(false); - const dispatch = useDispatch(); - const theme = useTheme(); - const authState = useSelector((state) => state.auth); - const handleRemove = async (event) => { - event.preventDefault(); - event.stopPropagation(); - let monitor = { _id: actions.id }; - const action = await dispatch( - deleteUptimeMonitor({ authToken: authState.authToken, monitor }) - ); - if (action.meta.requestStatus === "fulfilled") { - setIsOpen(false); // close modal - dispatch(getUptimeMonitorsByUserId(authState.authToken)); - createToast({ body: "Monitor deleted successfully." }); - } else { - createToast({ body: "Failed to delete monitor." }); - } - }; - - const openMenu = (event, id, url) => { - event.preventDefault(); - event.stopPropagation(); - setAnchorEl(event.currentTarget); - setActions({ id: id, url: url }); - }; - - const openRemove = (e) => { - closeMenu(e); - setIsOpen(true); - }; - - const closeMenu = (e) => { - e.stopPropagation(); - setAnchorEl(null); - }; - - const navigate = useNavigate(); - return ( - <> - { - event.stopPropagation(); - openMenu( - event, - monitor._id, - monitor.type === "ping" ? null : monitor.url - ); - }} - sx={{ - "&:focus": { - outline: "none", - }, - }} - > - - - - closeMenu(e)} - > - {actions.url !== null ? ( - { - closeMenu(e); - e.stopPropagation(); - window.open(actions.url, "_blank", "noreferrer"); - }} - > - Open site - - ) : ( - "" - )} - { - e.stopPropagation(); - navigate(`/monitors/${actions.id}`); - }} - > - Details - - {/* TODO - pass monitor id to Incidents page */} - Incidents - { - e.stopPropagation(); - - navigate(`/monitors/configure/${actions.id}`); - }} - > - Configure - - { - e.stopPropagation(); - openRemove(e); - }} - > - Remove - - - { - e.stopPropagation(); - setIsOpen(false); - }} - disablePortal - > - - - Do you really want to delete this monitor? - - - Once deleted, this monitor cannot be retrieved. - - - { - e.stopPropagation(); - setIsOpen(false); - }} - /> - handleRemove(e)} - /> - - - - > - ); -}; - -ActionsMenu.propTypes = { - monitor: PropTypes.shape({ - _id: PropTypes.string, - url: PropTypes.string, - type: PropTypes.string, - }).isRequired, -}; - -/** - * Host component. - * This subcomponent receives a params object and displays the host details. - * - * @component - * @param {Object} params - An object containing the following properties: - * @param {string} params.title - The name of the host. - * @param {string} params.percentageColor - The color of the percentage text. - * @param {number} params.percentage - The percentage to display. - * @returns {React.ElementType} Returns a div element with the host details. - */ -const Host = ({ params }) => { - return ( - - - {params.title} - - - {params.percentage}% - - {params.url} - - ); -}; - -Host.propTypes = { - params: PropTypes.shape({ - title: PropTypes.string, - percentageColor: PropTypes.string, - percentage: PropTypes.number, - }).isRequired, -}; - -/** - * StatusBox component displays a status box with a title and value. - * The icon and color change based on the status title. - * - * @param {Object} props - * @param {string} props.title - The status title, which determines the icon and color. - * @param {number|string} props.value - The value to be displayed inside the box. - * @returns {JSX.Element} The rendered StatusBox component. - */ - -const StatusBox = ({ title, value }) => { - const theme = useTheme(); - - let sharedStyles = { position: "absolute", right: 8, opacity: 0.5 }; - - let color; - let icon; - if (title === "up") { - color = theme.pie.green.stroke; - icon = ( - - - - ); - } else if (title === "down") { - color = theme.pie.red.stroke; - icon = ( - - - - ); - } else if (title === "paused") { - color = theme.pie.yellow.stroke; - icon = ( - - - - ); - } - - return ( - - - {title} - - {icon} - - {value} - - # - - - - ); -}; - -StatusBox.propTypes = { - title: PropTypes.oneOf(["up", "down", "paused"]).isRequired, - value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, -}; - -/** - * Renders a skeleton layout. - * - * @returns {JSX.Element} - */ -const SkeletonLayout = () => { - const theme = useTheme(); - - return ( - <> - - - - - - - - - - - - - - - > - ); -}; - -const Monitors = () => { - const theme = useTheme(); - const navigate = useNavigate(); - const monitorState = useSelector((state) => state.uptimeMonitors); - const authState = useSelector((state) => state.auth); - const dispatch = useDispatch({}); - - useEffect(() => { - dispatch(getUptimeMonitorsByUserId(authState.authToken)); - }, [authState.authToken, dispatch]); - - const up = monitorState.monitors.reduce((acc, cur) => { - return cur.status === true ? acc + 1 : acc; - }, 0); - - const down = monitorState.monitors.length - up; - - 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: [], - }; - - data.rows = monitorState.monitors.map((monitor, idx) => { - const params = { - url: monitor.url, - title: monitor.name, - percentage: 100, - percentageColor: - monitor.status === true - ? "var(--env-var-color-17)" - : "var(--env-var-color-19)", - status: monitor.status === true ? "up" : "down", - }; - - // Reverse checks so latest check is on the right - const reversedChecks = monitor.checks.slice().reverse(); - - return { - id: monitor._id, - // disabled for now - 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: , - }, - ], - }; - }); - - let loading = monitorState.isLoading && monitorState.monitors.length === 0; - - const now = new Date(); - const hour = now.getHours(); - - let greeting = ""; - let emoji = ""; - if (hour < 12) { - greeting = "morning"; - emoji = "🌅"; - } else if (hour < 18) { - greeting = "afternoon"; - emoji = "🌞"; - } else { - greeting = "evening"; - emoji = "🌙"; - } - - return ( - - {loading ? ( - - ) : ( - <> - - - - - - - Good {greeting},{" "} - - - {authState.user.firstName} {emoji} - - - - Here’s an overview of your uptime monitors. - - - {monitorState.monitors?.length !== 0 && ( - { - navigate("/monitors/create"); - }} - sx={{ fontWeight: 500, alignSelf: "flex-end" }} - /> - )} - - - {monitorState.monitors?.length === 0 ? ( - - No monitors found - - It looks like you don’t have any monitors set up yet. - - { - navigate("/monitors/create"); - }} - sx={{ mt: theme.gap.large }} - /> - - ) : ( - <> - - - - - - - - - Actively monitoring - - - {monitorState.monitors.length} - - {/* TODO - add search bar */} - - - - > - )} - > - )} - - ); -}; - -export default Monitors; diff --git a/Client/src/Pages/Monitors/styled.jsx b/Client/src/Pages/Monitors/styled.jsx new file mode 100644 index 000000000..afef6508e --- /dev/null +++ b/Client/src/Pages/Monitors/styled.jsx @@ -0,0 +1,35 @@ +import { Stack, styled } from "@mui/material"; + +export const ConfigBox = styled(Stack)(({ theme }) => ({ + flexDirection: "row", + border: 1, + borderStyle: "solid", + borderColor: theme.palette.border.light, + borderRadius: theme.spacing(2), + backgroundColor: theme.palette.background.main, + "& > *": { + paddingTop: theme.spacing(12), + paddingBottom: theme.spacing(18), + }, + "& > div:first-of-type": { + flex: 0.7, + borderRight: 1, + borderRightStyle: "solid", + borderRightColor: theme.palette.border.light, + paddingRight: theme.spacing(15), + paddingLeft: theme.spacing(15), + }, + "& > div:last-of-type": { + flex: 1, + paddingRight: theme.spacing(20), + paddingLeft: theme.spacing(18), + }, + "& h2": { + color: theme.palette.text.secondary, + fontSize: 15, + fontWeight: 600, + }, + "& h3, & p": { + color: theme.palette.text.tertiary, + }, +})); diff --git a/Client/src/Pages/NotFound/index.css b/Client/src/Pages/NotFound/index.css deleted file mode 100644 index 621172386..000000000 --- a/Client/src/Pages/NotFound/index.css +++ /dev/null @@ -1,15 +0,0 @@ -.not-found-page { - height: 100vh; -} -.not-found-page button, -.not-found-page p { - font-size: var(--env-var-font-size-medium); -} -.not-found-page h1 { - font-size: var(--env-var-font-size-large); - font-weight: 600; -} -.not-found-page h1, -.not-found-page p { - color: var(--env-var-color-5); -} diff --git a/Client/src/Pages/NotFound/index.jsx b/Client/src/Pages/NotFound/index.jsx index ef4b37839..7d7adf149 100644 --- a/Client/src/Pages/NotFound/index.jsx +++ b/Client/src/Pages/NotFound/index.jsx @@ -1,10 +1,9 @@ -import Button from "../../Components/Button"; -import "./index.css"; import React from "react"; import PropTypes from "prop-types"; -import { Stack, Typography } from "@mui/material"; -import { useNavigate } from "react-router"; import NotFoundSvg from "../../../src/assets/Images/sushi_404.svg"; +import { Button, Stack, Typography } from "@mui/material"; +import { useNavigate } from "react-router"; +import { useTheme } from "@emotion/react"; /** * Support for defaultProps will be removed from function components in a future major release @@ -33,18 +32,28 @@ const DefaultValue = { */ const NotFound = ({ title = DefaultValue.title, desc = DefaultValue.desc }) => { const navigate = useNavigate(); + const theme = useTheme(); + return ( - - + + - {title} - {desc} + + {title} + + {desc} navigate("/")} - /> + > + Go to the main dashboard + ); diff --git a/Client/src/Pages/PageSpeed/Configure/index.css b/Client/src/Pages/PageSpeed/Configure/index.css index 408e2d639..fe257233c 100644 --- a/Client/src/Pages/PageSpeed/Configure/index.css +++ b/Client/src/Pages/PageSpeed/Configure/index.css @@ -1,23 +1,9 @@ -.configure-pagespeed form > .MuiStack-root:nth-child(2) { - background-color: var(--env-var-color-8); - border: solid 1px var(--env-var-color-6); - border-radius: var(--env-var-radius-1); - padding: var(--env-var-font-size-large-plus) var(--env-var-font-size-xlarge); -} .configure-pagespeed h1.MuiTypography-root { font-size: var(--env-var-font-size-large-plus); } -.configure-pagespeed h1.MuiTypography-root, -.configure-pagespeed h2.MuiTypography-root { - color: var(--env-var-color-1); -} .configure-pagespeed span.MuiTypography-root { font-size: var(--env-var-font-size-small-plus); } -.configure-pagespeed p.MuiTypography-root, -.configure-pagespeed h3.MuiTypography-root { - color: var(--env-var-color-5); -} .configure-pagespeed h1.MuiTypography-root, .configure-pagespeed h3.MuiTypography-root, .configure-pagespeed h2.MuiTypography-root { @@ -38,13 +24,10 @@ .configure-pagespeed .checkbox-wrapper { margin-bottom: 1px; } -.configure-pagespeed .MuiInputBase-root:has(> .Mui-disabled) { - background-color: var(--env-var-color-13); -} + /* to be removed */ .configure-pagespeed .section-disabled { opacity: 0.4; - background-color: #f1f1f2; padding: 10px; border-radius: 8px; user-select: none; @@ -55,11 +38,6 @@ height: var(--env-var-height-2); } -body:has(.configure-pagespeed) .select-dropdown .MuiMenuItem-root, -.configure-pagespeed .select-wrapper .select-component > .MuiSelect-select { - text-transform: lowercase; -} - .configure-pagespeed h3.MuiTypography-root { flex: 0.7; } diff --git a/Client/src/Pages/PageSpeed/Configure/index.jsx b/Client/src/Pages/PageSpeed/Configure/index.jsx index 83484de7e..92dd5bb36 100644 --- a/Client/src/Pages/PageSpeed/Configure/index.jsx +++ b/Client/src/Pages/PageSpeed/Configure/index.jsx @@ -1,17 +1,16 @@ import { useEffect, useState } from "react"; import { useTheme } from "@emotion/react"; -import { Box, Modal, Skeleton, Stack, Typography } from "@mui/material"; +import { Box, Button, Modal, Skeleton, Stack, Typography } from "@mui/material"; import { useDispatch, useSelector } from "react-redux"; import { useNavigate, useParams } from "react-router"; import { deletePageSpeed, - getPageSpeedByUserId, + getPageSpeedByTeamId, updatePageSpeed, } from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice"; import { monitorValidation } from "../../../Validation/validation"; import { createToast } from "../../../Utils/toastUtils"; import { logger } from "../../../Utils/Logger"; -import Button from "../../../Components/Button"; import Field from "../../../Components/Inputs/Field"; import Select from "../../../Components/Inputs/Select"; import Checkbox from "../../../Components/Inputs/Checkbox"; @@ -32,8 +31,8 @@ const SkeletonLayout = () => { return ( <> - - + + { variant="rounded" width="50%" height={24} - sx={{ mb: theme.gap.small }} + sx={{ mb: theme.spacing(4) }} /> { ); if (action.meta.requestStatus === "fulfilled") { createToast({ body: "Monitor updated successfully!" }); - dispatch(getPageSpeedByUserId(authToken)); + dispatch(getPageSpeedByTeamId(authToken)); } else { createToast({ body: "Failed to update monitor." }); } @@ -147,7 +146,7 @@ const PageSpeedConfigure = () => { let loading = Object.keys(monitor).length === 0; return ( - + {loading ? ( ) : ( @@ -165,21 +164,22 @@ const PageSpeedConfigure = () => { spellCheck="false" onSubmit={handleSave} flex={1} - gap={theme.gap.large} + gap={theme.spacing(12)} > - + {monitor?.url} @@ -187,8 +187,8 @@ const PageSpeedConfigure = () => { component="span" sx={{ color: monitor?.status - ? "var(--env-var-color-17)" - : "var(--env-var-color-24)", + ? theme.palette.success.main + : theme.palette.error.text, }} > Your pagespeed monitor is {monitor?.status ? "live" : "down"}. @@ -196,34 +196,51 @@ const PageSpeedConfigure = () => { } + variant="contained" + color="secondary" sx={{ - backgroundColor: theme.palette.otherColors.fillGray, - pl: theme.gap.small, - pr: theme.gap.medium, + pl: theme.spacing(4), + pr: theme.spacing(6), "& svg": { - mr: theme.gap.xs, + mr: theme.spacing(2), + "& path": { + stroke: theme.palette.other.icon, + strokeWidth: 0.1, + }, }, }} - /> + > + + Pause + setIsOpen(true)} - /> + sx={{ + ml: theme.spacing(6), + }} + > + Remove + - + - Monitor friendly name + Monitor display name { error={errors.name} /> - + .Mui-disabled)": { + backgroundColor: theme.palette.background.accent, + }, + }} + > URL { Incidents notifications{" "} (coming soon) - - + + When there is a new incident, { label="Notify via email to following emails" isChecked={false} /> - + { onChange={() => logger.warn("disabled")} error="" /> - + You can separate multiple emails with a comma @@ -297,11 +324,13 @@ const PageSpeedConfigure = () => { + sx={{ px: theme.spacing(12) }} + > + Save + > @@ -314,41 +343,53 @@ const PageSpeedConfigure = () => { disablePortal > - + Do you really want to delete this monitor? - + Once deleted, this monitor cannot be retrieved. setIsOpen(false)} - /> - + > + Cancel + + + Delete + diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.css b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.css index 42ca0ffa8..4b46fd3f0 100644 --- a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.css +++ b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.css @@ -1,17 +1,3 @@ -.create-page-speed form > .MuiStack-root:not(:last-of-type) { - background-color: var(--env-var-color-8); - border: solid 1px var(--env-var-color-6); - border-radius: var(--env-var-radius-1); - padding: var(--env-var-font-size-large-plus) var(--env-var-font-size-xlarge); -} -.create-page-speed h1.MuiTypography-root { - font-size: 22px; - color: var(--env-var-color-1); -} -.create-page-speed p.MuiTypography-root, -.create-page-speed h3.MuiTypography-root { - color: var(--env-var-color-5); -} .create-page-speed h1.MuiTypography-root, .create-page-speed h3.MuiTypography-root { font-weight: 600; @@ -31,11 +17,12 @@ .create-page-speed .checkbox-wrapper { margin-bottom: 1px; } + /* to be removed */ .create-page-speed .section-disabled { opacity: 0.4; - background-color: #f1f1f2; padding: 10px; + margin-right: -5px; border-radius: 8px; user-select: none; pointer-events: none; @@ -45,11 +32,6 @@ height: var(--env-var-height-2); } -body:has(.create-page-speed) .select-dropdown .MuiMenuItem-root, -.create-page-speed .select-wrapper .select-component > .MuiSelect-select { - text-transform: lowercase; -} - .create-page-speed h3.MuiTypography-root { flex: 0.7; } diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx index c707db319..00c02a362 100644 --- a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx +++ b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx @@ -1,11 +1,10 @@ -import { Box, Stack, Typography } from "@mui/material"; +import { Box, Button, Stack, Typography } from "@mui/material"; import { useState } from "react"; import { useTheme } from "@emotion/react"; import { useDispatch, useSelector } from "react-redux"; import { useNavigate } from "react-router"; import Field from "../../../Components/Inputs/Field"; import Select from "../../../Components/Inputs/Select"; -import Button from "../../../Components/Button"; import Checkbox from "../../../Components/Inputs/Checkbox"; import { monitorValidation } from "../../../Validation/validation"; import { createToast } from "../../../Utils/toastUtils"; @@ -78,6 +77,7 @@ const CreatePageSpeed = () => { ...monitor, description: monitor.name, userId: user._id, + teamId: user.teamId, interval: form.interval * MS_PER_MINUTE, type: "pagespeed", }; @@ -98,7 +98,7 @@ const CreatePageSpeed = () => { }; return ( - + { noValidate spellCheck="false" onSubmit={handleCreate} - gap={theme.gap.large} + gap={theme.spacing(12)} flex={1} > - Create pagespeed monitor - + + + Create your{" "} + + + pagespeed{" "} + + + monitor + + + - Monitor friendly name + Monitor display name { Incidents notifications{" "} (coming soon) - - + + When there is a new incident, { label="Notify via email to following emails" isChecked={false} /> - + { onChange={() => logger.warn("disabled")} error="" /> - + You can separate multiple emails with a comma @@ -189,11 +228,13 @@ const CreatePageSpeed = () => { + sx={{ px: theme.spacing(12), mt: theme.spacing(12) }} + > + Create + diff --git a/Client/src/Pages/PageSpeed/Details/index.css b/Client/src/Pages/PageSpeed/Details/index.css index 357177c78..1da62a362 100644 --- a/Client/src/Pages/PageSpeed/Details/index.css +++ b/Client/src/Pages/PageSpeed/Details/index.css @@ -1,6 +1,5 @@ .page-speed-details h1 { font-size: var(--env-var-font-size-large-plus); - color: var(--env-var-color-1); } .page-speed-details h2, .page-speed-details h6 { @@ -15,13 +14,7 @@ .page-speed-details h4 > span { font-size: var(--env-var-font-size-small-plus); } -.page-speed-details h2, -.page-speed-details h4, -.page-speed-details p { - color: var(--env-var-color-5); -} .page-speed-details h6 { - color: var(--env-var-color-3); line-height: 22px; } .page-speed-details h1, @@ -33,14 +26,6 @@ height: 34px; } -.page-speed-details .stat-box, -.page-speed-details > .MuiStack-root:last-child, -.page-speed-details > .MuiBox-root:nth-last-child(3) { - border: solid 1px var(--env-var-color-6); - border-radius: var(--env-var-radius-1); - background-color: var(--env-var-color-8); - min-width: 200px; -} .page-speed-details .stat-box { flex: 1; } diff --git a/Client/src/Pages/PageSpeed/Details/index.jsx b/Client/src/Pages/PageSpeed/Details/index.jsx index 2e5cc12a7..29c0b72e0 100644 --- a/Client/src/Pages/PageSpeed/Details/index.jsx +++ b/Client/src/Pages/PageSpeed/Details/index.jsx @@ -1,5 +1,5 @@ import PropTypes from "prop-types"; -import { Box, Skeleton, Stack, Typography } from "@mui/material"; +import { Box, Button, Skeleton, Stack, Typography } from "@mui/material"; import { PieChart } from "@mui/x-charts/PieChart"; import { useDrawingArea } from "@mui/x-charts"; import { useEffect, useState } from "react"; @@ -12,7 +12,6 @@ import { } from "../../../Utils/timeUtils"; import { logger } from "../../../Utils/Logger"; import { networkService } from "../../../main"; -import Button from "../../../Components/Button"; import SettingsIcon from "../../../assets/icons/settings-bold.svg?react"; import LastCheckedIcon from "../../../assets/icons/calendar-check.svg?react"; import ClockIcon from "../../../assets/icons/maintenance.svg?react"; @@ -29,17 +28,28 @@ const StatBox = ({ icon, title, value }) => { {icon} - + {title} - {value} + + {value} + ); @@ -151,7 +161,7 @@ const SkeletonLayout = () => { return ( <> - + @@ -159,7 +169,7 @@ const SkeletonLayout = () => { variant="rounded" width="50%" height={18} - sx={{ mt: theme.gap.small }} + sx={{ mt: theme.spacing(4) }} /> { @@ -237,10 +247,37 @@ const PageSpeedDetails = () => { * @returns {{stroke: string, text: string, bg: string}} The color properties for the given performance value. */ const getColors = (value) => { - if (value >= 90 && value <= 100) return theme.pie.green; - else if (value >= 50 && value < 90) return theme.pie.yellow; - else if (value >= 0 && value < 50) return theme.pie.red; - return theme.pie.default; + if (value >= 90 && value <= 100) + return { + stroke: theme.palette.success.main, + strokeBg: theme.palette.success.light, + text: theme.palette.success.text, + bg: theme.palette.success.bg, + shape: "circle", + }; + else if (value >= 50 && value < 90) + return { + stroke: theme.palette.warning.main, + strokeBg: theme.palette.warning.bg, + text: theme.palette.warning.text, + bg: theme.palette.warning.light, + shape: "square", + }; + else if (value >= 0 && value < 50) + return { + stroke: theme.palette.error.text, + strokeBg: theme.palette.error.light, + text: theme.palette.error.text, + bg: theme.palette.error.bg, + shape: "circle", + }; + return { + stroke: theme.palette.unresolved.main, + strokeBg: theme.palette.unresolved.light, + text: theme.palette.unresolved.main, + bg: theme.palette.unresolved.bg, + shape: "", + }; }; /** @@ -312,8 +349,15 @@ const PageSpeedDetails = () => { let loading = Object.keys(monitor).length === 0; const data = monitor?.checks ? [...monitor.checks].reverse() : []; + + let sharedStyles = { + border: 1, + borderColor: theme.palette.border.light, + borderRadius: theme.shape.borderRadius, + backgroundColor: theme.palette.background.main, + }; return ( - + {loading ? ( ) : ( @@ -324,54 +368,58 @@ const PageSpeedDetails = () => { { name: "details", path: `/pagespeed/${monitorId}` }, ]} /> - + {monitor?.url} Your pagespeed monitor is live. - } + variant="contained" + color="secondary" onClick={() => navigate(`/pagespeed/configure/${monitorId}`)} sx={{ ml: "auto", alignSelf: "flex-end", - backgroundColor: theme.palette.otherColors.fillGray, - px: theme.gap.medium, + px: theme.spacing(5), "& svg": { - mr: "6px", + mr: theme.spacing(3), + "& path": { + stroke: theme.palette.other.icon, + }, }, }} - /> + > + + Configure + { value={formatDurationRounded(monitor?.interval)} > - Score history - + + Score history + + - Performance report - + + Performance report + + setExpand(false)}> {expand ? ( @@ -514,7 +582,7 @@ const PageSpeedDetails = () => { )} - + Values are estimated and may vary.{" "} { @@ -569,8 +639,8 @@ const PageSpeedDetails = () => { {Object.keys(audits).map((key) => { if (key === "_id") return; @@ -581,8 +651,8 @@ const PageSpeedDetails = () => { let shape = ( { shape = ( @@ -604,10 +674,14 @@ const PageSpeedDetails = () => { sx={{ width: 0, height: 0, - ml: `calc((${theme.gap.medium} - ${theme.gap.small}) / -2)`, - borderLeft: `${theme.gap.small} solid transparent`, - borderRight: `${theme.gap.small} solid transparent`, - borderBottom: `${theme.gap.medium} solid ${metricParams.stroke}`, + ml: `calc((${theme.spacing(6)} - ${theme.spacing( + 4 + )}) / -2)`, + borderLeft: `${theme.spacing(4)} solid transparent`, + borderRight: `${theme.spacing(4)} solid transparent`, + borderBottom: `${theme.spacing(6)} solid ${ + metricParams.stroke + }`, }} > ); @@ -630,7 +704,7 @@ const PageSpeedDetails = () => { className="metric" key={`${key}-box`} direction="row" - gap={theme.gap.small} + gap={theme.spacing(4)} > {shape} @@ -650,8 +724,8 @@ const PageSpeedDetails = () => { component="span" ml="2px" sx={{ - color: theme.palette.secondary.main, - fontSize: "13px", + color: theme.palette.text.secondary, + fontSize: 13, }} > {unit} diff --git a/Client/src/Pages/PageSpeed/index.css b/Client/src/Pages/PageSpeed/index.css index b910c1645..83935dde0 100644 --- a/Client/src/Pages/PageSpeed/index.css +++ b/Client/src/Pages/PageSpeed/index.css @@ -1,29 +1,17 @@ -.page-speed h1.MuiTypography-root, -.page-speed span.MuiTypography-root, -.page-speed p.MuiTypography-root { - color: var(--env-var-color-5); -} .page-speed h2.MuiTypography-root { - color: var(--env-var-color-3); line-height: 1.1; } -.page-speed h1.MuiTypography-root, +.page-speed:not(:has([class*="fallback__"])) h1.MuiTypography-root { + font-size: var(--env-var-font-size-large-plus); +} .page-speed h2.MuiTypography-root { font-size: var(--env-var-font-size-large); font-weight: 600; } -.page-speed p.MuiTypography-root { - font-size: var(--env-var-font-size-medium); -} .page-speed p:has(> span.MuiTypography-root), .page-speed p.MuiTypography-root > span.MuiTypography-root { font-size: var(--env-var-font-size-small-plus); } -.page-speed .MuiGrid-item > .MuiStack-root { - border: solid 1px var(--env-var-color-6); - border-radius: var(--env-var-radius-1); - background-color: var(--env-var-color-8); -} .page-speed .label { padding: 7px; height: 22px; @@ -31,8 +19,4 @@ } .page-speed:not(:has([class*="fallback__"])) button { height: 34px; - align-self: flex-end; -} -.page-speed [class*="fallback__"] h1 { - margin-left: var(--env-var-spacing-3); } diff --git a/Client/src/Pages/PageSpeed/index.jsx b/Client/src/Pages/PageSpeed/index.jsx index 368ae2062..eaaa3bc3e 100644 --- a/Client/src/Pages/PageSpeed/index.jsx +++ b/Client/src/Pages/PageSpeed/index.jsx @@ -1,17 +1,18 @@ -import { Box, Grid, Skeleton, Stack, Typography } from "@mui/material"; +import { Box, Button, Grid, Skeleton, Stack, Typography } from "@mui/material"; import { useEffect } from "react"; import { useTheme } from "@emotion/react"; import { formatDate, formatDurationRounded } from "../../Utils/timeUtils"; import { StatusLabel } from "../../Components/Label"; import { useDispatch, useSelector } from "react-redux"; -import { getPageSpeedByUserId } from "../../Features/PageSpeedMonitor/pageSpeedMonitorSlice"; +import { getPageSpeedByTeamId } from "../../Features/PageSpeedMonitor/pageSpeedMonitorSlice"; import PageSpeedIcon from "../../assets/icons/page-speed.svg?react"; import Fallback from "../../Components/Fallback"; import "./index.css"; -import Button from "../../Components/Button"; import { useNavigate } from "react-router"; import { getLastChecked } from "../../Utils/monitorUtils"; import PropTypes from "prop-types"; +import Breadcrumbs from "../../Components/Breadcrumbs"; +import Greeting from "../../Utils/greeting"; const Card = ({ data }) => { const theme = useTheme(); @@ -22,19 +23,36 @@ const Card = ({ data }) => { item lg={6} flexGrow={1} - sx={{ "&:hover > .MuiStack-root": { backgroundColor: "#f9fafb" } }} + sx={{ + "&:hover > .MuiStack-root": { + backgroundColor: theme.palette.background.accent, + }, + }} > navigate(`/pagespeed/${data._id}`)} - sx={{ cursor: "pointer" }} + border={1} + borderColor={theme.palette.border.light} + borderRadius={theme.shape.borderRadius} + backgroundColor={theme.palette.background.main} + sx={{ + cursor: "pointer", + "& svg path": { stroke: theme.palette.other.icon, strokeWidth: 0.8 }, + }} > - + - + {data.name} { text={data.status ? "Live (collecting data)" : "Inactive"} /> - {data.url.replace(/^https?:\/\//, "")} - + + {data.url.replace(/^https?:\/\//, "")} + + Last checked:{" "} @@ -71,11 +91,11 @@ const SkeletonLayout = () => { const theme = useTheme(); return ( - + @@ -83,7 +103,7 @@ const SkeletonLayout = () => { variant="rounded" width="50%" height={19.5} - sx={{ mt: theme.gap.xs }} + sx={{ mt: theme.spacing(2) }} /> { sx={{ alignSelf: "flex-end" }} /> - + { ); }; -const PageSpeed = () => { +const PageSpeed = ({ isAdmin }) => { const theme = useTheme(); const dispatch = useDispatch(); const navigate = useNavigate(); - const { authToken } = useSelector((state) => state.auth); + const { authToken, user } = useSelector((state) => state.auth); const { monitors, isLoading } = useSelector( (state) => state.pageSpeedMonitors ); useEffect(() => { - dispatch(getPageSpeedByUserId(authToken)); + dispatch(getPageSpeedByTeamId(authToken)); }, [authToken, dispatch]); // will show skeletons only on initial load @@ -141,47 +161,71 @@ const PageSpeed = () => { let isActuallyLoading = isLoading && monitors.length === 0; return ( - + [class*="fallback__"])': { + position: "relative", + border: 1, + borderColor: theme.palette.border.light, + borderRadius: theme.shape.borderRadius, + borderStyle: "dashed", + backgroundColor: theme.palette.background.main, + overflow: "hidden", + }, + }} + > {isActuallyLoading ? ( ) : monitors?.length !== 0 ? ( - - - - All page speed monitors - - Click on one of the monitors to get more site speed information. - - - navigate("/pagespeed/create")} - /> - - + + + + + + navigate("/pagespeed/create")} + > + Create new + + + + {monitors?.map((monitor) => ( ))} - + ) : ( )} ); }; +PageSpeed.propTypes = { + isAdmin: PropTypes.bool, +}; export default PageSpeed; diff --git a/Client/src/Pages/Settings/index.css b/Client/src/Pages/Settings/index.css index 3f6ac8d7a..516f50b0b 100644 --- a/Client/src/Pages/Settings/index.css +++ b/Client/src/Pages/Settings/index.css @@ -5,11 +5,6 @@ .settings p.MuiTypography-root { font-size: var(--env-var-font-size-medium); } -.settings h1.MuiTypography-root, -.settings h2.MuiTypography-root, -.settings p.MuiTypography-root { - color: var(--env-var-color-5); -} .settings h1.MuiTypography-root { font-size: var(--env-var-font-size-large); } @@ -23,21 +18,4 @@ .settings span.MuiTypography-root { opacity: 0.6; margin-right: 4px; -} -.settings .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); -} -.settings .config-box .MuiBox-root, -.settings .config-box .MuiStack-root { - flex: 1; -} - -.settings-form { - display: flex; - flex-direction: column; - gap: var(--env-var-spacing-4); -} +} \ No newline at end of file diff --git a/Client/src/Pages/Settings/index.jsx b/Client/src/Pages/Settings/index.jsx index 2b6f40682..fc615d848 100644 --- a/Client/src/Pages/Settings/index.jsx +++ b/Client/src/Pages/Settings/index.jsx @@ -1,14 +1,64 @@ import { useTheme } from "@emotion/react"; -import { Box, Stack, Typography } from "@mui/material"; -import Button from "../../Components/Button"; +import { Box, Button, Stack, styled, Typography } from "@mui/material"; import Field from "../../Components/Inputs/Field"; import Link from "../../Components/Link"; import Select from "../../Components/Inputs/Select"; import { logger } from "../../Utils/Logger"; import "./index.css"; - -const Settings = () => { +import { useDispatch, useSelector } from "react-redux"; +import { createToast } from "../../Utils/toastUtils"; +import { deleteMonitorChecksByTeamId } from "../../Features/UptimeMonitors/uptimeMonitorsSlice"; +import PropTypes from "prop-types"; +import LoadingButton from "@mui/lab/LoadingButton"; +const Settings = ({ isAdmin }) => { const theme = useTheme(); + const { user, authToken } = useSelector((state) => state.auth); + const { isLoading } = useSelector((state) => state.uptimeMonitors); + + const dispatch = useDispatch(); + const handleClearStats = async () => { + try { + const action = await dispatch( + deleteMonitorChecksByTeamId({ teamId: user.teamId, authToken }) + ); + + if (deleteMonitorChecksByTeamId.fulfilled.match(action)) { + createToast({ body: "Stats cleared successfully" }); + } else { + createToast({ body: "Failed to clear stats" }); + } + } catch (error) { + logger.error(error); + createToast({ body: "Failed to clear stats" }); + } + }; + + const ConfigBox = styled("div")({ + display: "flex", + flexDirection: "row", + justifyContent: "space-between", + gap: theme.spacing(20), + paddingTop: theme.spacing(12), + paddingInline: theme.spacing(15), + paddingBottom: theme.spacing(25), + backgroundColor: theme.palette.background.main, + border: 1, + borderStyle: "solid", + borderColor: theme.palette.border.light, + borderRadius: theme.spacing(2), + "& > div:first-of-type": { + flex: 0.7, + }, + "& > div:last-of-type": { + flex: 1, + }, + "& h1, & h2": { + color: theme.palette.text.secondary, + }, + "& p": { + color: theme.palette.text.tertiary, + }, + }); return ( { paddingBottom: 0, }} > - - + + General Settings - + Display timezone- The timezone of the dashboard you publicly display. @@ -35,7 +85,7 @@ const Settings = () => { timezone of your server. - + { items={[{ _id: "est", name: "America / Toronto" }]} /> - - - - History and monitoring - - Define here for how long you want to keep the data. You can also - remove all past data. - - - - logger.warn("Disabled")} - /> + + {isAdmin && ( + - Clear all stats. This is irreversible. - + History and monitoring + + Define here for how long you want to keep the data. You can also + remove all past data. + - - - + + logger.warn("Disabled")} + /> + + Clear all stats. This is irreversible. + + Clear all stats + + + + + )} + About BlueWave Uptime v1.0.0 Developed by Bluewave Labs. @@ -108,13 +154,22 @@ const Settings = () => { label="https://github.com/bluewave-labs" /> - + - + + Save + - + ); }; +Settings.propTypes = { + isAdmin: PropTypes.bool, +}; export default Settings; diff --git a/Client/src/Pages/Status/index.jsx b/Client/src/Pages/Status/index.jsx index d280132c4..cb759b33b 100644 --- a/Client/src/Pages/Status/index.jsx +++ b/Client/src/Pages/Status/index.jsx @@ -1,8 +1,25 @@ +import { Box } from "@mui/material"; +import { useTheme } from "@emotion/react"; import Fallback from "../../Components/Fallback"; const Status = () => { + const theme = useTheme(); + return ( - + [class*="fallback__"])': { + position: "relative", + border: 1, + borderColor: theme.palette.border.light, + borderRadius: theme.shape.borderRadius, + borderStyle: "dashed", + backgroundColor: theme.palette.background.main, + overflow: "hidden", + }, + }} + > { "Build trust with your customers", ]} /> - + ); }; diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js index de4508cff..31a23b5b1 100644 --- a/Client/src/Utils/NetworkService.js +++ b/Client/src/Utils/NetworkService.js @@ -17,6 +17,27 @@ class NetworkService { ); } + /** + * + * ************************************ + * Create a new monitor + * ************************************ + * + * @async + * @param {string} authToken - The authorization token to be used in the request header. + * @param {string} monitorId - The monitor ID to be sent in the param. + * @returns {Promise} The response from the axios GET request. + */ + + async getMonitorByid(authToken, monitorId) { + return this.axiosInstance.get(`/monitors/${monitorId}`, { + headers: { + Authorization: `Bearer ${authToken}`, + "Content-Type": "application/json", + }, + }); + } + /** * * ************************************ @@ -52,9 +73,9 @@ class NetworkService { * @param {boolean} [normalize] - Whether to normalize the retrieved monitors. * @returns {Promise} The response from the axios GET request. */ - async getMonitorsByUserId( + async getMonitorsByTeamId( authToken, - userId, + teamId, limit, types, status, @@ -74,7 +95,7 @@ class NetworkService { if (normalize) params.append("normalize", normalize); return this.axiosInstance.get( - `/monitors/user/${userId}?${params.toString()}`, + `/monitors/team/${teamId}?${params.toString()}`, { headers: { Authorization: `Bearer ${authToken}`, @@ -164,6 +185,47 @@ class NetworkService { }); } + /** + * ************************************ + * Deletes all checks for all monitor by teamID + * ************************************ + * + * @async + * @param {string} authToken - The authorization token to be used in the request header. + * @param {string} monitorId - The ID of the monitor to be deleted. + * @returns {Promise} The response from the axios DELETE request. + */ + async deleteChecksByTeamId(authToken, teamId) { + return this.axiosInstance.delete(`/checks/team/${teamId}`, { + headers: { + Authorization: `Bearer ${authToken}`, + "Content-Type": "application/json", + }, + }); + } + /** + * ************************************ + * Pauses a single monitor by its ID + * ************************************ + * + * @async + * @param {string} authToken - The authorization token to be used in the request header. + * @param {string} monitorId - The ID of the monitor to be paused. + * @returns {Promise} The response from the axios POST request. + */ + async pauseMonitorById(authToken, monitorId) { + return this.axiosInstance.post( + `/monitors/pause/${monitorId}`, + {}, + { + headers: { + Authorization: `Bearer ${authToken}`, + "Content-Type": "application/json", + }, + } + ); + } + /** * ************************************ * Gets the certificate expiry for a monitor @@ -226,7 +288,6 @@ class NetworkService { return this.axiosInstance.put(`/auth/user/${userId}`, form, { headers: { Authorization: `Bearer ${authToken}`, - "Content-Type": "application/json", }, }); } @@ -293,8 +354,8 @@ class NetworkService { * @returns {Promise} The response from the axios GET request. * */ - async doesAdminExist() { - return this.axiosInstance.get("/auth/users/admin"); + async doesSuperAdminExist() { + return this.axiosInstance.get("/auth/users/superadmin"); } /** @@ -327,7 +388,7 @@ class NetworkService { */ async requestInvitationToken(authToken, email, role) { return this.axiosInstance.post( - `/auth/invite`, + `/invite`, { email, role }, { headers: { Authorization: `Bearer ${authToken}` }, @@ -346,7 +407,7 @@ class NetworkService { * */ async verifyInvitationToken(token) { - return this.axiosInstance.post(`/auth/invite/verify`, { + return this.axiosInstance.post(`/invite/verify`, { token, }); } @@ -409,9 +470,9 @@ class NetworkService { * @returns {Promise} The response from the axios GET request. * */ - async getChecksByUser( + async getChecksByTeam( authToken, - userId, + teamId, sortOrder, limit, dateRange, @@ -427,7 +488,7 @@ class NetworkService { if (page) params.append("page", page); if (rowsPerPage) params.append("rowsPerPage", rowsPerPage); return this.axiosInstance.get( - `/checks/user/${userId}?${params.toString()}`, + `/checks/team/${teamId}?${params.toString()}`, { headers: { Authorization: `Bearer ${authToken}` }, } diff --git a/Client/src/Utils/Theme.js b/Client/src/Utils/Theme.js deleted file mode 100644 index c4faa2d1f..000000000 --- a/Client/src/Utils/Theme.js +++ /dev/null @@ -1,171 +0,0 @@ -import { createTheme } from "@mui/material"; - -// Colors for MUI theme -const primaryMain = "#1570EF"; -const secondaryMain = "#475467"; -const tertiaryMain = "#475467"; -const tertiaryLinkHover = "#eff8ff"; - -// Label Colors -const labelOrange = "#F79009"; -const labelGray = "#475467"; -const labelPurple = "#6941C6"; -const labelGreen = "#067647"; -const labelRed = "#F04438"; - -// Section colors -const sectionBorder = "#D0D5DD"; -const sectionBg = "#F8F9F8"; - -// Other Colors -const otherColorsBlackish = "#101828"; -const otherColorsBluishGray = "#344054"; -const otherColorsGraishWhite = "#eaecf0"; -const otherColorsPurple = "#7f56d9"; -const otherColorsWhite = "#fff"; -const otherColorsGraishWhiteLight = "#f2f2f2"; -const otherColorsStrongBlue = "#f2f2f2"; -const otherColorsSlateGray = "#667085"; -const otherColorsFillGray = "#f4f4f4"; - -//box shadow -const shadow = - "0px 4px 24px -4px rgba(16, 24, 40, 0.08), 0px 3px 3px -3px rgba(16, 24, 40, 0.03)"; - -// Global Font Family -const fontFamilyDefault = - '"Inter","system-ui", "Avenir", "Helvetica", "Arial", sans-serif'; - -const theme = createTheme({ - typography: { fontFamily: fontFamilyDefault, fontSize: 13 }, - palette: { - primary: { - main: primaryMain, - }, - secondary: { - main: secondaryMain, - }, - tertiary: { - main: tertiaryMain, - linkHover: tertiaryLinkHover, - }, - labelOrange: { - color: labelOrange, - }, - labelGray: { - color: labelGray, - }, - labelPurple: { - color: labelPurple, - }, - labelGreen: { - color: labelGreen, - }, - labelRed: { - color: labelRed, - }, - section: { - borderColor: sectionBorder, - bgColor: sectionBg, - }, - otherColors: { - blackish: otherColorsBlackish, - bluishGray: otherColorsBluishGray, - graishWhite: otherColorsGraishWhite, - purple: otherColorsPurple, - white: otherColorsWhite, - graishWhiteLight: otherColorsGraishWhiteLight, - strongBlue: otherColorsStrongBlue, - slateGray: otherColorsSlateGray, - fillGray: otherColorsFillGray, - }, - }, - shape: { - borderRadius: 4, - borderThick: 2, - boxShadow: shadow, - }, - content: { - pX: "80px", - pY: "40px", - }, - gap: { - xs: "4px", - small: "8px", - medium: "12px", - ml: "16px", - mlplus: "20px", - large: "24px", - lgplus: "32px", - xl: "40px", - xxl: "60px", - triplexl: "120px", - }, - alert: { - info: { - color: secondaryMain, - bg: otherColorsWhite, - border: sectionBorder, - }, - error: { - color: "#d92d20", - bg: "hsla(0, 100%, 52%, 0.03)", - border: "#f04438", - }, - warning: { - color: "#DC6803", - bg: "#fffcf5", - border: "#fec84b", - }, - }, - //add more as needed - label: { - up: { - borderColor: "#D4F4E1", - bgColor: "#ecfdf3", - dotColor: "#17B26A", - }, - down: { - borderColor: "#fbd1d1", - bgColor: "#f9eced", - dotColor: "#f04438", - }, - "cannot resolve": { - borderColor: "#e2eaf7", - bgColor: "#f2f4f7", - dotColor: "#4e5ba6", - }, - }, - pie: { - green: { - stroke: "#17b26a", - strokeBg: "#d4f4e1", - text: "#079455", - bg: "#ecfdf3", - shape: "circle", - }, - yellow: { - stroke: "#fdb022", - strokeBg: "rgba(255, 192, 34, 0.3)", - text: "#dc6803", - bg: "#fffcf5", - shape: "square", - }, - red: { - stroke: "#f04438", - strokeBg: "#ffecea", - text: "#f04438", - bg: "#ffeaea", - shape: "triangle", - }, - default: { - stroke: "#4e5ba6", - strokeBg: "#f2f4f7", - text: "#4e5ba6", - bg: "#f2f4f7", - shape: "", - }, - }, -}); - -export default theme; diff --git a/Client/src/Utils/Theme/darkTheme.js b/Client/src/Utils/Theme/darkTheme.js new file mode 100644 index 000000000..15df7c1b8 --- /dev/null +++ b/Client/src/Utils/Theme/darkTheme.js @@ -0,0 +1,216 @@ +import { createTheme } from "@mui/material"; + +const text = { + primary: "#fafafa", + secondary: "#e6e6e6", + tertiary: "#a1a1aa", + accent: "#e6e6e6", + disabled: "rgba(172, 172, 172, 0.3)", +}; +const background = { + main: "#151518", + alt: "#09090b", + fill: "#2e2e2e", + accent: "#18181a", +}; +const border = { light: "#27272a", dark: "#2c2c2c" }; + +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)"; + +const darkTheme = createTheme({ + typography: { fontFamily: fontFamilyDefault, fontSize: 13 }, + palette: { + mode: "dark", + primary: { main: "#1570ef" }, + secondary: { main: "#2e2e2e" }, + text: text, + background: background, + border: border, + info: { + text: text.primary, + main: text.secondary, + bg: background.main, + light: background.main, + border: border.light, + }, + success: { + text: "#079455", + main: "#45bb7a", + light: "#1e1e1e", + bg: "#27272a", + }, + error: { + text: "#f04438", + main: "#d32f2f", + light: "#1e1e1e", + bg: "#27272a", + dark: "#932020", + border: "#f04438", + }, + warning: { + text: "#e88c30", + main: "#FF9F00", + light: "#27272a", + bg: "#1E1E1E", + border: "#e88c30", + }, + unresolved: { main: "#4e5ba6", light: "#e2eaf7", bg: "#f2f4f7" }, + divider: border.light, + other: { + icon: "#e6e6e6", + line: "#27272a", + fill: "#18181a", + grid: "#454546", + }, + }, + spacing: 2, + components: { + MuiButton: { + defaultProps: { + disableRipple: true, + }, + styleOverrides: { + root: ({ theme }) => ({ + variants: [ + { + props: (props) => props.variant === "group", + style: { + color: theme.palette.secondary.contrastText, + backgroundColor: theme.palette.background.main, + border: 1, + borderStyle: "solid", + borderColor: theme.palette.border.light, + }, + }, + { + props: (props) => + props.variant === "group" && props.filled === "true", + style: { + backgroundColor: theme.palette.secondary.main, + }, + }, + ], + fontWeight: 400, + borderRadius: 4, + boxShadow: "none", + textTransform: "none", + "&:focus": { + outline: "none", + }, + "&:hover": { + boxShadow: "none", + }, + }), + }, + }, + MuiIconButton: { + styleOverrides: { + root: { + padding: 4, + transition: "none", + "&:hover": { + backgroundColor: background.fill, + }, + }, + }, + }, + MuiPaper: { + styleOverrides: { + root: { + marginTop: 4, + padding: 0, + border: 1, + borderStyle: "solid", + borderColor: border.light, + borderRadius: 4, + boxShadow: shadow, + backgroundColor: background.main, + backgroundImage: "none", + }, + }, + }, + MuiList: { + styleOverrides: { + root: { + padding: 0, + }, + }, + }, + MuiListItemButton: { + styleOverrides: { + root: { + transition: "none", + }, + }, + }, + MuiMenuItem: { + styleOverrides: { + root: { + borderRadius: 4, + backgroundColor: "inherit", + padding: "4px 6px", + color: text.secondary, + fontSize: 13, + margin: 2, + minWidth: 100, + "&:hover, &.Mui-selected, &.Mui-selected:hover, &.Mui-selected.Mui-focusVisible": + { + backgroundColor: background.fill, + }, + }, + }, + }, + MuiTableCell: { + styleOverrides: { + root: { + borderBottomColor: border.light, + }, + }, + }, + MuiTableHead: { + styleOverrides: { + root: { + backgroundColor: background.accent, + }, + }, + }, + MuiPagination: { + styleOverrides: { + root: { + backgroundColor: background.main, + border: 1, + borderStyle: "solid", + borderColor: border.light, + "& button": { + color: text.tertiary, + borderRadius: 4, + }, + "& li:first-of-type button, & li:last-of-type button": { + border: 1, + borderStyle: "solid", + borderColor: border.light, + }, + }, + }, + }, + MuiPaginationItem: { + styleOverrides: { + root: { + "&:not(.MuiPaginationItem-ellipsis):hover, &.Mui-selected": { + backgroundColor: background.fill, + }, + }, + }, + }, + }, + shape: { + borderRadius: 2, + borderThick: 2, + boxShadow: shadow, + }, +}); + +export default darkTheme; diff --git a/Client/src/Utils/Theme/lightTheme.js b/Client/src/Utils/Theme/lightTheme.js new file mode 100644 index 000000000..bb83d09b2 --- /dev/null +++ b/Client/src/Utils/Theme/lightTheme.js @@ -0,0 +1,212 @@ +import { createTheme } from "@mui/material"; + +const text = { + primary: "#1c2130", + secondary: "#344054", + tertiary: "#475467", + accent: "#838c99", +}; +const background = { + main: "#FFFFFF", + alt: "#FCFCFD", + fill: "#F4F4F4", + accent: "#f9fafb", +}; +const border = { light: "#eaecf0", dark: "#d0d5dd" }; + +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)"; + +const lightTheme = createTheme({ + typography: { fontFamily: fontFamilyDefault, fontSize: 13 }, + palette: { + primary: { main: "#1570EF" }, + secondary: { main: "#F4F4F4", dark: "#e3e3e3", contrastText: "#475467" }, + text: text, + background: background, + border: border, + info: { + text: text.primary, + main: text.tertiary, + bg: background.main, + light: background.main, + border: border.dark, + }, + success: { + text: "#079455", + main: "#17b26a", + light: "#d4f4e1", + bg: "#ecfdf3", + }, + error: { + text: "#f04438", + main: "#d32f2f", + light: "#fbd1d1", + bg: "#f9eced", + border: "#f04438", + }, + warning: { + text: "#DC6803", + main: "#fdb022", + light: "#fffcf5", + bg: "#ffecbc", + border: "#fec84b", + }, + unresolved: { main: "#4e5ba6", light: "#e2eaf7", bg: "#f2f4f7" }, + divider: border.light, + other: { + icon: "#667085", + line: "#d6d9dd", + fill: "#e3e3e3", + grid: "#a2a3a3", + }, + }, + spacing: 2, + components: { + MuiButton: { + defaultProps: { + disableRipple: true, + }, + styleOverrides: { + root: ({ theme }) => ({ + variants: [ + { + props: (props) => props.variant === "group", + style: { + color: theme.palette.secondary.contrastText, + backgroundColor: theme.palette.background.main, + border: 1, + borderStyle: "solid", + borderColor: theme.palette.border.light, + }, + }, + { + props: (props) => + props.variant === "group" && props.filled === "true", + style: { + backgroundColor: theme.palette.secondary.main, + }, + }, + ], + fontWeight: 400, + borderRadius: 4, + boxShadow: "none", + textTransform: "none", + "&:focus": { + outline: "none", + }, + "&:hover": { + boxShadow: "none", + }, + }), + }, + }, + MuiIconButton: { + styleOverrides: { + root: { + padding: 4, + transition: "none", + "&:hover": { + backgroundColor: background.fill, + }, + }, + }, + }, + MuiPaper: { + styleOverrides: { + root: { + marginTop: 4, + border: 1, + borderStyle: "solid", + borderColor: border.light, + borderRadius: 4, + boxShadow: shadow, + backgroundColor: background.main, + }, + }, + }, + MuiList: { + styleOverrides: { + root: { + padding: 0, + }, + }, + }, + MuiListItemButton: { + styleOverrides: { + root: { + transition: "none", + }, + }, + }, + MuiMenuItem: { + styleOverrides: { + root: { + borderRadius: 4, + backgroundColor: "inherit", + padding: "4px 6px", + color: text.secondary, + fontSize: 13, + margin: 2, + marginBottom: 0, + minWidth: 100, + "&:hover, &.Mui-selected, &.Mui-selected:hover, &.Mui-selected.Mui-focusVisible": + { + backgroundColor: background.fill, + }, + }, + }, + }, + MuiTableCell: { + styleOverrides: { + root: { + borderBottomColor: border.light, + }, + }, + }, + MuiTableHead: { + styleOverrides: { + root: { + backgroundColor: background.accent, + }, + }, + }, + MuiPagination: { + styleOverrides: { + root: { + backgroundColor: background.main, + border: 1, + borderStyle: "solid", + borderColor: border.light, + "& button": { + color: text.tertiary, + borderRadius: 4, + }, + "& li:first-of-type button, & li:last-of-type button": { + border: 1, + borderStyle: "solid", + borderColor: border.light, + }, + }, + }, + }, + MuiPaginationItem: { + styleOverrides: { + root: { + "&:not(.MuiPaginationItem-ellipsis):hover, &.Mui-selected": { + backgroundColor: background.fill, + }, + }, + }, + }, + }, + shape: { + borderRadius: 2, + borderThick: 2, + boxShadow: shadow, + }, +}); + +export default lightTheme; diff --git a/Client/src/Utils/greeting.jsx b/Client/src/Utils/greeting.jsx new file mode 100644 index 000000000..cb878c991 --- /dev/null +++ b/Client/src/Utils/greeting.jsx @@ -0,0 +1,192 @@ +import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; +import { Box, Typography } from "@mui/material"; +import { useDispatch, useSelector } from "react-redux"; +import { useEffect } from "react"; +import { setGreeting } from "../Features/UI/uiSlice"; + +const early = [ + { + prepend: "Rise and shine", + append: "If you’re up this early, you might as well be a legend!", + emoji: "☕", + }, + { + prepend: "Good morning", + append: "The world’s still asleep, but you’re already awesome!", + emoji: "🦉", + }, + { + prepend: "Good morning", + append: "Are you a wizard? Only magical people are up at this hour!", + emoji: "🌄", + }, + { + prepend: "Up before the roosters", + append: "Ready to tackle the day before it even starts?", + emoji: "🐓", + }, + { + prepend: "Early bird special", + append: "Let’s get things done while everyone else is snoozing!", + emoji: "🌟", + }, +]; + +const morning = [ + { + prepend: "Good morning", + append: "Is it coffee o’clock yet, or should we start with high fives?", + emoji: "☕", + }, + { + prepend: "Morning", + append: "The sun is up, and so are you—time to be amazing!", + emoji: "🌞", + }, + { + prepend: "Good morning", + append: "Time to make today the best thing since sliced bread!", + emoji: "🥐", + }, + { + prepend: "Morning", + append: "Let’s kick off the day with more energy than a double espresso!", + emoji: "🚀", + }, + { + prepend: "Rise and shine", + append: "You’re about to make today so great, even Monday will be jealous!", + emoji: "🌟", + }, +]; + +const afternoon = [ + { + prepend: "Good afternoon", + append: "How about a break to celebrate how awesome you’re doing?", + emoji: "🥪", + }, + { + prepend: "Afternoon", + append: "If you’re still going strong, you’re officially a rockstar!", + emoji: "🌞", + }, + { + prepend: "Hey there", + append: "The afternoon is your playground—let’s make it epic!", + emoji: "🍕", + }, + { + prepend: "Good afternoon", + append: "Time to crush the rest of the day like a pro!", + emoji: "🏆", + }, + { + prepend: "Afternoon", + append: "Time to turn those afternoon slumps into afternoon triumphs!", + emoji: "🎉", + }, +]; + +const evening = [ + { + prepend: "Good evening", + append: "Time to wind down and think about how you crushed today!", + emoji: "🌇", + }, + { + prepend: "Evening", + append: "You’ve earned a break—let’s make the most of these evening vibes!", + emoji: "🍹", + }, + { + prepend: "Hey there", + append: "Time to relax and bask in the glow of your day’s awesomeness!", + emoji: "🌙", + }, + { + prepend: "Good evening", + append: "Ready to trade productivity for chill mode?", + emoji: "🛋️ ", + }, + { + prepend: "Evening", + append: "Let’s call it a day and toast to your success!", + emoji: "🕶️", + }, +]; + +/** + * Greeting component that displays a personalized greeting message + * based on the time of day and the user's first name. + * + * @component + * @example + * return ; + * + * @param {Object} props + * @param {string} props.type - The type of monitor to be displayed in the message + * @returns {JSX.Element} The rendered Greeting component + */ + +const Greeting = ({ type = "" }) => { + const theme = useTheme(); + const dispatch = useDispatch(); + const { firstName } = useSelector((state) => state.auth.user); + const index = useSelector((state) => state.ui.greeting.index); + const lastUpdate = useSelector((state) => state.ui.greeting.lastUpdate); + + const now = new Date(); + const hour = now.getHours(); + + useEffect(() => { + const hourDiff = lastUpdate ? hour - lastUpdate : null; + + if (!lastUpdate || hourDiff >= 1) { + let random = Math.floor(Math.random() * 5); + dispatch(setGreeting({ index: random, lastUpdate: hour })); + } + }, [dispatch, hour]); + + let greetingArray = + hour < 6 ? early : hour < 12 ? morning : hour < 18 ? afternoon : evening; + const { prepend, append, emoji } = greetingArray[index]; + + return ( + + + + {prepend},{" "} + + + {firstName} {emoji} + + + + {append} — Here’s an overview of your {type} monitors. + + + ); +}; + +Greeting.propTypes = { + type: PropTypes.string, +}; + +export default Greeting; diff --git a/Client/src/Utils/toastUtils.jsx b/Client/src/Utils/toastUtils.jsx index ff3f341f8..0383ba466 100644 --- a/Client/src/Utils/toastUtils.jsx +++ b/Client/src/Utils/toastUtils.jsx @@ -19,7 +19,7 @@ export const createToast = ({ config = {}, }) => { const toastConfig = { - position: "bottom-right", + position: "top-right", autoClose: 3000, hideProgressBar: true, closeButton: false, diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index 6aa7107b6..948c228cc 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -76,6 +76,7 @@ const credentials = joi.object({ return value; }), role: joi.array(), + teamId: joi.string().allow("").optional(), }); const monitorValidation = joi.object({ @@ -135,9 +136,9 @@ const maintenanceWindowValidation = joi.object({ "any.only": "Unit must be one of ['minutes', 'hours', 'days'].", "any.required": "Unit is required.", }), - friendlyName: joi.string().max(50).required().messages({ - "string.empty": "Friendly name is required.", - "string.max": "Friendly name must be less than 50 characters long", + displayName: joi.string().max(50).required().messages({ + "string.empty": "Display name is required.", + "string.max": "Display name must be less than 50 characters long", }), addMonitors: joi.string().max(50).required().messages({ "string.empty": "Add monitors is required.", diff --git a/Client/src/assets/Images/create-placeholder-dark.svg b/Client/src/assets/Images/create-placeholder-dark.svg new file mode 100644 index 000000000..2dd68750b --- /dev/null +++ b/Client/src/assets/Images/create-placeholder-dark.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Client/src/assets/icons/dots-vertical.svg b/Client/src/assets/icons/dots-vertical.svg new file mode 100644 index 000000000..e720066c4 --- /dev/null +++ b/Client/src/assets/icons/dots-vertical.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Client/src/assets/icons/radio-checked.svg b/Client/src/assets/icons/radio-checked.svg new file mode 100644 index 000000000..1c584b79a --- /dev/null +++ b/Client/src/assets/icons/radio-checked.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Client/src/index.css b/Client/src/index.css index d513ac9a1..105776bd8 100644 --- a/Client/src/index.css +++ b/Client/src/index.css @@ -7,59 +7,12 @@ :root { font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; font-weight: 400; - - /* color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; */ - font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; /* Generalized Stylings */ - - --env-var-font-familt-1: "Roboto", "Helvetica", "Arial", sans-serif; - - --env-var-color-0: #fff; - --env-var-color-1: #1c2130; - --env-var-color-2: #475467; - --env-var-color-2-light: #838c99; - --env-var-color-3: #1570ef; - - --env-var-color-4: #d0d5dd; - --env-var-color-5: #344054; - --env-var-color-6: #eaecf0; - --env-var-color-7: #7f56d9; - --env-var-color-8: #fff; - --env-var-color-9: #f2f2f2; - --env-var-color-10: #175cd3; - --env-var-color-11: #5d6b98; - --env-var-color-12: #182230; - --env-var-color-13: #f9fafb; - --env-var-color-14: #98a2b3; - --env-var-color-15: #f2f4f7; - --env-var-color-16: #ebebeb; - --env-var-color-17: #079455; - --env-var-color-18: #ffc301; - --env-var-color-19: #f04438; - --env-var-color-20: #ecfdf3; - --env-var-color-21: hsla(0, 100%, 52%, 0.06); - --env-var-color-22: #4e5ba6; - --env-var-color-23: #17b26a; - --env-var-color-24: #d92d20; - --env-var-color-25: #667085; - --env-var-color-26: #ff1818; - - --env-var-color-27: #fecf60; - --lighter-env-var-color-27: rgba(254, 207, 96, 0.1); - --env-var-color-28: #f79009; - --env-var-color-29: #d0d5dd; - --env-var-color-30: #fcfcfd; - --env-var-color-31: #1a1919; - --env-var-color-32: #f5f5f5; - --env-var-color-33: rgba(83, 83, 83, 0.1); - --env-var-radius-1: 4px; --env-var-radius-2: 8px; @@ -99,48 +52,12 @@ 0px 3px 3px -3px rgba(16, 24, 40, 0.03); } -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} - -a:hover { - color: #535bf2; -} - body { margin: 0; min-width: 320px; height: 100%; } -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} - -button:hover { - border-color: #646cff; -} - -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - .MuiInputBase-root.Mui-disabled input { cursor: not-allowed; } @@ -150,6 +67,8 @@ button:focus-visible { } .Toastify__toast-body .alert { min-width: 150px; + padding: 5px 10px; + align-items: center; } .Toastify [class^="Toastify__toast"] { padding: 0; @@ -159,13 +78,16 @@ button:focus-visible { min-height: 0; border-radius: 4px; } +.Toastify [class*="Toastify__toast-theme"] { + background-color: transparent; +} .MuiTouchRipple-root { display: none; } body .MuiSkeleton-root { - background-color: var(--env-var-color-15); + background-color: #f2f4f7; } @keyframes ripple { @@ -178,18 +100,3 @@ body .MuiSkeleton-root { transform: scale(2); } } - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - - a:hover { - color: #747bff; - } - - button { - background-color: #f9f9f9; - } -} diff --git a/Client/src/main.jsx b/Client/src/main.jsx index 00cec7d14..d592f9137 100644 --- a/Client/src/main.jsx +++ b/Client/src/main.jsx @@ -3,8 +3,6 @@ import ReactDOM from "react-dom/client"; import App from "./App.jsx"; import "./index.css"; import { BrowserRouter as Router, HashRouter } from "react-router-dom"; -import theme from "./Utils/Theme.js"; -import { ThemeProvider } from "@mui/material"; import { Provider } from "react-redux"; import { persistor, store } from "./store"; import { PersistGate } from "redux-persist/integration/react"; @@ -13,11 +11,9 @@ export const networkService = new NetworkService(store); ReactDOM.createRoot(document.getElementById("root")).render( - - - - - + + + ); diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index 6fbcdb669..0cf297a47 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -1,6 +1,6 @@ const express = require("express"); const { - registerValidation, + registrationBodyValidation, loginValidation, editUserParamValidation, editUserBodyValidation, @@ -50,7 +50,7 @@ const getTokenFromHeaders = (headers) => { const registerController = async (req, res, next) => { // joi validation try { - await registerValidation.validateAsync(req.body); + await registrationBodyValidation.validateAsync(req.body); } catch (error) { error.status = 422; error.service = SERVICE_NAME; @@ -78,7 +78,7 @@ const registerController = async (req, res, next) => { // Create a new user try { - const newUser = await req.db.insertUser(req, res); + const newUser = await req.db.insertUser({ ...req.body }, req.file); logger.info(successMessages.AUTH_CREATE_USER, { service: SERVICE_NAME, userId: newUser._id, @@ -90,12 +90,20 @@ const registerController = async (req, res, next) => { const token = issueToken(userForToken); - await req.emailService.buildAndSendEmail( - "welcomeEmailTemplate", - { name: newUser.firstName }, - newUser.email, - "Welcome to Uptime Monitor" - ); + req.emailService + .buildAndSendEmail( + "welcomeEmailTemplate", + { name: newUser.firstName }, + newUser.email, + "Welcome to Uptime Monitor" + ) + .catch((error) => { + logger.error("Error sending welcome email", { + service: SERVICE_NAME, + error: error.message, + }); + }); + return res.status(200).json({ success: true, msg: successMessages.AUTH_CREATE_USER, @@ -143,8 +151,7 @@ const loginController = async (req, res, next) => { // Happy path, return token const token = issueToken(userWithoutPassword); - user.authToken = token; - await user.save(); + // reset avatar image userWithoutPassword.avatarImage = user.avatarImage; @@ -226,7 +233,8 @@ const inviteController = async (req, res, next) => { try { // Only admins can invite const token = getTokenFromHeaders(req.headers); - const { role, firstname } = jwt.decode(token); + const { role, firstname, teamId } = jwt.decode(token); + req.body.teamId = teamId; try { await inviteRoleValidation.validateAsync({ roles: role }); await inviteBodyValidation.validateAsync(req.body); @@ -240,15 +248,22 @@ const inviteController = async (req, res, next) => { } const inviteToken = await req.db.requestInviteToken(req, res); - await req.emailService.buildAndSendEmail( - "employeeActivationTemplate", - { - name: firstname, - link: `${process.env.CLIENT_HOST}/register/${inviteToken.token}`, - }, - req.body.email, - "Welcome to Uptime Monitor" - ); + req.emailService + .buildAndSendEmail( + "employeeActivationTemplate", + { + name: firstname, + link: `${process.env.CLIENT_HOST}/register/${inviteToken.token}`, + }, + req.body.email, + "Welcome to Uptime Monitor" + ) + .catch((error) => { + logger.error("Error sending invite email", { + service: SERVICE_NAME, + error: error.message, + }); + }); return res .status(200) @@ -274,7 +289,6 @@ const inviteVerifyController = async (req, res, next) => { try { const invite = await req.db.getInviteToken(req, res); - res .status(200) .json({ status: "success", msg: "Invite verified", data: invite }); @@ -293,13 +307,13 @@ const inviteVerifyController = async (req, res, next) => { * @returns {Promise} */ -const checkAdminController = async (req, res) => { +const checkSuperadminController = async (req, res) => { try { - const adminExists = await req.db.checkAdmin(req, res); + const superAdminExists = await req.db.checkSuperadmin(req, res); return res.status(200).json({ success: true, msg: successMessages.AUTH_ADMIN_EXISTS, - data: adminExists, + data: superAdminExists, }); } catch (error) { error.service = SERVICE_NAME; @@ -508,7 +522,7 @@ module.exports = { userEditController, inviteController, inviteVerifyController, - checkAdminController, + checkSuperadminController, recoveryRequestController, validateRecoveryTokenController, resetPasswordController, diff --git a/Server/controllers/checkController.js b/Server/controllers/checkController.js index 957eeed4e..85bce0b54 100644 --- a/Server/controllers/checkController.js +++ b/Server/controllers/checkController.js @@ -3,9 +3,10 @@ const { createCheckBodyValidation, getChecksParamValidation, getChecksQueryValidation, - getUserChecksParamValidation, - getUserChecksQueryValidation, + getTeamChecksParamValidation, + getTeamChecksQueryValidation, deleteChecksParamValidation, + deleteChecksByTeamIdParamValidation, } = require("../validation/joi"); const { successMessages } = require("../utils/messages"); const SERVICE_NAME = "check"; @@ -62,10 +63,10 @@ const getChecks = async (req, res, next) => { } }; -const getUserChecks = async (req, res, next) => { +const getTeamChecks = async (req, res, next) => { try { - await getUserChecksParamValidation.validateAsync(req.params); - await getUserChecksQueryValidation.validateAsync(req.query); + await getTeamChecksParamValidation.validateAsync(req.params); + await getTeamChecksQueryValidation.validateAsync(req.query); } catch (error) { error.status = 422; error.service = SERVICE_NAME; @@ -75,7 +76,7 @@ const getUserChecks = async (req, res, next) => { return; } try { - const checkData = await req.db.getUserChecks(req); + const checkData = await req.db.getTeamChecks(req); return res.status(200).json({ success: true, msg: successMessages.CHECK_GET, @@ -112,9 +113,34 @@ const deleteChecks = async (req, res, next) => { } }; +const deleteChecksByTeamId = async (req, res, next) => { + try { + await deleteChecksByTeamIdParamValidation.validateAsync(req.params); + } catch (error) { + error.service = SERVICE_NAME; + error.method = "deleteChecksByTeam"; + error.status = 422; + next(error); + } + + try { + const deletedCount = await req.db.deleteChecksByTeamId(req.params.teamId); + return res.status(200).json({ + success: true, + msg: successMessages.CHECK_DELETE, + data: { deletedCount }, + }); + } catch (error) { + error.service = SERVICE_NAME; + error.method = "deleteChecksByTeamId"; + next(error); + } +}; + module.exports = { createCheck, getChecks, - getUserChecks, + getTeamChecks, deleteChecks, + deleteChecksByTeamId, }; diff --git a/Server/controllers/inviteController.js b/Server/controllers/inviteController.js new file mode 100644 index 000000000..e685d0eef --- /dev/null +++ b/Server/controllers/inviteController.js @@ -0,0 +1,96 @@ +const { + inviteRoleValidation, + inviteBodyValidation, + inviteVerifciationBodyValidation, +} = require("../validation/joi"); +const logger = require("../utils/logger"); +require("dotenv").config(); +const { errorMessages, successMessages } = require("../utils/messages"); +var jwt = require("jsonwebtoken"); +const SERVICE_NAME = "inviteController"; + +const getTokenFromHeaders = (headers) => { + const authorizationHeader = headers.authorization; + if (!authorizationHeader) throw new Error("No auth headers"); + + const parts = authorizationHeader.split(" "); + if (parts.length !== 2 || parts[0] !== "Bearer") + throw new Error("Invalid auth headers"); + + return parts[1]; +}; + +const inviteController = async (req, res, next) => { + try { + // Only admins can invite + const token = getTokenFromHeaders(req.headers); + const { role, firstname, teamId } = jwt.decode(token); + req.body.teamId = teamId; + try { + await inviteRoleValidation.validateAsync({ roles: role }); + await inviteBodyValidation.validateAsync(req.body); + } catch (error) { + error.status = 422; + error.service = SERVICE_NAME; + error.message = + error.details?.[0]?.message || error.message || "Validation Error"; + next(error); + return; + } + + const inviteToken = await req.db.requestInviteToken({ ...req.body }); + req.emailService + .buildAndSendEmail( + "employeeActivationTemplate", + { + name: firstname, + link: `${process.env.CLIENT_HOST}/register/${inviteToken.token}`, + }, + req.body.email, + "Welcome to Uptime Monitor" + ) + .catch((error) => { + logger.error("Error sending invite email", { + service: SERVICE_NAME, + error: error.message, + }); + }); + + return res + .status(200) + .json({ success: true, msg: "Invite sent", data: inviteToken }); + } catch (error) { + error.service = SERVICE_NAME; + next(error); + return; + } +}; + +const inviteVerifyController = async (req, res, next) => { + try { + await inviteVerifciationBodyValidation.validateAsync(req.body); + } catch (error) { + error.status = 422; + error.service = SERVICE_NAME; + error.message = + error.details?.[0]?.message || error.message || "Validation Error"; + next(error); + return; + } + + try { + const invite = await req.db.getInviteToken(req.body.token); + res + .status(200) + .json({ status: "success", msg: "Invite verified", data: invite }); + } catch (error) { + error.service = SERVICE_NAME; + next(error); + return; + } +}; + +module.exports = { + inviteController, + inviteVerifyController, +}; diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index 37aff6c96..f33ddcf10 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -1,10 +1,11 @@ const { getMonitorByIdParamValidation, getMonitorByIdQueryValidation, - getMonitorsByUserIdValidation, + getMonitorsByTeamIdValidation, createMonitorBodyValidation, editMonitorBodyValidation, - getMonitorsByUserIdQueryValidation, + getMonitorsByTeamIdQueryValidation, + pauseMonitorParamValidation, } = require("../validation/joi"); const sslChecker = require("ssl-checker"); @@ -123,7 +124,7 @@ const getMonitorById = async (req, res, next) => { } try { - const monitor = await req.db.getMonitorById(req, res); + const monitor = await req.db.getMonitorById(req.params.monitorId); if (!monitor) { const error = new Error(errorMessages.MONITOR_GET_BY_ID); error.status = 404; @@ -149,10 +150,10 @@ const getMonitorById = async (req, res, next) => { * @returns {Promise} * @throws {Error} */ -const getMonitorsByUserId = async (req, res, next) => { +const getMonitorsByTeamId = async (req, res, next) => { try { - await getMonitorsByUserIdValidation.validateAsync(req.params); - await getMonitorsByUserIdQueryValidation.validateAsync(req.query); + await getMonitorsByTeamIdValidation.validateAsync(req.params); + await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); } catch (error) { error.status = 422; error.service = SERVICE_NAME; @@ -163,12 +164,12 @@ const getMonitorsByUserId = async (req, res, next) => { } try { - const userId = req.params.userId; - const monitors = await req.db.getMonitorsByUserId(req, res); + const teamId = req.params.teamId; + const monitors = await req.db.getMonitorsByTeamId(req, res); return res.json({ success: true, - msg: successMessages.MONITOR_GET_BY_USER_ID(userId), + msg: successMessages.MONITOR_GET_BY_USER_ID(teamId), data: monitors, }); } catch (error) { @@ -204,12 +205,14 @@ const createMonitor = async (req, res, next) => { const monitor = await req.db.createMonitor(req, res); if (notifications && notifications.length !== 0) { - await Promise.all( + const setNotifications = await Promise.all( notifications.map(async (notification) => { notification.monitorId = monitor._id; await req.db.createNotification(notification); }) ); + monitor.notifications = setNotifications; + await monitor.save(); } // Add monitor to job queue req.jobQueue.addJob(monitor._id, monitor); @@ -309,7 +312,7 @@ const editMonitor = async (req, res, next) => { // Get notifications from the request body const notifications = req.body.notifications; - const editedMonitor = await req.db.editMonitor(req, res); + const editedMonitor = await req.db.editMonitor(monitorId, req.body); await req.db.deleteNotificationsByMonitorId(editedMonitor._id); @@ -337,14 +340,49 @@ const editMonitor = async (req, res, next) => { } }; +const pauseMonitor = async (req, res, next) => { + try { + await pauseMonitorParamValidation.validateAsync(req.params); + } catch (error) { + error.status = 422; + error.service = SERVICE_NAME; + error.message = + error.details?.[0]?.message || error.message || "Validation Error"; + next(error); + } + + try { + const monitor = await req.db.getMonitorById(req.params.monitorId); + if (monitor.isActive) { + await req.jobQueue.deleteJob(monitor); + } else { + await req.jobQueue.addJob(monitor._id, monitor); + } + monitor.isActive = !monitor.isActive; + monitor.save(); + return res.status(200).json({ + success: true, + msg: monitor.isActive + ? successMessages.MONITOR_RESUME + : successMessages.MONITOR_PAUSE, + data: monitor, + }); + } catch (error) { + error.service = SERVICE_NAME; + error.method = "pauseMonitor"; + next(error); + } +}; + module.exports = { getAllMonitors, getMonitorStatsById, getMonitorCertificate, getMonitorById, - getMonitorsByUserId, + getMonitorsByTeamId, createMonitor, deleteMonitor, deleteAllMonitors, editMonitor, + pauseMonitor, }; diff --git a/Server/db/mongo/MongoDB.js b/Server/db/mongo/MongoDB.js index 9c1005c32..912bfb438 100644 --- a/Server/db/mongo/MongoDB.js +++ b/Server/db/mongo/MongoDB.js @@ -15,10 +15,10 @@ const connect = async () => { } }; -const checkAdmin = async (req, res) => { +const checkSuperadmin = async (req, res) => { try { - const admin = await UserModel.findOne({ role: "admin" }); - if (admin !== null) { + const superAdmin = await UserModel.findOne({ role: "superadmin" }); + if (superAdmin !== null) { return true; } return false; @@ -66,7 +66,7 @@ const { getAllMonitors, getMonitorStatsById, getMonitorById, - getMonitorsByUserId, + getMonitorsByTeamId, createMonitor, deleteMonitor, deleteAllMonitors, @@ -92,8 +92,9 @@ const { createCheck, getChecksCount, getChecks, - getUserChecks, + getTeamChecks, deleteChecks, + deleteChecksByTeamId, } = require("./modules/checkModule"); //**************************************** @@ -141,11 +142,11 @@ module.exports = { requestRecoveryToken, validateRecoveryToken, resetPassword, - checkAdmin, + checkSuperadmin, getAllMonitors, getMonitorStatsById, getMonitorById, - getMonitorsByUserId, + getMonitorsByTeamId, createMonitor, deleteMonitor, deleteAllMonitors, @@ -153,8 +154,9 @@ module.exports = { createCheck, getChecksCount, getChecks, - getUserChecks, + getTeamChecks, deleteChecks, + deleteChecksByTeamId, createAlert, getAlertsByUserId, getAlertsByMonitorId, diff --git a/Server/db/mongo/modules/checkModule.js b/Server/db/mongo/modules/checkModule.js index 782e5f64d..32968431d 100644 --- a/Server/db/mongo/modules/checkModule.js +++ b/Server/db/mongo/modules/checkModule.js @@ -22,7 +22,27 @@ const dateRangeLookup = { const createCheck = async (checkData) => { try { + const { monitorId, status } = checkData; + + const n = await Check.countDocuments({ monitorId }) + 1; + const check = await new Check({ ...checkData }).save(); + + const monitor = await Monitor.findById(monitorId); + + if (!monitor) { + throw new Error("Monitor not found"); + } + + if (monitor.uptimePercentage === undefined) { + monitor.uptimePercentage = (status === true) ? 1 : 0; + } else { + monitor.uptimePercentage = + (monitor.uptimePercentage * (n - 1) + (status === true ? 1: 0)) / n; + } + + await monitor.save(); + return check; } catch (error) { throw error; @@ -116,12 +136,12 @@ const getChecks = async (req) => { } }; -const getUserChecks = async (req) => { - const { userId } = req.params; +const getTeamChecks = async (req) => { + const { teamId } = req.params; let { sortOrder, limit, dateRange, filter, page, rowsPerPage } = req.query; // Get monitorIDs - const userMonitors = await Monitor.find({ userId: userId }); + const userMonitors = await Monitor.find({ teamId: teamId }); const monitorIds = userMonitors.map((monitor) => monitor._id); //Build check query @@ -170,7 +190,7 @@ const getUserChecks = async (req) => { }; /** - * Delete all checks for a user + * Delete all checks for a monitor * @async * @param {string} monitorId * @returns {number} @@ -185,10 +205,40 @@ const deleteChecks = async (monitorId) => { throw error; } }; + +/** + * Delete all checks for a team + * @async + * @param {string} monitorId + * @returns {number} + * @throws {Error} + */ + +const deleteChecksByTeamId = async (teamId) => { + try { + const teamMonitors = await Monitor.find({ teamId: teamId }); + let totalDeletedCount = 0; + + await Promise.all( + teamMonitors.map(async (monitor) => { + const result = await Check.deleteMany({ monitorId: monitor._id }); + totalDeletedCount += result.deletedCount; + monitor.status = true; + await monitor.save(); + }) + ); + + return totalDeletedCount; + } catch (error) { + throw error; + } +}; + module.exports = { createCheck, getChecksCount, getChecks, - getUserChecks, + getTeamChecks, deleteChecks, + deleteChecksByTeamId, }; diff --git a/Server/db/mongo/modules/inviteModule.js b/Server/db/mongo/modules/inviteModule.js index b4404c615..975f6f1e8 100644 --- a/Server/db/mongo/modules/inviteModule.js +++ b/Server/db/mongo/modules/inviteModule.js @@ -2,14 +2,25 @@ const InviteToken = require("../../../models/InviteToken"); const crypto = require("crypto"); const { errorMessages } = require("../../../utils/messages"); -const requestInviteToken = async (req, res) => { +/** + * Request an invite token for a user. + * + * This function deletes any existing invite tokens for the user's email, + * generates a new token, saves it, and then returns the new token. + * + * @param {Object} userData - The user data. + * @param {string} userData.email - The user's email. + * @param {mongoose.Schema.Types.ObjectId} userData.teamId - The ID of the team. + * @param {Array} userData.role - The user's role(s). + * @param {Date} [userData.expiry=Date.now] - The expiry date of the token. Defaults to the current date and time. + * @returns {Promise} The invite token. + * @throws {Error} If there is an error. + */ +const requestInviteToken = async (userData) => { try { - await InviteToken.deleteMany({ email: req.body.email }); - let inviteToken = new InviteToken({ - email: req.body.email, - role: req.body.role, - token: crypto.randomBytes(32).toString("hex"), - }); + await InviteToken.deleteMany({ email: userData.email }); + userData.token = crypto.randomBytes(32).toString("hex"); + let inviteToken = new InviteToken(userData); await inviteToken.save(); return inviteToken; } catch (error) { @@ -17,10 +28,20 @@ const requestInviteToken = async (req, res) => { } }; -const getInviteToken = async (req, res) => { +/** + * Retrieves and deletes an invite token. + * + * This function searches for an invite token in the database and deletes it. + * If the invite token is not found, it throws an error. + * + * @param {string} token - The invite token to search for. + * @returns {Promise} The invite token data. + * @throws {Error} If the invite token is not found or there is another error. + */ +const getInviteToken = async (token) => { try { const invite = await InviteToken.findOneAndDelete({ - token: req.body.token, + token, }); if (invite === null) { throw new Error(errorMessages.AUTH_INVITE_NOT_FOUND); diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 9778b6224..79e8917ea 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -15,7 +15,7 @@ const { NormalizeData } = require("../../../utils/dataUtils"); */ const getAllMonitors = async (req, res) => { try { - const monitors = await Monitor.find({ isActive: true }); + const monitors = await Monitor.find(); return monitors; } catch (error) { throw error; @@ -280,24 +280,33 @@ const getMonitorStatsById = async (req) => { const getMonitorById = async (monitorId) => { try { const monitor = await Monitor.findById(monitorId); - return monitor; + if (monitor === null || monitor === undefined) { + throw new Error(errorMessages.DB_FIND_MONTIOR_BY_ID(monitorId)); + } + // Get notifications + const notifications = await Notification.find({ + monitorId: monitorId, + }); + monitor.notifications = notifications; + const monitorWithNotifications = await monitor.save(); + return monitorWithNotifications; } catch (error) { throw error; } }; /** - * Get monitors by UserID + * Get monitors by TeamID * @async * @param {Express.Request} req * @param {Express.Response} res * @returns {Promise>} * @throws {Error} */ -const getMonitorsByUserId = async (req, res) => { +const getMonitorsByTeamId = async (req, res) => { try { let { limit, type, status, sortOrder, normalize } = req.query || {}; - const monitorQuery = { userId: req.params.userId }; + const monitorQuery = { teamId: req.params.teamId }; if (type !== undefined) { const types = Array.isArray(type) ? type : [type]; @@ -340,12 +349,7 @@ const getMonitorsByUserId = async (req, res) => { if (normalize !== undefined) { checks = NormalizeData(checks, 10, 100); } - - // Get notifications - const notifications = await Notification.find({ - monitorId: monitor._id, - }); - return { ...monitor.toObject(), checks, notifications }; + return { ...monitor.toObject(), checks }; }) ); return monitorsWithChecks; @@ -367,7 +371,6 @@ const createMonitor = async (req, res) => { const monitor = new Monitor({ ...req.body }); // Remove notifications fom monitor as they aren't needed here monitor.notifications = undefined; - monitor.userId = req.user._id; await monitor.save(); return monitor; } catch (error) { @@ -432,9 +435,7 @@ const deleteMonitorsByUserId = async (userId) => { * @returns {Promise} * @throws {Error} */ -const editMonitor = async (req, res) => { - const candidateId = req.params.monitorId; - const candidateMonitor = req.body; +const editMonitor = async (candidateId, candidateMonitor) => { candidateMonitor.notifications = undefined; try { @@ -453,7 +454,7 @@ module.exports = { getAllMonitors, getMonitorStatsById, getMonitorById, - getMonitorsByUserId, + getMonitorsByTeamId, createMonitor, deleteMonitor, deleteAllMonitors, diff --git a/Server/db/mongo/modules/userModule.js b/Server/db/mongo/modules/userModule.js index 3dacfd943..1a5c19f36 100644 --- a/Server/db/mongo/modules/userModule.js +++ b/Server/db/mongo/modules/userModule.js @@ -1,4 +1,5 @@ const UserModel = require("../../../models/user"); +const TeamModel = require("../../../models/Team"); const { errorMessages } = require("../../../utils/messages"); const { GenerateAvatarImage } = require("../../../utils/imageProcessing"); @@ -13,20 +14,29 @@ const { ParseBoolean } = require("../../../utils/utils"); * @returns {Promise} * @throws {Error} */ -const insertUser = async (req, res) => { +const insertUser = async (userData, imageFile) => { try { - const userData = { ...req.body }; - if (req.file) { + if (imageFile) { // 1. Save the full size image userData.profileImage = { - data: req.file.buffer, - contentType: req.file.mimetype, + data: imageFile.buffer, + contentType: imageFile.mimetype, }; // 2. Get the avatar sized image - const avatar = await GenerateAvatarImage(req.file); + const avatar = await GenerateAvatarImage(imageFile); userData.avatarImage = avatar; } + + // Handle creating team if superadmin + if (userData.role.includes("superadmin")) { + const team = new TeamModel({ + email: userData.email, + }); + userData.teamId = team._id; + await team.save(); + } + const newUser = new UserModel(userData); await newUser.save(); return await UserModel.findOne({ _id: newUser._id }) @@ -79,7 +89,6 @@ const getUserByEmail = async (email) => { const updateUser = async (req, res) => { const candidateUserId = req.params.userId; - try { const candidateUser = { ...req.body }; // ****************************************** diff --git a/Server/index.js b/Server/index.js index 9a5f1797f..846b11e3d 100644 --- a/Server/index.js +++ b/Server/index.js @@ -7,6 +7,7 @@ const { verifyJWT } = require("./middleware/verifyJWT"); const { handleErrors } = require("./middleware/handleErrors"); const { errorMessages } = require("./utils/messages"); const authRouter = require("./routes/authRoute"); +const inviteRouter = require("./routes/inviteRoute"); const monitorRouter = require("./routes/monitorRoute"); const checkRouter = require("./routes/checkRoute"); const alertRouter = require("./routes/alertRoute"); @@ -71,6 +72,7 @@ const startApp = async () => { //routes app.use("/api/v1/auth", authRouter); + app.use("/api/v1/invite", inviteRouter); app.use("/api/v1/monitors", verifyJWT, monitorRouter); app.use("/api/v1/checks", verifyJWT, checkRouter); app.use("/api/v1/alerts", verifyJWT, alertRouter); diff --git a/Server/middleware/isAllowed.js b/Server/middleware/isAllowed.js new file mode 100644 index 000000000..fefb26099 --- /dev/null +++ b/Server/middleware/isAllowed.js @@ -0,0 +1,53 @@ +const jwt = require("jsonwebtoken"); +const TOKEN_PREFIX = "Bearer "; +const SERVICE_NAME = "allowedRoles"; +const { errorMessages } = require("../utils/messages"); + +const isAllowed = (allowedRoles) => { + return (req, res, next) => { + const token = req.headers["authorization"]; + + // If no token is pressent, return an error + if (!token) { + const error = new Error(errorMessages.NO_AUTH_TOKEN); + error.status = 401; + error.service = SERVICE_NAME; + next(error); + return; + } + + // If the token is improperly formatted, return an error + if (!token.startsWith(TOKEN_PREFIX)) { + const error = new Error(errorMessages.INVALID_AUTH_TOKEN); + error.status = 400; + error.service = SERVICE_NAME; + next(error); + return; + } + // Parse the token + try { + const parsedToken = token.slice(TOKEN_PREFIX.length, token.length); + var decoded = jwt.verify(parsedToken, process.env.JWT_SECRET); + const userRoles = decoded.role; + + // Check if the user has the required role + if (userRoles.some((role) => allowedRoles.includes(role))) { + next(); + return; + } else { + const error = new Error(errorMessages.INSUFFICIENT_PERMISSIONS); + error.status = 401; + error.service = SERVICE_NAME; + next(error); + return; + } + } catch (error) { + error.status = 401; + error.service = SERVICE_NAME; + next(error); + return; + } + }; +}; + +module.exports = { isAllowed }; diff --git a/Server/middleware/verifyAdmin.js b/Server/middleware/verifySuperAdmin.js similarity index 91% rename from Server/middleware/verifyAdmin.js rename to Server/middleware/verifySuperAdmin.js index 7ef123451..82401b3b7 100644 --- a/Server/middleware/verifyAdmin.js +++ b/Server/middleware/verifySuperAdmin.js @@ -11,7 +11,7 @@ const { errorMessages } = require("../utils/messages"); * @param {express.NextFunction} next * @returns {express.Response} */ -const verifyAdmin = (req, res, next) => { +const verifySuperAdmin = (req, res, next) => { const token = req.headers["authorization"]; // Make sure a token is provided if (!token) { @@ -42,7 +42,7 @@ const verifyAdmin = (req, res, next) => { .json({ success: false, msg: errorMessages.INVALID_AUTH_TOKEN }); } - if (decoded.role.includes("admin") === false) { + if (decoded.role.includes("superadmin") === false) { logger.error(errorMessages.INVALID_AUTH_TOKEN, { service: SERVICE_NAME, }); @@ -54,4 +54,4 @@ const verifyAdmin = (req, res, next) => { }); }; -module.exports = { verifyAdmin }; +module.exports = { verifySuperAdmin }; diff --git a/Server/models/InviteToken.js b/Server/models/InviteToken.js index 10515dadb..5fbd4fc73 100644 --- a/Server/models/InviteToken.js +++ b/Server/models/InviteToken.js @@ -7,6 +7,12 @@ const InviteTokenSchema = mongoose.Schema( required: true, unique: true, }, + teamId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Team", + immutable: true, + required: true, + }, role: { type: Array, required: true, diff --git a/Server/models/Monitor.js b/Server/models/Monitor.js index 7d4bd17d2..fdc147d1b 100644 --- a/Server/models/Monitor.js +++ b/Server/models/Monitor.js @@ -1,4 +1,5 @@ const mongoose = require("mongoose"); +const Notification = require("./Notification"); const MonitorSchema = mongoose.Schema( { @@ -6,6 +7,13 @@ const MonitorSchema = mongoose.Schema( type: mongoose.Schema.Types.ObjectId, ref: "User", immutable: true, + required: true, + }, + teamId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Team", + immutable: true, + required: true, }, name: { type: String, @@ -36,6 +44,16 @@ const MonitorSchema = mongoose.Schema( type: Number, default: 60000, }, + uptimePercentage: { + type: Number, + default: undefined, + }, + notifications: [ + { + type: mongoose.Schema.Types.ObjectId, + ref: "Notification", + }, + ], }, { timestamps: true, diff --git a/Server/models/Team.js b/Server/models/Team.js new file mode 100644 index 000000000..25e1f9b22 --- /dev/null +++ b/Server/models/Team.js @@ -0,0 +1,16 @@ +const mongoose = require("mongoose"); + +const TeamSchema = mongoose.Schema( + { + email: { + type: String, + required: true, + unique: true, + }, + }, + { + timestamps: true, + } +); + +module.exports = mongoose.model("Team", TeamSchema); diff --git a/Server/models/user.js b/Server/models/user.js index 977cabc15..829129b4d 100644 --- a/Server/models/user.js +++ b/Server/models/user.js @@ -38,7 +38,12 @@ const UserSchema = mongoose.Schema( role: { type: [String], default: "user", - enum: ["user", "admin"], + enum: ["user", "admin", "superadmin"], + }, + teamId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Team", + immutable: true, }, }, { @@ -70,4 +75,10 @@ UserSchema.methods.comparePassword = async function (submittedPassword) { return res; }; +const User = mongoose.model("User", UserSchema); + +User.init().then(() => { + console.log("User model initialized"); +}); + module.exports = mongoose.model("User", UserSchema); diff --git a/Server/routes/authRoute.js b/Server/routes/authRoute.js index 201807121..743a55c19 100644 --- a/Server/routes/authRoute.js +++ b/Server/routes/authRoute.js @@ -1,7 +1,7 @@ const router = require("express").Router(); const { verifyJWT } = require("../middleware/verifyJWT"); -const { verifyAdmin } = require("../middleware/verifyAdmin"); const { verifyOwnership } = require("../middleware/verifyOwnership"); +const { isAllowed } = require("../middleware/isAllowed"); const multer = require("multer"); const upload = multer(); const User = require("../models/user"); @@ -14,11 +14,9 @@ const { recoveryRequestController, validateRecoveryTokenController, resetPasswordController, - checkAdminController, + checkSuperadminController, getAllUsersController, deleteUserController, - inviteController, - inviteVerifyController, } = require("../controllers/authController"); //Auth routes @@ -30,8 +28,13 @@ router.put( verifyJWT, userEditController ); -router.get("/users/admin", checkAdminController); -router.get("/users", verifyJWT, verifyAdmin, getAllUsersController); +router.get("/users/superadmin", checkSuperadminController); +router.get( + "/users", + verifyJWT, + isAllowed(["admin", "superadmin"]), + getAllUsersController +); router.delete( "/user/:userId", verifyJWT, @@ -39,9 +42,6 @@ router.delete( deleteUserController ); -router.post("/invite", verifyJWT, inviteController); -router.post("/invite/verify", inviteVerifyController); - //Recovery routes router.post("/recovery/request", recoveryRequestController); router.post("/recovery/validate", validateRecoveryTokenController); diff --git a/Server/routes/checkRoute.js b/Server/routes/checkRoute.js index 4bba24e79..649a68ccf 100644 --- a/Server/routes/checkRoute.js +++ b/Server/routes/checkRoute.js @@ -1,6 +1,7 @@ const router = require("express").Router(); const checkController = require("../controllers/checkController"); const { verifyOwnership } = require("../middleware/verifyOwnership"); +const { isAllowed } = require("../middleware/isAllowed"); const Monitor = require("../models/Monitor"); router.post( @@ -9,13 +10,9 @@ router.post( checkController.createCheck ); -router.get( - "/:monitorId", - verifyOwnership(Monitor, "monitorId"), - checkController.getChecks -); +router.get("/:monitorId", checkController.getChecks); -router.get("/user/:userId", checkController.getUserChecks); +router.get("/team/:teamId", checkController.getTeamChecks); router.delete( "/:monitorId", @@ -23,4 +20,10 @@ router.delete( checkController.deleteChecks ); +router.delete( + "/team/:teamId", + isAllowed(["admin", "superadmin"]), + checkController.deleteChecksByTeamId +); + module.exports = router; diff --git a/Server/routes/inviteRoute.js b/Server/routes/inviteRoute.js new file mode 100644 index 000000000..54e74b0fa --- /dev/null +++ b/Server/routes/inviteRoute.js @@ -0,0 +1,22 @@ +const router = require("express").Router(); +const { verifyJWT } = require("../middleware/verifyJWT"); +const { isAllowed } = require("../middleware/isAllowed"); + +const { + inviteController, + inviteVerifyController, +} = require("../controllers/inviteController"); + +router.post( + "/", + isAllowed(["admin", "superadmin"]), + verifyJWT, + inviteController +); +router.post( + "/verify", + isAllowed(["admin", "superadmin"]), + inviteVerifyController +); + +module.exports = router; diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js index 847b2b25d..ee1c08488 100644 --- a/Server/routes/monitorRoute.js +++ b/Server/routes/monitorRoute.js @@ -1,25 +1,41 @@ const router = require("express").Router(); const monitorController = require("../controllers/monitorController"); -const { verifyOwnership } = require("../middleware/verifyOwnership"); -const Monitor = require("../models/Monitor"); +const { isAllowed } = require("../middleware/isAllowed"); router.get("/", monitorController.getAllMonitors); router.get("/stats/:monitorId", monitorController.getMonitorStatsById); router.get("/certificate/:monitorId", monitorController.getMonitorCertificate); router.get("/:monitorId", monitorController.getMonitorById); -router.get("/user/:userId", monitorController.getMonitorsByUserId); +router.get("/team/:teamId", monitorController.getMonitorsByTeamId); + +router.post( + "/", + isAllowed(["admin", "superadmin"]), + monitorController.createMonitor +); -router.post("/", monitorController.createMonitor); router.delete( "/:monitorId", - verifyOwnership(Monitor, "monitorId"), + isAllowed(["admin", "superadmin"]), monitorController.deleteMonitor ); + router.put( "/:monitorId", - verifyOwnership(Monitor, "monitorId"), + isAllowed(["admin", "superadmin"]), monitorController.editMonitor ); -router.delete("/all", monitorController.deleteAllMonitors); +router.delete( + "/all", + isAllowed(["superadmin"]), + monitorController.deleteAllMonitors +); + +router.post( + "/pause/:monitorId", + isAllowed(["admin", "superadmin"]), + monitorController.pauseMonitor +); + module.exports = router; diff --git a/Server/service/networkService.js b/Server/service/networkService.js index b688b6fff..f5c333a60 100644 --- a/Server/service/networkService.js +++ b/Server/service/networkService.js @@ -1,6 +1,8 @@ const axios = require("axios"); const ping = require("ping"); const logger = require("../utils/logger"); +const http = require("http"); +const { errorMessages, successMessages } = require("../utils/messages"); class NetworkService { constructor(db, emailService) { @@ -14,37 +16,45 @@ class NetworkService { } async handleNotification(job, isAlive) { - const { _id } = job.data; - const monitor = await this.db.getMonitorById(_id); - if (monitor === null || monitor === undefined) { - logger.error(`Null Monitor: ${_id}`, { + try { + const { _id } = job.data; + const monitor = await this.db.getMonitorById(_id); + if (monitor === null || monitor === undefined) { + logger.error(`Null Monitor: ${_id}`, { + method: "handleNotification", + service: this.SERVICE_NAME, + jobId: job.id, + }); + return; + } + + // If monitor status changes, update monitor status and send notification + if (monitor.status !== isAlive) { + monitor.status = !monitor.status; + await monitor.save(); + + let template = + isAlive === true ? "serverIsUpTemplate" : "serverIsDownTemplate"; + let status = isAlive === true ? "up" : "down"; + + const notifications = await this.db.getNotificationsByMonitorId(_id); + for (const notification of notifications) { + if (notification.type === "email") { + await this.emailService.buildAndSendEmail( + template, + { monitorName: monitor.name, monitorUrl: monitor.url }, + notification.address, + `Monitor ${monitor.name} is ${status}` + ); + } + } + } + } catch (error) { + logger.error(error.message, { method: "handleNotification", service: this.SERVICE_NAME, jobId: job.id, }); - return; - } - - // If monitor status changes, update monitor status and send notification - if (monitor.status !== isAlive) { - monitor.status = !monitor.status; - await monitor.save(); - - let template = - isAlive === true ? "serverIsUpTemplate" : "serverIsDownTemplate"; - let status = isAlive === true ? "up" : "down"; - - const notifications = await this.db.getNotificationsByMonitorId(_id); - for (const notification of notifications) { - if (notification.type === "email") { - await this.emailService.buildAndSendEmail( - template, - { monitorName: monitor.name, monitorUrl: monitor.url }, - notification.address, - `Monitor ${monitor.name} is ${status}` - ); - } - } } } @@ -84,11 +94,13 @@ class NetworkService { const { responseTime, response } = await this.measureResponseTime(operation); isAlive = response.alive; - const checkData = { monitorId: job.data._id, status: isAlive, responseTime, + message: isAlive + ? successMessages.PING_SUCCESS + : errorMessages.PING_CANNOT_RESOLVE, }; return await this.logAndStoreCheck(checkData, this.db.createCheck); } catch (error) { @@ -96,6 +108,7 @@ class NetworkService { const checkData = { monitorId: job.data._id, status: isAlive, + message: errorMessages.PING_CANNOT_RESOLVE, responseTime: error.responseTime, }; return await this.logAndStoreCheck(checkData, this.db.createCheck); @@ -122,7 +135,6 @@ class NetworkService { try { const { responseTime, response } = await this.measureResponseTime(operation); - // check if response is in the 200 range, if so, service is up isAlive = response.status >= 200 && response.status < 300; @@ -132,21 +144,21 @@ class NetworkService { status: isAlive, responseTime, statusCode: response.status, + message: http.STATUS_CODES[response.status], }; return await this.logAndStoreCheck(checkData, this.db.createCheck); } catch (error) { + const statusCode = error.response?.status || this.NETWORK_ERROR; + let message = http.STATUS_CODES[statusCode] || "Network Error"; isAlive = false; const checkData = { monitorId: job.data._id, status: isAlive, + statusCode, responseTime: error.responseTime, + message, }; - // The server returned a response - if (error.response) { - checkData.statusCode = error.response.status; - } else { - checkData.statusCode = this.NETWORK_ERROR; - } + return await this.logAndStoreCheck(checkData, this.db.createCheck); } finally { this.handleNotification(job, isAlive); @@ -197,6 +209,8 @@ class NetworkService { const checkData = { monitorId: job.data._id, status: isAlive, + statusCode: response.status, + message: http.STATUS_CODES[response.status], accessibility: (categories.accessibility?.score || 0) * 100, bestPractices: (categories["best-practices"]?.score || 0) * 100, seo: (categories.seo?.score || 0) * 100, @@ -213,9 +227,13 @@ class NetworkService { this.logAndStoreCheck(checkData, this.db.createPageSpeedCheck); } catch (error) { isAlive = false; + const statusCode = error.response?.status || this.NETWORK_ERROR; + const message = http.STATUS_CODES[statusCode] || "Network Error"; const checkData = { monitorId: job.data._id, status: isAlive, + statusCode, + message, accessibility: 0, bestPractices: 0, seo: 0, diff --git a/Server/utils/messages.js b/Server/utils/messages.js index 55f0fa7e9..53a8f689f 100644 --- a/Server/utils/messages.js +++ b/Server/utils/messages.js @@ -17,6 +17,9 @@ const errorMessages = { VERIFY_OWNER_NOT_FOUND: "Document not found", VERIFY_OWNER_UNAUTHORIZED: "Unauthorized access", + //Permissions Middleware + INSUFFICIENT_PERMISSIONS: "Insufficient permissions", + //DB Errors DB_USER_EXISTS: "User already exists", DB_USER_NOT_FOUND: "User not found", @@ -41,6 +44,9 @@ const errorMessages = { JOB_QUEUE_WORKER_CLOSE: "Error closing worker", JOB_QUEUE_DELETE_JOB: "Job not found in queue", JOB_QUEUE_OBLITERATE: "Error obliterating queue", + + // PING Operations + PING_CANNOT_RESOLVE: "No response", }; const successMessages = { @@ -81,10 +87,15 @@ const successMessages = { //Job Queue JOB_QUEUE_DELETE_JOB: "Job removed successfully", JOB_QUEUE_OBLITERATE: "Queue OBLITERATED!!!", + JOB_QUEUE_PAUSE_JOB: "Job paused successfully", + JOB_QUEUE_RESUME_JOB: "Job resumed successfully", //Maintenance Window Controller MAINTENANCE_WINDOW_CREATE: "Maintenance Window created successfully", MAINTEANCE_WINDOW_GET_BY_USER: "Got Maintenance Windows by User successfully", + + //Ping Operations + PING_SUCCESS: "Success", }; module.exports = { diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 4f8042e12..ccc8da9f6 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -5,8 +5,11 @@ const joi = require("joi"); //**************************************** const roleValidatior = (role) => (value, helpers) => { - if (!value.includes(role)) { - throw new joi.ValidationError(`You do not have ${role} authorization`); + const hasRole = role.some((role) => value.includes(role)); + if (!hasRole) { + throw new Joi.ValidationError( + `You do not have the required authorization. Required roles: ${roles.join(", ")}` + ); } return value; }; @@ -36,7 +39,7 @@ const loginValidation = joi.object({ ), }); -const registerValidation = joi.object({ +const registrationBodyValidation = joi.object({ firstName: joi .string() .required() @@ -66,9 +69,10 @@ const registerValidation = joi.object({ profileImage: joi.any(), role: joi .array() - .items(joi.string().valid("admin", "user")) + .items(joi.string().valid("superadmin", "admin", "user")) .min(1) .required(), + teamId: joi.string().allow("").required(), }); const editUserParamValidation = joi.object({ @@ -123,7 +127,7 @@ const deleteUserParamValidation = joi.object({ }); const inviteRoleValidation = joi.object({ - roles: joi.custom(roleValidatior("admin")).required(), + roles: joi.custom(roleValidatior(["admin", "superadmin"])).required(), }); const inviteBodyValidation = joi.object({ @@ -132,6 +136,7 @@ const inviteBodyValidation = joi.object({ "string.email": "Must be a valid email address", }), role: joi.array().required(), + teamId: joi.string().required(), }); const inviteVerifciationBodyValidation = joi.object({ @@ -155,11 +160,11 @@ const getMonitorByIdQueryValidation = joi.object({ normalize: joi.boolean(), }); -const getMonitorsByUserIdValidation = joi.object({ - userId: joi.string().required(), +const getMonitorsByTeamIdValidation = joi.object({ + teamId: joi.string().required(), }); -const getMonitorsByUserIdQueryValidation = joi.object({ +const getMonitorsByTeamIdQueryValidation = joi.object({ status: joi.boolean(), sortOrder: joi.string().valid("asc", "desc"), limit: joi.number(), @@ -175,6 +180,7 @@ const getMonitorsByUserIdQueryValidation = joi.object({ const createMonitorBodyValidation = joi.object({ _id: joi.string(), userId: joi.string().required(), + teamId: joi.string().required(), name: joi.string().required(), description: joi.string().required(), type: joi.string().required(), @@ -191,6 +197,10 @@ const editMonitorBodyValidation = joi.object({ notifications: joi.array().items(joi.object()), }); +const pauseMonitorParamValidation = joi.object({ + monitorId: joi.string().required(), +}); + //**************************************** // Alerts //**************************************** @@ -265,11 +275,11 @@ const getChecksQueryValidation = joi.object({ rowsPerPage: joi.number(), }); -const getUserChecksParamValidation = joi.object({ - userId: joi.string().required(), +const getTeamChecksParamValidation = joi.object({ + teamId: joi.string().required(), }); -const getUserChecksQueryValidation = joi.object({ +const getTeamChecksQueryValidation = joi.object({ sortOrder: joi.string().valid("asc", "desc"), limit: joi.number(), dateRange: joi.string().valid("day", "week", "month"), @@ -282,6 +292,10 @@ const deleteChecksParamValidation = joi.object({ monitorId: joi.string().required(), }); +const deleteChecksByTeamIdParamValidation = joi.object({ + teamId: joi.string().required(), +}); + //**************************************** // PageSpeedCheckValidation //**************************************** @@ -331,7 +345,7 @@ const getMaintenanceWindowsByMonitorIdParamValidation = joi.object({ module.exports = { roleValidatior, loginValidation, - registerValidation, + registrationBodyValidation, recoveryValidation, recoveryTokenValidation, newPasswordValidation, @@ -341,9 +355,10 @@ module.exports = { createMonitorBodyValidation, getMonitorByIdParamValidation, getMonitorByIdQueryValidation, - getMonitorsByUserIdValidation, - getMonitorsByUserIdQueryValidation, + getMonitorsByTeamIdValidation, + getMonitorsByTeamIdQueryValidation, editMonitorBodyValidation, + pauseMonitorParamValidation, editUserParamValidation, editUserBodyValidation, createAlertParamValidation, @@ -358,9 +373,10 @@ module.exports = { createCheckBodyValidation, getChecksParamValidation, getChecksQueryValidation, - getUserChecksParamValidation, - getUserChecksQueryValidation, + getTeamChecksParamValidation, + getTeamChecksQueryValidation, deleteChecksParamValidation, + deleteChecksByTeamIdParamValidation, deleteUserParamValidation, getPageSpeedCheckParamValidation, createPageSpeedCheckParamValidation,
+ + {new Date(label).toLocaleDateString("en-US", { weekday: "short", // Mon month: "long", // July @@ -18,10 +37,18 @@ const CustomToolTip = ({ active, payload, label }) => { minute: "2-digit", // 15 hour12: true, // AM/PM format })} -
Response Time (ms): {payload[0].payload.originalResponseTime}