Merge pull request #780 from bluewave-labs/develop

Develop -> Master
This commit is contained in:
Alexander Holliday authored and GitHub committed 2024-09-02 08:32:26 -07:00
commit 1c7a6a7eef
138 files changed
+4847 -3746

No files matched your search

-28
View File
@@ -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;
}
+22 -9
View File
@@ -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 (
<>
<ThemeProvider theme={mode === "light" ? lightTheme : darkTheme}>
<Routes>
<Route exact path="/" element={<HomeLayout />}>
<Route
exact
path="/"
element={<ProtectedRoute Component={Monitors} />}
element={<ProtectedRoute Component={MonitorsWithAdminProp} />}
/>
<Route
path="/monitors"
element={<ProtectedRoute Component={Monitors} />}
element={<ProtectedRoute Component={MonitorsWithAdminProp} />}
/>
<Route
path="/monitors/create"
@@ -49,7 +62,7 @@ function App() {
/>
<Route
path="/monitors/:monitorId/"
element={<ProtectedRoute Component={Details} />}
element={<ProtectedRoute Component={DetailsWithAdminProp} />}
/>
<Route
path="/monitors/configure/:monitorId/"
@@ -70,7 +83,7 @@ function App() {
/>
<Route
path="maintenance"
element={<ProtectedRoute Component={Maintenance} />}
element={<ProtectedRoute Component={MaintenanceWithAdminProp} />}
/>
<Route
path="/maintenance/create"
@@ -78,7 +91,7 @@ function App() {
/>
<Route
path="settings"
element={<ProtectedRoute Component={Settings} />}
element={<ProtectedRoute Component={SettingsWithAdminProp} />}
/>
<Route
path="account/profile"
@@ -94,7 +107,7 @@ function App() {
/>
<Route
path="pagespeed"
element={<ProtectedRoute Component={PageSpeed} />}
element={<ProtectedRoute Component={PageSpeedWithAdminProp} />}
/>
<Route
path="pagespeed/create"
@@ -125,7 +138,7 @@ function App() {
/>
</Routes>
<ToastContainer />
</>
</ThemeProvider>
);
}
+17 -25
View File
@@ -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 && <Box sx={{ color: color }}>{icon}</Box>}
{hasIcon && <Box sx={{ color: text }}>{icon}</Box>}
<Stack direction="column" gap="2px" sx={{ flex: 1 }}>
{title && (
<Typography sx={{ fontWeight: "700", color: `${color} !important` }}>
<Typography sx={{ fontWeight: "700", color: `${text}` }}>
{title}
</Typography>
)}
{body && (
<Typography sx={{ fontWeight: "400", color: `${color} !important` }}>
<Typography sx={{ fontWeight: "400", color: `${text}` }}>
{body}
</Typography>
)}
{hasIcon && isToast && (
<Button
level="tertiary"
label="Dismiss"
variant="text"
color="info"
onClick={onClick}
sx={{
fontWeight: "600",
width: "fit-content",
mt: theme.gap.small,
mt: theme.spacing(4),
padding: 0,
minWidth: 0,
"&:hover": {
backgroundColor: "transparent",
},
"& .MuiTouchRipple-root": {
pointerEvents: "none",
display: "none",
},
}}
></Button>
>
Dismiss
</Button>
)}
</Stack>
{isToast && (
@@ -95,10 +91,6 @@ const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => {
"&:focus": {
outline: "none",
},
"& .MuiTouchRipple-root": {
pointerEvents: "none",
display: "none",
},
}}
>
<CloseIcon
+1
View File
@@ -57,6 +57,7 @@ const Avatar = ({ src, small, sx }) => {
}
sx={{
fontSize: small ? "16px" : "22px",
color: "white",
fontWeight: 400,
backgroundColor: stringToColor(`${user?.firstName} ${user?.lastName}`),
display: "inline-flex",
+2 -63
View File
@@ -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;
+47 -26
View File
@@ -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 (
<Box sx={{ flexShrink: 0, ml: "24px" }}>
<Button
level="secondary"
label=""
img={<LeftArrowDouble />}
variant="group"
onClick={handleFirstPageButtonClick}
disabled={page === 0}
aria-label="first page"
/>
>
<LeftArrowDouble />
</Button>
<Button
level="secondary"
label=""
img={<LeftArrow />}
variant="group"
onClick={handleBackButtonClick}
disabled={page === 0}
aria-label="previous page"
/>
>
<LeftArrow />
</Button>
<Button
level="secondary"
label=""
img={<RightArrow />}
variant="group"
onClick={handleNextButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="next page"
/>
>
<RightArrow />
</Button>
<Button
level="secondary"
label=""
img={<RightArrowDouble />}
variant="group"
onClick={handleLastPageButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="last page"
/>
>
<RightArrowDouble />
</Button>
</Box>
);
};
@@ -215,6 +215,9 @@ const BasicTable = ({ data, paginated, reversed, table }) => {
<TableRow
sx={{
cursor: row.handleClick ? "pointer" : "default",
"&:hover": {
backgroundColor: theme.palette.background.accent,
},
}}
key={row.id}
onClick={row.handleClick ? row.handleClick : null}
@@ -233,9 +236,14 @@ 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,
},
}}
>
<Typography sx={{ opacity: 0.7 }}>
<Typography px={theme.spacing(2)} fontSize={12} sx={{ opacity: 0.7 }}>
Showing {getRange()} of {data.rows.length} monitor(s)
</Typography>
<TablePagination
@@ -260,27 +268,40 @@ const BasicTable = ({ data, paginated, reversed, table }) => {
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,
},
}}
/>
</Stack>
+1 -10
View File
@@ -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;
}
+21 -8
View File
@@ -25,23 +25,36 @@ const Breadcrumbs = ({ list }) => {
<MUIBreadcrumbs
separator={<ArrowRight />}
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 (
<Box
component="a"
key={`${item.name}-${index}`}
px={theme.gap.small}
pt={theme.gap.xs}
pb="6px"
sx={{ textTransform: "capitalize" }}
borderRadius={`${theme.shape.borderRadius}px`}
px={theme.spacing(4)}
pt={theme.spacing(2)}
pb={theme.spacing(3)}
borderRadius={theme.shape.borderRadius}
onClick={() => navigate(item.path)}
sx={{
opacity: 0.8,
textTransform: "capitalize",
"&, &:hover": {
color: theme.palette.text.tertiary,
},
}}
>
{item.name}
</Box>
-33
View File
@@ -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);
}
-107
View File
@@ -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
* <Button type="submit" level="error" label="Error" disabled sx={{marginTop: "1rem"}}/>
*/
const Button = ({
id,
animate,
type,
level,
label,
disabled,
img,
onClick,
props,
sx,
}) => {
const { variant, color } = levelConfig[level];
return (
<MuiButton
id={id}
className={`button-${animate}`}
type={type}
variant={variant}
color={color}
disabled={disabled}
onClick={onClick}
disableRipple
sx={{
lineHeight: 1.5,
fontWeight: 400,
boxShadow: "none",
textTransform: "none",
"&:focus": {
outline: "none",
},
"&:hover": {
boxShadow: "none",
},
...sx,
}}
{...props}
>
{img && img}
<span>{label}</span>
</MuiButton>
);
};
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;
@@ -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
* <ButtonSpinner
* level="primary"
* label="Save"
* img={<SaveIcon />}
* 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 (
<LoadingButton
variant={variant}
color={color}
disabled={disabled}
loadingIndicator={loadingText}
loadingPosition={position}
startIcon={position === "start" && img}
endIcon={position === "end" && img}
onClick={onClick}
loading={isLoading}
disableRipple
sx={{
boxShadow: "none",
textTransform: "none",
"&:focus": {
outline: "none",
},
"&:hover": {
boxShadow: "none",
transition: "none"
},
...sx,
}}
>
<span>{label}</span>
</LoadingButton>
);
};
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;
@@ -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;
}
+37 -16
View File
@@ -26,7 +26,7 @@ const BarChart = ({ checks = [] }) => {
<Stack
direction="row"
flexWrap="nowrap"
gap="3px"
gap={theme.spacing(1.5)}
height="50px"
width="fit-content"
onClick={(event) => event.stopPropagation()}
@@ -39,11 +39,11 @@ const BarChart = ({ checks = [] }) => {
<Box
key={`${check}-${index}`}
position="relative"
width="9px"
width={theme.spacing(4.5)}
height="100%"
backgroundColor={theme.palette.otherColors.fillGray}
backgroundColor={theme.palette.background.fill}
sx={{
borderRadius: "3px",
borderRadius: theme.spacing(1.5),
}}
/>
) : (
@@ -53,15 +53,15 @@ const BarChart = ({ checks = [] }) => {
<Typography>
{formatDate(new Date(check.createdAt), { year: undefined })}
</Typography>
<Box mt={theme.gap.xs}>
<Box mt={theme.spacing(2)}>
<Box
display="inline-block"
width={theme.gap.small}
height={theme.gap.small}
width={theme.spacing(4)}
height={theme.spacing(4)}
backgroundColor={
check.status
? theme.label.up.dotColor
: theme.label.down.dotColor
? theme.palette.success.main
: theme.palette.error.text
}
sx={{ borderRadius: "50%" }}
/>
@@ -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)}
>
<Typography component="span" sx={{ opacity: 0.8 }}>
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)",
}}
/>
@@ -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 (
<BarChart
@@ -1,19 +0,0 @@
.area-tooltip {
background-color: white;
border: solid 1px var(--env-var-color-4);
border-radius: var(--env-var-radius-1);
padding: var(--env-var-spacing-1) var(--env-var-spacing-2);
}
.area-tooltip p {
margin: 0;
padding: 0;
}
.area-tooltip p:first-of-type {
color: var(--env-var-color-3);
font-size: var(--env-var-font-size-medium);
}
.area-tooltip p:last-of-type {
margin-top: 5px;
color: var(--env-var-color-5);
font-size: var(--env-var-font-size-small);
}
@@ -1,12 +1,31 @@
import PropTypes from "prop-types";
import { AreaChart, Area, XAxis, Tooltip, ResponsiveContainer } from "recharts";
import { Box, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import "./index.css";
const CustomToolTip = ({ active, payload, label }) => {
const theme = useTheme();
if (active && payload && payload.length) {
return (
<div className="area-tooltip">
<p>
<Box
className="area-tooltip"
sx={{
backgroundColor: theme.palette.background.main,
border: 1,
borderColor: theme.palette.border.dark,
borderRadius: theme.shape.borderRadius,
py: theme.spacing(6),
px: theme.spacing(8),
}}
>
<Typography
sx={{
color: theme.palette.primary.main,
fontSize: 13,
}}
>
{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
})}
</p>
<p>Response Time (ms): {payload[0].payload.originalResponseTime}</p>{" "}
</Typography>
<Typography
mt={theme.spacing(2.5)}
sx={{
color: theme.palette.text.secondary,
fontSize: 13,
}}
>
Response Time (ms): {payload[0].payload.originalResponseTime}
</Typography>{" "}
{/* Display original value */}
</div>
</Box>
);
}
return null;
@@ -1,7 +0,0 @@
.chart-container {
display: flex;
justify-content: space-around;
align-items: flex-end;
height: 50px;
width: 300px;
}
@@ -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 (
<div className="chart-container">
<Stack
flexDirection="row"
justifyContent="space-around"
alignItems="flex-end"
height="50px"
width="300px"
>
<ResponsiveContainer width="100%" height="100%">
<BarChart
width={150}
@@ -18,15 +25,15 @@ const ResponseTimeChart = ({ checks = [] }) => {
key={`cell-${index}`}
fill={
check.status === true
? "var(--env-var-color-23)"
: "var(--env-var-color-24)"
? "var(--success-color)"
: "var(--error-color)"
}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
</Stack>
);
};
@@ -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 (
<div className="statistic-tile" style={{ fontFamily: fontFamily }}>
<div className="statistic-tile-title">{title}</div>
<div className="statistic-tile-value">{value}</div>
</div>
);
};
Statistic.propTypes = {
title: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
};
export default Statistic;
@@ -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);
}
+27 -11
View File
@@ -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 (
<Stack
direction="row"
gap={outlined ? theme.gap.medium : theme.gap.small}
className={`check${
variant === "error"
? " check-error"
: variant === "success"
? " check-success"
: " check-info"
}`}
className="check"
gap={outlined ? theme.spacing(6) : theme.spacing(4)}
alignItems="center"
>
{outlined ? (
<CheckOutlined alt="check" />
) : (
<CheckGrey alt="form checks" />
<Box
lineHeight={0}
sx={{
"& svg > path": { fill: colors[variant] },
}}
>
<CheckGrey alt="form checks" />
</Box>
)}
<Typography component="span">{text}</Typography>
<Typography
component="span"
sx={{
color:
variant === "info" ? theme.palette.text.tertiary : colors[variant],
opacity: 0.8,
}}
>
{text}
</Typography>
</Stack>
);
};
+1 -17
View File
@@ -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);
}
-4
View File
@@ -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;
+38 -16
View File
@@ -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 (
<Stack
className={`fallback__${title.trim().split(" ")[0]}`}
alignItems="center"
gap={theme.gap.xl}
gap={theme.spacing(20)}
>
<Skeleton style={{ zIndex: 1 }} />
{mode === "light" ? (
<Skeleton style={{ zIndex: 1 }} />
) : (
<SkeletonDark style={{ zIndex: 1 }} />
)}
<Box
className="background-pattern-svg"
sx={{ backgroundImage: `url(${background})` }}
/>
<Stack gap={theme.gap.small} maxWidth={"275px"} zIndex={1}>
<Typography component="h1" marginY={theme.gap.medium}>
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.border.light,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
<Stack gap={theme.spacing(4)} maxWidth={"275px"} zIndex={1}>
<Typography
component="h1"
marginY={theme.spacing(4)}
color={theme.palette.text.tertiary}
>
A {title} is used to:
</Typography>
{checks.map((check, index) => (
@@ -46,12 +62,17 @@ const Fallback = ({ title, checks, link = "/" }) => {
/>
))}
</Stack>
<Button
level="primary"
label={`Let's create your ${title}`}
sx={{ alignSelf: "center" }}
onClick={() => navigate(link)}
/>
{/* TODO - display a different fallback if user is not an admin*/}
{isAdmin && (
<Button
variant="contained"
color="primary"
sx={{ alignSelf: "center" }}
onClick={() => navigate(link)}
>
Let's create your {title}
</Button>
)}
</Stack>
);
};
@@ -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;
@@ -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;
}
@@ -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
* <Checkbox
* id="checkbox-id"
@@ -37,6 +38,7 @@ const Checkbox = ({
isDisabled,
}) => {
const sizes = { small: "14px", medium: "16px", large: "18px" };
const theme = useTheme();
return (
<FormControlLabel
@@ -61,10 +63,20 @@ const Checkbox = ({
label={label}
disabled={isDisabled}
sx={{
p: "5px",
m: "-5px",
borderRadius: theme.shape.borderRadius,
p: theme.spacing(2.5),
m: theme.spacing(-2.5),
"& .MuiButtonBase-root": {
width: "20px",
width: theme.spacing(10),
p: 0,
mr: theme.spacing(6),
},
"&:not(:has(.Mui-disabled)):hover": {
backgroundColor: theme.palette.background.accent,
},
"& span.MuiTypography-root": {
fontSize: 13,
color: theme.palette.text.tertiary,
},
}}
/>
+1 -52
View File
@@ -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,
+88 -67
View File
@@ -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 (
<Stack gap={theme.gap.xs} className={`field field-${type}`}>
<Stack
gap={theme.spacing(2)}
className={`field field-${type}`}
sx={{
"& fieldset": {
borderColor: theme.palette.border.dark,
borderRadius: theme.shape.borderRadius,
},
"&:not(:has(.Mui-disabled)):not(:has(.input-error)) .MuiOutlinedInput-root:hover:not(:has(input:focus)):not(:has(textarea:focus)) fieldset":
{
borderColor: theme.palette.border.dark,
},
"&:has(.input-error) .MuiOutlinedInput-root fieldset": {
borderColor: theme.palette.error.text,
},
}}
>
{label && (
<Typography component="h3">
<Typography
component="h3"
color={theme.palette.text.secondary}
fontWeight={500}
>
{label}
{isRequired ? <span className="field-required">*</span> : ""}
{isRequired ? (
<Typography
component="span"
ml={theme.spacing(1)}
color={theme.palette.error.text}
>
*
</Typography>
) : (
""
)}
{isOptional ? (
<span className="field-optional">
<Typography
component="span"
fontSize="inherit"
fontWeight={400}
ml={theme.spacing(2)}
sx={{ opacity: 0.6 }}
>
{optionalLabel || "(optional)"}
</span>
</Typography>
) : (
""
)}
@@ -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),
}}
>
<Typography component="h5" sx={{ lineHeight: 1 }}>
<Typography
component="h5"
color={theme.palette.text.secondary}
sx={{ lineHeight: 1 }}
>
{https ? "https" : "http"}://
</Typography>
</Stack>
),
endAdornment:
type === "password" ? (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={() => 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 ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
) : (
hasCopy && (
<InputAdornment className="copy" position="end">
<Button
level="tertiary"
label={copy ? "Copied" : "Copy"}
img={<ContentCopyIcon />}
onClick={handleCopy}
sx={{
borderLeft: `solid 1px ${theme.palette.section.borderColor}`,
lineHeight: 0,
"& .MuiTouchRipple-root": {
pointerEvents: "none",
display: "none",
},
}}
/>
</InputAdornment>
)
),
endAdornment: type === "password" && (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={() => 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 ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
/>
{error && (
<Typography
component="span"
className="input-error"
mt={theme.gap.xs}
color={theme.palette.error.text}
mt={theme.spacing(2)}
sx={{
opacity: 0.8,
}}
>
{error}
</Typography>
@@ -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,
@@ -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;
}
+29 -9
View File
@@ -31,15 +31,15 @@ const ImageField = ({ id, src, loading, onChange }) => {
<>
<Box
className="image-field-wrapper"
mt="20px"
mt={theme.spacing(8)}
sx={{
position: "relative",
height: "fit-content",
border: "dashed",
borderRadius: `${theme.shape.borderRadius}px`,
borderRadius: theme.shape.borderRadius,
borderColor: isDragging
? theme.palette.primary.main
: theme.palette.otherColors.graishWhite,
: theme.palette.border.light,
borderWidth: "2px",
transition: "0.2s",
"&:hover": {
@@ -85,20 +85,40 @@ const ImageField = ({ id, src, loading, onChange }) => {
<IconButton
sx={{
pointerEvents: "none",
borderRadius: `${theme.shape.borderRadius}px`,
border: `solid ${theme.shape.borderThick}px ${theme.palette.otherColors.graishWhite}`,
borderRadius: theme.shape.borderRadius,
border: `solid ${theme.shape.borderThick}px ${theme.palette.border.light}`,
boxShadow: theme.shape.boxShadow,
}}
>
<CloudUploadIcon />
</IconButton>
<Typography component="h2">
<span>Click to upload</span> or drag and drop
<Typography component="h2" color={theme.palette.text.tertiary}>
<Typography
component="span"
fontSize="inherit"
color={theme.palette.primary.main}
fontWeight={500}
>
Click to upload
</Typography>{" "}
or drag and drop
</Typography>
<Typography
component="p"
color={theme.palette.text.tertiary}
sx={{ opacity: 0.6 }}
>
(maximum size: 3MB)
</Typography>
<Typography component="p">(maximum size: 3MB)</Typography>
</Stack>
</Box>
<Typography component="p">Supported formats: JPG, PNG</Typography>
<Typography
component="p"
color={theme.palette.text.tertiary}
sx={{ opacity: 0.6 }}
>
Supported formats: JPG, PNG
</Typography>
</>
) : (
<Stack direction="row" justifyContent="center">
@@ -0,0 +1 @@
@@ -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:
* <RadioButton
* <Radio
* title="Radio Button Title"
* desc="Radio Button Description"
* size="small"
@@ -15,47 +21,66 @@
* @param {string} props.title - The title of the radio button.
* @param {string} [props.desc] - The description of the radio button.
* @param {string} [props.size="small"] - The size of the radio button.
* @returns {JSX.Element} - The rendered RadioButton component.
* @returns {JSX.Element} - The rendered Radio component.
*/
import PropTypes from "prop-types";
import { FormControlLabel, Radio, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import "./index.css";
function RadioButton(props) {
const Radio = (props) => {
const theme = useTheme();
return (
<FormControlLabel
className="custom-radio-button"
checked={props.checked}
value={props.value}
control={
<Radio
<MUIRadio
id={props.id}
size={props.size}
sx={{ color: theme.palette.secondary.main }}
checkedIcon={<RadioChecked />}
sx={{
color: "transparent",
width: 16,
height: 16,
boxShadow: "inset 0 0 0 1px #656a74",
mt: theme.spacing(0.5),
}}
/>
}
onChange={props.onChange}
label={
<>
<Typography component="p">{props.title}</Typography>
<Typography component="h6" mt="2px">
<Typography
component="h6"
mt={theme.spacing(1)}
color={theme.palette.text.secondary}
>
{props.desc}
</Typography>
</>
}
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;
+2 -41
View File
@@ -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;
}
+29 -18
View File
@@ -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 (
<Stack gap={theme.gap.xs} className="select-wrapper">
{label && <Typography component="h3">{label}</Typography>}
<Stack gap={theme.spacing(2)} className="select-wrapper">
{label && (
<Typography
component="h3"
color={theme.palette.text.secondary}
fontWeight={500}
fontSize={13}
>
{label}
</Typography>
)}
<MuiSelect
className="select-component"
value={value}
onChange={onChange}
displayEmpty
inputProps={{ id: id }}
MenuProps={{
PaperProps: {
className: "select-dropdown",
style: {
marginTop: theme.gap.xs,
},
},
MenuListProps: {
style: { padding: 0 },
},
}}
IconComponent={KeyboardArrowDownIcon}
sx={{ ...sx }}
sx={{
fontSize: 13,
minWidth: "125px",
"& fieldset": {
borderRadius: theme.shape.borderRadius,
borderColor: theme.palette.border.dark,
},
"&:not(.Mui-focused):hover fieldset": {
borderColor: theme.palette.border.dark,
},
"& svg path": {
fill: theme.palette.other.icon,
},
...sx,
}}
>
{placeholder && (
<MenuItem
+23 -5
View File
@@ -30,9 +30,9 @@ const BaseLabel = ({ label, styles, children }) => {
<Box
className="label"
sx={{
borderRadius: `${borderRadius}px`,
borderColor: theme.palette.tertiary.main,
color: theme.palette.tertiary.main,
borderRadius: borderRadius,
borderColor: theme.palette.text.tertiary,
color: theme.palette.text.tertiary,
padding: padding,
...styles,
}}
@@ -90,7 +90,7 @@ const ColoredLabel = ({ label, color }) => {
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 (
<BaseLabel
+5 -5
View File
@@ -16,24 +16,24 @@ const Link = ({ level, label, url }) => {
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,
},
},
},
@@ -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;
}
+44 -19
View File
@@ -32,21 +32,36 @@ const ProgressUpload = ({
error,
}) => {
const theme = useTheme();
return (
<Box
className="progress-bar-container"
mt="20px"
mt={theme.spacing(10)}
p={theme.spacing(8)}
sx={{
minWidth: "200px",
height: "fit-content",
borderRadius: `${theme.shape.borderRadius}px`,
border: `solid 1px ${theme.palette.otherColors.graishWhite}`,
borderRadius: theme.shape.borderRadius,
border: 1,
borderColor: theme.palette.border.light,
backgroundColor: "#fafafa",
"&:has(.input-error)": {
borderColor: theme.palette.error.main,
backgroundColor: theme.palette.error.bg,
py: theme.spacing(4),
px: theme.spacing(8),
"& > .MuiStack-root > svg": {
fill: theme.palette.error.text,
width: "20px",
height: "20px",
},
},
}}
>
<Stack
direction="row"
mb={error ? "0" : "10px"}
gap="10px"
mb={error ? 0 : theme.spacing(5)}
gap={theme.spacing(5)}
alignItems={error ? "center" : "flex-end"}
>
{error ? (
@@ -54,10 +69,11 @@ const ProgressUpload = ({
) : icon ? (
<IconButton
sx={{
backgroundColor: theme.palette.otherColors.white,
backgroundColor: theme.palette.background.main,
pointerEvents: "none",
borderRadius: `${theme.shape.borderRadius}px`,
border: `solid ${theme.shape.borderThick}px ${theme.palette.otherColors.graishWhite}`,
borderRadius: theme.shape.borderRadius,
border: 2,
borderColor: theme.palette.border.light,
boxShadow: theme.shape.boxShadow,
}}
>
@@ -67,15 +83,21 @@ const ProgressUpload = ({
""
)}
{error ? (
<Typography component="p" className="input-error">
<Typography
component="p"
className="input-error"
color={theme.palette.error.text}
>
{error}
</Typography>
) : (
<Box>
<Typography component="h2" mb="5px">
<Box color={theme.palette.text.tertiary}>
<Typography component="h2" mb={theme.spacing(1.5)}>
{error ? error : label}
</Typography>
<Typography component="p">{!error && size}</Typography>
<Typography component="p" sx={{ opacity: 0.6 }}>
{!error && size}
</Typography>
</Box>
)}
<IconButton
@@ -85,9 +107,9 @@ const ProgressUpload = ({
? {
alignSelf: "flex-start",
ml: "auto",
mr: "-5px",
mt: "-5px",
padding: "5px",
mr: theme.spacing(-2.5),
mt: theme.spacing(-2.5),
padding: theme.spacing(2.5),
"&:focus": {
outline: "none",
},
@@ -109,20 +131,23 @@ const ProgressUpload = ({
</Stack>
{!error ? (
<Stack direction="row" alignItems="center">
<Box sx={{ width: "100%", mr: "10px" }}>
<Box sx={{ width: "100%", mr: theme.spacing(5) }}>
<LinearProgress
variant="determinate"
value={progress}
sx={{
width: "100%",
height: "10px",
borderRadius: `${theme.shape.borderRadius}px`,
borderRadius: theme.shape.borderRadius,
maxWidth: "500px",
backgroundColor: theme.palette.otherColors.graishWhite,
backgroundColor: theme.palette.border.light,
}}
/>
</Box>
<Typography component="p" sx={{ minWidth: "max-content" }}>
<Typography
component="p"
sx={{ minWidth: "max-content", opacity: 0.6 }}
>
{progress}
<span>%</span>
</Typography>
@@ -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;
}
+1 -81
View File
@@ -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 {
+205 -96
View File
@@ -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() {
<Stack
component="aside"
className={collapsed ? "collapsed" : "expanded"}
gap={theme.gap.medium}
py={theme.spacing(6)}
gap={theme.spacing(6)}
sx={{
border: 1,
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
backgroundColor: theme.palette.background.main,
"& .selected-path, & .MuiListItemButton-root:hover": {
backgroundColor: theme.palette.background.accent,
},
"& .MuiList-root svg path": {
stroke: theme.palette.text.tertiary,
},
"& p, & span, & .MuiListSubheader-root": {
color: theme.palette.text.secondary,
},
}}
>
<Stack pt={theme.gap.medium} pb={theme.gap.large} pl={theme.gap.ml}>
<Stack direction="row" alignItems="center" gap={theme.gap.small}>
<Stack pt={theme.spacing(6)} pb={theme.spacing(12)} pl={theme.spacing(8)}>
<Stack direction="row" alignItems="center" gap={theme.spacing(4)}>
<Stack
justifyContent="center"
alignItems="center"
minWidth={theme.gap.lgplus}
minHeight={theme.gap.lgplus}
fontSize="18px"
minWidth={theme.spacing(16)}
minHeight={theme.spacing(16)}
pl="1px"
fontSize={18}
color="white"
sx={{
position: "relative",
backgroundColor: theme.palette.primary.main,
borderRadius: "4px",
borderRadius: theme.shape.borderRadius,
userSelect: "none",
}}
>
@@ -146,7 +163,7 @@ function Sidebar() {
</Stack>
<Typography
component="span"
mt={theme.gap.xs}
mt={theme.spacing(2)}
sx={{ opacity: 0.8, fontWeight: 500 }}
>
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
</ListSubheader>
}
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),
}}
>
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon}</ListItemIcon>
@@ -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),
}}
>
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon}</ListItemIcon>
@@ -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),
}}
>
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon}</ListItemIcon>
@@ -344,13 +380,13 @@ function Sidebar() {
<List
component="div"
disablePadding
sx={{ pl: theme.gap.large }}
sx={{ pl: theme.spacing(12) }}
>
{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)",
},
}}
>
<ListItemIcon sx={{ minWidth: 0 }}>
@@ -383,7 +445,7 @@ function Sidebar() {
)
)}
</List>
<Divider sx={{ my: theme.gap.small }} />
<Divider sx={{ my: theme.spacing(4) }} />
{/* other */}
<List
component="nav"
@@ -393,15 +455,15 @@ function Sidebar() {
component="div"
id="nested-other-subheader"
sx={{
pt: theme.gap.small,
px: collapsed ? 0 : theme.gap.small,
backgroundColor: "transparent"
pt: theme.spacing(4),
px: collapsed ? 0 : theme.spacing(4),
backgroundColor: "transparent",
}}
>
Other
</ListSubheader>
}
sx={{ px: theme.gap.medium }}
sx={{ px: theme.spacing(6) }}
>
{other.map((item) => (
<Tooltip
@@ -436,9 +498,9 @@ function Sidebar() {
: navigate(`/${item.path}`)
}
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),
}}
>
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon}</ListItemIcon>
@@ -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() {
<Avatar small={true} />
</IconButton>
</Tooltip>
<Menu
className="sidebar-popup"
anchorEl={anchorEl}
open={Boolean(anchorEl) && popup === "logout"}
onClose={closePopup}
anchorOrigin={{
vertical: "top",
horizontal: "right",
}}
sx={{ ml: theme.gap.ml }}
>
<MenuItem sx={{ cursor: "default", minWidth: "150px" }}>
<Box>
<Typography component="span" fontWeight={500} fontSize="13px">
{authState.user?.firstName} {authState.user?.lastName}
</Typography>
<Typography
sx={{ textTransform: "capitalize", fontSize: "12px" }}
>
{authState.user?.role}
</Typography>
</Box>
</MenuItem>
<Divider />
<MenuItem
onClick={logout}
sx={{
gap: theme.gap.small,
borderRadius: `${theme.shape.borderRadius}px`,
pl: theme.gap.small,
}}
>
<LogoutSvg />
Log out
</MenuItem>
</Menu>
</>
) : (
<>
<Avatar small={true} />
<Box ml={theme.gap.xs}>
<Box ml={theme.spacing(2)}>
<Typography component="span" fontWeight={500}>
{authState.user?.firstName} {authState.user?.lastName}
</Typography>
@@ -531,16 +557,99 @@ function Sidebar() {
{authState.user?.role}
</Typography>
</Box>
<Tooltip title="Log Out">
<Tooltip title="Controls" disableInteractive>
<IconButton
sx={{ ml: "auto", "&:focus": { outline: "none" } }}
onClick={logout}
sx={{
ml: "auto",
mr: "-8px",
"&:focus": { outline: "none" },
"& svg": {
width: "20px",
height: "20px",
},
"& svg path": {
stroke: theme.palette.other.icon,
},
}}
onClick={(event) => openPopup(event, "logout")}
>
<LogoutSvg style={{ width: "20px", height: "20px" }} />
<DotsVertical />
</IconButton>
</Tooltip>
</>
)}
<Menu
className="sidebar-popup"
anchorEl={anchorEl}
open={Boolean(anchorEl) && popup === "logout"}
onClose={closePopup}
anchorOrigin={{
vertical: "top",
horizontal: "right",
}}
slotProps={{
paper: {
sx: {
marginTop: theme.spacing(-4),
marginLeft: collapsed ? theme.spacing(2) : 0,
},
},
}}
MenuListProps={{
sx: {
p: 2,
"& li": { m: 0 },
"& li:has(.MuiBox-root):hover": {
backgroundColor: "transparent",
},
},
}}
sx={{
ml: theme.spacing(8),
}}
>
{collapsed && (
<MenuItem sx={{ cursor: "default", minWidth: "150px" }}>
<Box mb={theme.spacing(2)}>
<Typography component="span" fontWeight={500} fontSize={13}>
{authState.user?.firstName} {authState.user?.lastName}
</Typography>
<Typography sx={{ textTransform: "capitalize", fontSize: 12 }}>
{authState.user?.role}
</Typography>
</Box>
</MenuItem>
)}
{collapsed && <Divider />}
<MenuItem
onClick={() => {
dispatch(setMode("light"));
closePopup();
}}
>
Light
</MenuItem>
<MenuItem
onClick={() => {
dispatch(setMode("dark"));
closePopup();
}}
>
Dark
</MenuItem>
<Divider />
<MenuItem
onClick={logout}
sx={{
gap: theme.spacing(4),
borderRadius: theme.shape.borderRadius,
pl: theme.spacing(4),
}}
>
<LogoutSvg />
Log out
</MenuItem>
</Menu>
</Stack>
</Stack>
);
@@ -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 (
<TabPanel value="password">
<TabPanel
value="password"
sx={{
"& h1, & input": {
color: theme.palette.text.tertiary,
},
}}
>
<Stack
component="form"
onSubmit={handleSubmit}
noValidate
spellCheck="false"
gap={theme.gap.xl}
gap={theme.spacing(20)}
>
<Stack direction="row">
<Box flex={0.9}>
@@ -157,19 +164,20 @@ const PasswordPanel = () => {
</Box>
</Stack>
<Stack direction="row" justifyContent="flex-end">
<ButtonSpinner
level="primary"
label="Save"
<LoadingButton
variant="contained"
color="primary"
onClick={handleSubmit}
isLoading={isLoading}
loadingText="Saving..."
loading={isLoading}
loadingIndicator="Saving..."
disabled={Object.keys(errors).length !== 0 && true}
sx={{
paddingX: theme.gap.large,
width: "fit-content",
mt: theme.gap.xl,
px: theme.spacing(12),
mt: theme.spacing(20),
}}
/>
>
Save
</LoadingButton>
</Stack>
</Stack>
</TabPanel>
@@ -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 (
<TabPanel value="profile">
<TabPanel
value="profile"
sx={{
"& h1, & p, & input": {
color: theme.palette.text.tertiary,
},
}}
>
<Stack
component="form"
className="edit-profile-form"
noValidate
spellCheck="false"
gap={theme.gap.xl}
gap={theme.spacing(20)}
>
<Stack direction="row">
<Box flex={0.9}>
@@ -285,52 +291,48 @@ const ProfilePanel = () => {
}
sx={{ mr: "8px" }}
/>
<Button
level="tertiary"
label="Delete"
onClick={handleDeletePicture}
/>
<Button
level="tertiary"
label="Update"
onClick={openPictureModal}
sx={{
color: theme.palette.primary.main,
}}
/>
<Button variant="text" color="info" onClick={handleDeletePicture}>
Delete
</Button>
<Button variant="text" color="primary" onClick={openPictureModal}>
Update
</Button>
</Stack>
</Stack>
<Divider
aria-hidden="true"
width="0"
sx={{ marginY: theme.spacing(1) }}
sx={{
marginY: theme.spacing(1),
}}
/>
<Stack direction="row" justifyContent="flex-end">
<Box width="fit-content">
<ButtonSpinner
level="primary"
label="Save"
<LoadingButton
variant="contained"
color="primary"
onClick={handleSaveProfile}
isLoading={isLoading}
loadingText="Saving..."
loading={isLoading}
loadingIndicator="Saving..."
disabled={
Object.keys(errors).length !== 0 && !errors?.picture && true
}
sx={{
paddingX: theme.gap.large,
}}
/>
sx={{ px: theme.spacing(12) }}
>
Save
</LoadingButton>
</Box>
</Stack>
</Stack>
<Divider aria-hidden="true" sx={{ marginY: theme.spacing(6) }} />
<Stack
component="form"
noValidate
spellCheck="false"
gap={theme.gap.small}
>
<Box>
<Divider
aria-hidden="true"
sx={{
marginY: theme.spacing(20),
borderColor: theme.palette.border.light,
}}
/>
<Box component="form" noValidate spellCheck="false">
<Box mb={theme.spacing(6)}>
<Typography component="h1">Delete account</Typography>
<Typography component="p" sx={{ opacity: 0.6 }}>
Note that deleting your account will remove all data from our
@@ -338,12 +340,13 @@ const ProfilePanel = () => {
</Typography>
</Box>
<Button
level="error"
label="Delete account"
variant="contained"
color="error"
onClick={() => setIsOpen("delete")}
sx={{ width: "fit-content", mt: theme.gap.small }}
/>
</Stack>
>
Delete account
</Button>
</Box>
{/* TODO - Update ModalPopup Component with @mui for reusability */}
<Modal
aria-labelledby="modal-delete-account"
@@ -353,18 +356,19 @@ const ProfilePanel = () => {
disablePortal
>
<Stack
gap="10px"
gap={theme.spacing(5)}
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 500,
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",
},
@@ -378,18 +382,23 @@ const ProfilePanel = () => {
and all of your data will be deleted. Deleting your account is
permanent and non-recoverable action.
</Typography>
<Stack direction="row" gap="10px" mt="10px" justifyContent="flex-end">
<Button
level="tertiary"
label="Cancel"
onClick={() => setIsOpen("")}
/>
<ButtonSpinner
level="error"
label="Delete account"
<Stack
direction="row"
gap={theme.spacing(5)}
mt={theme.spacing(5)}
justifyContent="flex-end"
>
<Button variant="text" color="info" onClick={() => setIsOpen("")}>
Cancel
</Button>
<LoadingButton
variant="contained"
color="error"
onClick={handleDeleteAccount}
isLoading={isLoading}
/>
loading={isLoading}
>
Delete account
</LoadingButton>
</Stack>
</Stack>
</Modal>
@@ -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",
},
}}
>
<Typography id="modal-update-picture" component="h1">
<Typography
id="modal-update-picture"
component="h1"
color={theme.palette.text.secondary}
>
Upload Image
</Typography>
<ImageField
@@ -447,17 +461,26 @@ const ProfilePanel = () => {
) : (
""
)}
<Stack direction="row" mt="20px" gap="10px" justifyContent="flex-end">
<Stack
direction="row"
mt={theme.spacing(10)}
gap={theme.spacing(5)}
justifyContent="flex-end"
>
<Button
level="secondary"
label="Edit"
variant="outlined"
color="secondary"
disabled
sx={{ mr: "auto" }}
/>
<Button level="tertiary" label="Remove" onClick={removePicture} />
>
Edit
</Button>
<Button variant="text" color="info" onClick={removePicture}>
Remove
</Button>
<Button
level="primary"
label="Update"
variant="contained"
color="primary"
onClick={handleUpdatePicture}
disabled={
(Object.keys(errors).length !== 0 && errors?.picture) ||
@@ -465,7 +488,9 @@ const ProfilePanel = () => {
? true
: false
}
/>
>
Update
</Button>
</Stack>
</Stack>
</Modal>
@@ -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: (
<Stack>
<Typography
style={{ color: theme.palette.otherColors.blackish }}
>
<Typography style={{ color: theme.palette.text.secondary }}>
{member.firstName + " " + member.lastName}
</Typography>
<Typography sx={{ opacity: 0.6 }}>
<Typography>
Created {new Date(member.createdAt).toLocaleDateString()}
</Typography>
</Stack>
@@ -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 (
<TabPanel value="team">
<TabPanel
value="team"
sx={{
"& h1": {
color: theme.palette.text.tertiary,
},
"& .MuiTable-root .MuiTableBody-root .MuiTableCell-root, & .MuiTable-root p + p":
{
color: theme.palette.text.accent,
},
}}
>
{/* FEATURE STILL TO BE IMPLEMENTED */}
{/* <Stack component="form">
<Box sx={{ alignSelf: "flex-start" }}>
@@ -229,7 +252,7 @@ const TeamPanel = () => {
},
}}
inputProps={{
sx: { textAlign: "end", padding: theme.gap.small },
sx: { textAlign: "end", padding: theme.spacing(4) },
}}
/>
<Button
@@ -239,8 +262,8 @@ const TeamPanel = () => {
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,
}}
/>
</Stack>
@@ -250,52 +273,47 @@ const TeamPanel = () => {
component="form"
noValidate
spellCheck="false"
gap={theme.gap.large}
gap={theme.spacing(12)}
>
<Typography component="h1">Team members</Typography>
<Stack direction="row" justifyContent="space-between">
<Stack
direction="row"
alignItems="flex-end"
gap={theme.gap.medium}
sx={{ fontSize: "14px" }}
gap={theme.spacing(6)}
sx={{ fontSize: 14 }}
>
<ButtonGroup>
<Button
level="secondary"
label="All"
variant="group"
filled={(filter === "all").toString()}
onClick={() => setFilter("all")}
sx={{
backgroundColor:
filter === "all" && theme.palette.otherColors.fillGray,
}}
/>
>
All
</Button>
<Button
level="secondary"
label="Administrator"
variant="group"
filled={(filter === "admin").toString()}
onClick={() => setFilter("admin")}
sx={{
backgroundColor:
filter === "admin" && theme.palette.otherColors.fillGray,
}}
/>
>
Administrator
</Button>
<Button
level="secondary"
label="Member"
variant="group"
filled={(filter === "user").toString()}
onClick={() => setFilter("user")}
sx={{
backgroundColor:
filter === "user" && theme.palette.otherColors.fillGray,
}}
/>
>
Member
</Button>
</ButtonGroup>
</Stack>
<Button
level="primary"
label="Invite a team member"
sx={{ paddingX: "30px" }}
variant="contained"
color="primary"
onClick={() => setIsOpen(true)}
/>
>
Invite a team member
</Button>
</Stack>
<BasicTable
data={tableData}
@@ -312,18 +330,19 @@ const TeamPanel = () => {
disablePortal
>
<Stack
gap="10px"
gap={theme.spacing(5)}
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 400,
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",
},
@@ -335,7 +354,7 @@ const TeamPanel = () => {
<Typography
id="invite-member-to-team"
component="p"
sx={{ mb: theme.gap.medium }}
sx={{ mb: theme.spacing(6) }}
>
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" },
]}
/>
<Stack
direction="row"
gap={theme.gap.small}
mt={theme.gap.ml}
gap={theme.spacing(4)}
mt={theme.spacing(8)}
justifyContent="flex-end"
>
<Button
level="tertiary"
label="Cancel"
onClick={closeInviteModal}
/>
<ButtonSpinner
level="primary"
label="Send invite"
<Button variant="text" color="info" onClick={closeInviteModal}>
Cancel
</Button>
<LoadingButton
variant="contained"
color="primary"
onClick={handleInviteMember}
isLoading={false}
loading={false}
disabled={Object.keys(errors).length !== 0}
/>
>
Send invite
</LoadingButton>
</Stack>
</Stack>
</Modal>
@@ -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 }) => (
<Tooltip
{...props}
placement={placement}
arrow={arrow}
classes={{ popper: className }}
/>
)
)(({ theme }) => ({
[`& .${tooltipClasses.tooltip}`]: {
backgroundColor: "black",
},
[`& .${tooltipClasses.arrow}`]: {
color: "black",
},
}));
function TooltipWithTail({ placement, arrow = false, title, text }) {
return (
<div>
<CustomizedTooltip
arrow={arrow}
placement={placement}
title={
<React.Fragment>
{title && <div className="tooltip-title">{title}</div>}
<div className="tooltip-description">{text}</div>
</React.Fragment>
}
className="tooltip-holder"
>
<HelpOutlineIcon style={{ fill: "#98A2B3" }} />
</CustomizedTooltip>
</div>
);
}
export default TooltipWithTail;
@@ -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;
}
+2 -2
View File
@@ -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);
}
@@ -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
+16 -3
View File
@@ -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;
@@ -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";
});
},
});
+2 -2
View File
@@ -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 <WrappedComponent {...props} isAdmin={true} />;
return <WrappedComponent {...props} isSuperAdmin={true} />;
};
const wrappedComponentName =
WrappedComponent.displayName || WrappedComponent.name || "Component";
+20
View File
@@ -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 <WrappedComponent {...props} isAdmin={isAdmin} />;
};
const wrappedComponentName =
WrappedComponent.displayName || WrappedComponent.name || "Component";
WithAdminProp.displayName = `WithAdminProp(${wrappedComponentName})`;
return WithAdminProp;
};
export default withAdminProp;
+2 -16
View File
@@ -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%;
+8 -3
View File
@@ -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 (
<Box className="home-layout">
<Sidebar />
<Outlet />
<Box backgroundColor={theme.palette.background.alt}>
<Box className="home-layout">
<Sidebar />
<Outlet />
</Box>
</Box>
);
};
-13
View File
@@ -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);
}
+17 -13
View File
@@ -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 (
<Box
className="account"
px={theme.gap.xl}
py={theme.gap.large}
px={theme.spacing(20)}
py={theme.spacing(12)}
border={1}
borderColor={theme.palette.otherColors.graishWhite}
borderRadius={`${theme.shape.borderRadius}px`}
backgroundColor={theme.palette.otherColors.white}
borderColor={theme.palette.border.light}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.background.main}
>
<TabContext value={tab}>
<Box
sx={{
borderBottom: 1,
borderColor: "var(--env-var-color-16)",
borderColor: theme.palette.border.light,
"& .MuiTabs-root": { height: "fit-content", minHeight: "0" },
}}
>
@@ -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" }) => {
</Box>
<ProfilePanel />
<PasswordPanel />
{user.role.includes("admin") && <TeamPanel />}
{!hideTeams && <TeamPanel />}
</TabContext>
</Box>
);
+36 -20
View File
@@ -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 (
<Stack className="check-email-page auth" overflow="hidden">
<Stack
className="check-email-page auth"
overflow="hidden"
sx={{
"& h1": {
color: theme.palette.primary.main,
fontWeight: 600,
fontSize: 22,
},
"& p": {
fontSize: 14,
color: theme.palette.text.accent,
},
}}
>
<Box
className="background-pattern-svg"
sx={{ backgroundImage: `url(${background})` }}
@@ -93,8 +106,8 @@ const CheckEmail = () => {
<Stack
direction="row"
alignItems="center"
px={theme.gap.large}
gap={theme.gap.small}
px={theme.spacing(12)}
gap={theme.spacing(4)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>BlueWave Uptime</Typography>
@@ -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),
},
},
}}
>
<Stack
gap={{ xs: theme.gap.ml, sm: theme.gap.large }}
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
alignItems="center"
textAlign="center"
>
<Box>
<EmailIcon alt="email icon" />
<Typography component="h1">Check your email</Typography>
<Typography mt={theme.gap.xs}>
<Typography mt={theme.spacing(2)}>
We sent a password reset link to{" "}
<Typography className="email-sent-to" component="span">
{email || "username@email.com"}
@@ -136,15 +149,17 @@ const CheckEmail = () => {
</Typography>
</Box>
<Button
level="primary"
label="Open email app"
variant="contained"
color="primary"
onClick={openMail}
sx={{
width: "100%",
maxWidth: 400,
}}
/>
<Typography sx={{ alignSelf: "center", mb: theme.gap.medium }}>
>
Open email app
</Button>
<Typography sx={{ alignSelf: "center", mb: theme.spacing(6) }}>
Didn&apos;t receive the email?{" "}
<Typography
component="span"
@@ -162,11 +177,12 @@ const CheckEmail = () => {
</Typography>
</Stack>
</Stack>
<Box textAlign="center" p={theme.gap.large}>
<Box textAlign="center" p={theme.spacing(12)}>
<Typography display="inline-block">Go back to —</Typography>
<Typography
component="span"
ml={theme.gap.xs}
ml={theme.spacing(2)}
color={theme.palette.primary.main}
onClick={handleNavigate}
sx={{ userSelect: "none" }}
>
+60 -39
View File
@@ -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 (
<Stack className="forgot-password-page auth" overflow="hidden">
<Stack
className="forgot-password-page auth"
overflow="hidden"
sx={{
"& h1": {
color: theme.palette.primary.main,
fontWeight: 600,
fontSize: 24,
},
"& p": {
fontSize: 14,
color: theme.palette.text.accent,
},
}}
>
<Box
className="background-pattern-svg"
sx={{ backgroundImage: `url(${background})` }}
@@ -95,8 +109,8 @@ const ForgotPassword = () => {
<Stack
direction="row"
alignItems="center"
px={theme.gap.large}
gap={theme.gap.small}
px={theme.spacing(12)}
gap={theme.spacing(4)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>BlueWave Uptime</Typography>
@@ -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),
},
},
}}
>
<Stack
gap={{ xs: theme.gap.ml, sm: theme.gap.large }}
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
alignItems="center"
textAlign="center"
>
@@ -134,39 +148,46 @@ const ForgotPassword = () => {
No worries, we&apos;ll send you reset instructions.
</Typography>
</Box>
<Box width="100%" textAlign="left">
<form noValidate spellCheck={false} onSubmit={handleSubmit}>
<Field
type="email"
id="forgot-password-email-input"
label="Email"
isRequired={true}
placeholder="Enter your email"
value={form.email}
onChange={handleChange}
error={errors.email}
/>
<ButtonSpinner
disabled={errors.email !== undefined}
onClick={handleSubmit}
isLoading={isLoading}
level="primary"
label="Send instructions"
sx={{
width: "100%",
fontWeight: 400,
mt: theme.gap.mlplus,
}}
/>
</form>
<Box
component="form"
width="95%"
textAlign="left"
noValidate
spellCheck={false}
onSubmit={handleSubmit}
>
<Field
type="email"
id="forgot-password-email-input"
label="Email"
isRequired={true}
placeholder="Enter your email"
value={form.email}
onChange={handleChange}
error={errors.email}
/>
<LoadingButton
variant="contained"
color="primary"
loading={isLoading}
disabled={errors.email !== undefined}
onClick={handleSubmit}
sx={{
width: "100%",
mt: theme.spacing(15),
}}
>
Send instructions
</LoadingButton>
</Box>
</Stack>
</Stack>
<Box textAlign="center" p={theme.gap.large}>
<Box textAlign="center" p={theme.spacing(12)}>
<Typography display="inline-block">Go back to —</Typography>
<Typography
component="span"
ml={theme.gap.xs}
color={theme.palette.primary.main}
ml={theme.spacing(2)}
onClick={handleNavigate}
sx={{ userSelect: "none" }}
>
+103 -50
View File
@@ -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 (
<>
<Stack
gap={{ xs: theme.gap.ml, sm: theme.gap.large }}
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
alignItems="center"
textAlign="center"
>
@@ -40,17 +39,22 @@ const LandingPage = ({ onContinue }) => {
</Box>
<Box width="100%">
<Button
level="secondary"
label="Continue with Email"
img={<Mail />}
variant="outlined"
color="info"
onClick={onContinue}
sx={{
width: "100%",
"& svg": {
mr: theme.gap.small,
mr: theme.spacing(4),
"& path": {
stroke: theme.palette.other.icon,
},
},
}}
/>
>
<Mail />
Continue with Email
</Button>
</Box>
<Box maxWidth={400}>
<Typography className="tos-p">
@@ -64,6 +68,11 @@ const LandingPage = ({ onContinue }) => {
"noreferrer"
);
}}
sx={{
"&:hover": {
color: theme.palette.text.tertiary,
},
}}
>
Terms of Service
</Typography>{" "}
@@ -77,6 +86,11 @@ const LandingPage = ({ onContinue }) => {
"noreferrer"
);
}}
sx={{
"&:hover": {
color: theme.palette.text.tertiary,
},
}}
>
Privacy Policy.
</Typography>
@@ -114,12 +128,15 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
return (
<>
<Stack gap={{ xs: theme.gap.ml, sm: theme.gap.large }} textAlign="center">
<Stack
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
textAlign="center"
>
<Box>
<Typography component="h1">Log In</Typography>
<Typography>Enter your email address</Typography>
</Box>
<Box textAlign="left">
<Box textAlign="left" mb={theme.spacing(5)}>
<form noValidate spellCheck={false} onSubmit={onSubmit}>
<Field
type="email"
@@ -138,27 +155,29 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
</Box>
<Stack direction="row" justifyContent="space-between">
<Button
level="secondary"
label="Back"
animate="slideLeft"
img={<ArrowBackRoundedIcon />}
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 }}
/>
>
<ArrowBackRoundedIcon />
Back
</Button>
<Button
level="primary"
label="Continue"
variant="contained"
color="primary"
onClick={onSubmit}
disabled={errors.email && true}
sx={{ width: "30%" }}
/>
>
Continue
</Button>
</Stack>
</Stack>
</>
@@ -204,12 +223,16 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
return (
<>
<Stack gap={{ xs: theme.gap.ml, sm: theme.gap.large }} textAlign="center">
<Stack
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
position="relative"
textAlign="center"
>
<Box>
<Typography component="h1">Log In</Typography>
<Typography>Enter your password</Typography>
</Box>
<Box textAlign="left">
<Box textAlign="left" mb={theme.spacing(5)}>
<form noValidate spellCheck={false} onSubmit={onSubmit}>
<Field
type="password"
@@ -227,35 +250,50 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
</Box>
<Stack direction="row" justifyContent="space-between">
<Button
level="secondary"
label="Back"
animate="slideLeft"
img={<ArrowBackRoundedIcon />}
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 }}
/>
>
<ArrowBackRoundedIcon />
Back
</Button>
<Button
level="primary"
label="Continue"
variant="contained"
color="primary"
onClick={onSubmit}
disabled={errors.password && true}
sx={{ width: "30%" }}
/>
>
Continue
</Button>
</Stack>
<Box textAlign="center">
<Typography className="forgot-p" display="inline-block">
<Box
textAlign="center"
sx={{
position: "absolute",
top: "103%",
left: "50%",
transform: "translateX(-50%)"
}}
>
<Typography
className="forgot-p"
display="inline-block"
color={theme.palette.primary.main}
>
Forgot password?
</Typography>
<Typography
component="span"
ml={theme.gap.xs}
color={theme.palette.primary.main}
ml={theme.spacing(2)}
sx={{ userSelect: "none" }}
onClick={handleNavigate}
>
@@ -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 (
<Stack className="login-page auth" overflow="hidden">
<Stack
className="login-page auth"
overflow="hidden"
sx={{
"& h1": {
color: theme.palette.primary.main,
fontWeight: 600,
fontSize: 30,
},
"& p": {
fontSize: 14,
color: theme.palette.text.accent,
},
}}
>
<Box
className="background-pattern-svg"
sx={{ backgroundImage: `url(${background})` }}
@@ -401,8 +453,8 @@ const Login = () => {
<Stack
direction="row"
alignItems="center"
px={theme.gap.large}
gap={theme.gap.small}
px={theme.spacing(12)}
gap={theme.spacing(4)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>BlueWave Uptime</Typography>
@@ -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 = () => {
)
)}
</Stack>
<Box textAlign="center" p={theme.gap.large}>
<Box textAlign="center" p={theme.spacing(12)}>
<Typography display="inline-block">
Don&apos;t have an account? —
</Typography>
<Typography
component="span"
ml={theme.gap.xs}
color={theme.palette.primary.main}
ml={theme.spacing(2)}
onClick={() => {
navigate("/register");
}}
+35 -19
View File
@@ -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 (
<Stack className="password-confirmed-page auth" overflow="hidden">
<Stack
className="password-confirmed-page auth"
overflow="hidden"
sx={{
"& h1": {
color: theme.palette.primary.main,
fontWeight: 600,
fontSize: 22,
},
"& p": {
fontSize: 14,
color: theme.palette.text.accent,
},
}}
>
<Box
className="background-pattern-svg"
sx={{ backgroundImage: `url(${background})` }}
@@ -30,8 +43,8 @@ const NewPasswordConfirmed = () => {
<Stack
direction="row"
alignItems="center"
px={theme.gap.large}
gap={theme.gap.small}
px={theme.spacing(12)}
gap={theme.spacing(4)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>BlueWave Uptime</Typography>
@@ -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),
},
},
}}
>
<Stack
gap={{ xs: theme.gap.ml, sm: theme.gap.large }}
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
alignItems="center"
textAlign="center"
>
<Box>
<ConfirmIcon alt="password confirm icon" />
<Typography component="h1">Password reset</Typography>
<Typography mt={theme.gap.xs}>
<Typography mt={theme.spacing(2)}>
Your password has been successfully reset. Click below to log in
magically.
</Typography>
</Box>
<Button
level="primary"
label="Continue"
variant="contained"
color="primary"
onClick={() => navigate("/monitors")}
sx={{
width: "100%",
maxWidth: 400,
}}
/>
>
Continue
</Button>
</Stack>
</Stack>
<Box textAlign="center" p={theme.gap.large}>
<Box textAlign="center" p={theme.spacing(12)}>
<Typography display="inline-block">Go back to —</Typography>
<Typography
component="span"
ml={theme.gap.xs}
color={theme.palette.primary.main}
ml={theme.spacing(2)}
onClick={handleNavigate}
sx={{ userSelect: "none" }}
>
+169 -81
View File
@@ -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 (
<>
<Stack
gap={{ xs: theme.gap.ml, sm: theme.gap.large }}
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
alignItems="center"
textAlign="center"
>
<Box>
<Typography component="h1">Sign Up</Typography>
<Typography>
Create your {isAdmin ? "admin " : ""}account to get started.
Create your {isSuperAdmin ? "Super admin " : ""}account to get
started.
</Typography>
</Box>
<Box width="100%">
<Button
level="secondary"
label="Sign up with Email"
img={<Mail />}
variant="outlined"
color="info"
onClick={onSignup}
sx={{
width: "100%",
"& svg": {
mr: theme.gap.small,
mr: theme.spacing(4),
"& path": {
stroke: theme.palette.other.icon,
},
},
}}
/>
>
<Mail />
Sign up with Email
</Button>
</Box>
<Box maxWidth={400}>
<Typography className="tos-p">
@@ -69,6 +73,11 @@ const LandingPage = ({ isAdmin, onSignup }) => {
"noreferrer"
);
}}
sx={{
"&:hover": {
color: theme.palette.text.tertiary,
},
}}
>
Terms of Service
</Typography>{" "}
@@ -82,6 +91,11 @@ const LandingPage = ({ isAdmin, onSignup }) => {
"noreferrer"
);
}}
sx={{
"&:hover": {
color: theme.palette.text.tertiary,
},
}}
>
Privacy Policy.
</Typography>
@@ -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 (
<>
<Stack gap={{ xs: theme.gap.ml, sm: theme.gap.large }} textAlign="center">
<Stack
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
textAlign="center"
>
<Box>
<Typography component="h1">Sign Up</Typography>
<Typography>Enter your personal details</Typography>
</Box>
<Box textAlign="left">
<form noValidate spellCheck={false} onSubmit={onSubmit}>
<Box
component="form"
noValidate
spellCheck={false}
onSubmit={onSubmit}
mb={theme.spacing(10)}
>
<Field
id="register-firstname-input"
label="Name"
@@ -138,8 +161,14 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
error={errors.firstName}
ref={inputRef}
/>
</form>
<form noValidate spellCheck={false} onSubmit={onSubmit}>
</Box>
<Box
component="form"
noValidate
spellCheck={false}
onSubmit={onSubmit}
mb={theme.spacing(5)}
>
<Field
id="register-lastname-input"
label="Surname"
@@ -150,30 +179,33 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
onChange={onChange}
error={errors.lastName}
/>
</form>
</Box>
</Box>
<Stack direction="row" justifyContent="space-between">
<Button
level="secondary"
label="Back"
animate="slideLeft"
img={<ArrowBackRoundedIcon />}
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 }}
/>
>
<ArrowBackRoundedIcon />
Back
</Button>
<Button
level="primary"
label="Continue"
variant="contained"
color="primary"
onClick={onSubmit}
disabled={(errors.firstName || errors.lastName) && true}
sx={{ width: "30%" }}
/>
>
Continue
</Button>
</Stack>
</Stack>
</>
@@ -211,13 +243,22 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
return (
<>
<Stack gap={{ xs: theme.gap.ml, sm: theme.gap.large }} textAlign="center">
<Stack
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
textAlign="center"
>
<Box>
<Typography component="h1">Sign Up</Typography>
<Typography>Enter your email address</Typography>
</Box>
<Box textAlign="left">
<form noValidate spellCheck={false} onSubmit={onSubmit}>
<Box
component="form"
noValidate
spellCheck={false}
onSubmit={onSubmit}
mb={theme.spacing(5)}
>
<Field
type="email"
id="register-email-input"
@@ -231,30 +272,33 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
error={errors.email}
ref={inputRef}
/>
</form>
</Box>
</Box>
<Stack direction="row" justifyContent="space-between">
<Button
level="secondary"
label="Back"
animate="slideLeft"
img={<ArrowBackRoundedIcon />}
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 }}
/>
>
<ArrowBackRoundedIcon />
Back
</Button>
<Button
level="primary"
label="Continue"
variant="contained"
color="primary"
onClick={onSubmit}
disabled={errors.email && true}
sx={{ width: "30%" }}
/>
>
Continue
</Button>
</Stack>
</Stack>
</>
@@ -292,13 +336,28 @@ const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => {
return (
<>
<Stack gap={{ xs: theme.gap.ml, sm: theme.gap.large }} textAlign="center">
<Stack
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
textAlign="center"
>
<Box>
<Typography component="h1">Sign Up</Typography>
<Typography>Create your password</Typography>
</Box>
<Box textAlign="left">
<form noValidate spellCheck={false} onSubmit={onSubmit}>
<Box
textAlign="left"
sx={{
"& .input-error": {
display: "none",
},
}}
>
<Box
component="form"
noValidate
spellCheck={false}
onSubmit={onSubmit}
>
<Field
type="password"
id="register-password-input"
@@ -311,8 +370,13 @@ const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => {
error={errors.password}
ref={inputRef}
/>
</form>
<form noValidate spellCheck={false} onSubmit={onSubmit}>
</Box>
<Box
component="form"
noValidate
spellCheck={false}
onSubmit={onSubmit}
>
<Field
type="password"
id="register-confirm-input"
@@ -324,10 +388,10 @@ const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => {
onChange={onChange}
error={errors.confirm}
/>
</form>
</Box>
<Stack
gap={theme.gap.small}
mb={{ xs: theme.gap.ml, sm: theme.gap.large }}
gap={theme.spacing(4)}
mb={{ xs: theme.spacing(6), sm: theme.spacing(8) }}
>
<Check
text={
@@ -388,26 +452,29 @@ const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => {
</Box>
<Stack direction="row" justifyContent="space-between">
<Button
level="secondary"
label="Back"
animate="slideLeft"
img={<ArrowBackRoundedIcon />}
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 }}
/>
>
<ArrowBackRoundedIcon />
Back
</Button>
<Button
level="primary"
label="Continue"
variant="contained"
color="primary"
onClick={onSubmit}
disabled={errors.email && true}
sx={{ width: "30%" }}
/>
>
Continue
</Button>
</Stack>
</Stack>
</>
@@ -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 (
<Stack className="register-page auth" overflow="hidden">
<Stack
className="register-page auth"
overflow="hidden"
sx={{
"& h1": {
color: theme.palette.primary.main,
fontWeight: 600,
fontSize: 30,
},
"& p": {
fontSize: 14,
color: theme.palette.text.accent,
},
}}
>
<Box
className="background-pattern-svg"
sx={{ backgroundImage: `url(${background})` }}
@@ -588,8 +673,8 @@ const Register = ({ isAdmin }) => {
<Stack
direction="row"
alignItems="center"
px={theme.gap.large}
gap={theme.gap.small}
px={theme.spacing(12)}
gap={theme.spacing(4)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>BlueWave Uptime</Typography>
@@ -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 ? (
<LandingPage isAdmin={isAdmin} onSignup={() => setStep(1)} />
<LandingPage
isSuperAdmin={isSuperAdmin}
onSignup={() => setStep(1)}
/>
) : step === 1 ? (
<StepOne
form={form}
@@ -645,17 +733,17 @@ const Register = ({ isAdmin }) => {
""
)}
</Stack>
<Box textAlign="center" p={theme.gap.large}>
<Box textAlign="center" p={theme.spacing(12)}>
<Typography display="inline-block">
Already have an account? —
</Typography>
<Typography
component="span"
ml={theme.gap.xs}
ml={theme.spacing(2)}
onClick={() => {
navigate("/login");
}}
sx={{ userSelect: "none" }}
sx={{ userSelect: "none", color: theme.palette.primary.main }}
>
Log In
</Typography>
@@ -664,6 +752,6 @@ const Register = ({ isAdmin }) => {
);
};
Register.propTypes = {
isAdmin: PropTypes.bool,
isSuperAdmin: PropTypes.bool,
};
export default Register;
+61 -26
View File
@@ -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 (
<Stack className="set-new-password-page auth" overflow="hidden">
<Stack
className="set-new-password-page auth"
overflow="hidden"
sx={{
"& h1": {
color: theme.palette.primary.main,
fontWeight: 600,
fontSize: 24,
},
"& p": {
fontSize: 14,
color: theme.palette.text.accent,
},
}}
>
<Box
className="background-pattern-svg"
sx={{ backgroundImage: `url(${background})` }}
@@ -112,8 +126,8 @@ const SetNewPassword = () => {
<Stack
direction="row"
alignItems="center"
px={theme.gap.large}
gap={theme.gap.small}
px={theme.spacing(12)}
gap={theme.spacing(4)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>BlueWave Uptime</Typography>
@@ -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),
},
},
}}
>
<Stack
gap={{ xs: theme.gap.ml, sm: theme.gap.large }}
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
alignItems="center"
textAlign="center"
>
@@ -151,8 +165,21 @@ const SetNewPassword = () => {
Your new password must be different to previously used passwords.
</Typography>
</Box>
<Box width="100%" textAlign="left">
<form noValidate spellCheck={false} onSubmit={handleSubmit}>
<Box
width="100%"
textAlign="left"
sx={{
"& .input-error": {
display: "none",
},
}}
>
<Box
component="form"
noValidate
spellCheck={false}
onSubmit={handleSubmit}
>
<Field
type="password"
id="register-password-input"
@@ -163,8 +190,13 @@ const SetNewPassword = () => {
onChange={handleChange}
error={errors.password}
/>
</form>
<form noValidate spellCheck={false} onSubmit={handleSubmit}>
</Box>
<Box
component="form"
noValidate
spellCheck={false}
onSubmit={handleSubmit}
>
<Field
type="password"
id="confirm-password-input"
@@ -175,8 +207,8 @@ const SetNewPassword = () => {
onChange={handleChange}
error={errors.confirm}
/>
</form>
<Stack gap={theme.gap.small} mb={theme.gap.large}>
</Box>
<Stack gap={theme.spacing(4)} mb={theme.spacing(12)}>
<Check
text={
<>
@@ -234,21 +266,24 @@ const SetNewPassword = () => {
/>
</Stack>
</Box>
<ButtonSpinner
disabled={Object.keys(errors).length !== 0}
isLoading={isLoading}
<LoadingButton
variant="contained"
color="primary"
loading={isLoading}
onClick={handleSubmit}
level="primary"
label="Reset password"
disabled={Object.keys(errors).length !== 0}
sx={{ width: "100%", maxWidth: 400 }}
/>
>
Reset password
</LoadingButton>
</Stack>
</Stack>
<Box textAlign="center" p={theme.gap.large}>
<Box textAlign="center" p={theme.spacing(12)}>
<Typography display="inline-block">Go back to —</Typography>
<Typography
component="span"
ml={theme.gap.xs}
color={theme.palette.primary.main}
ml={theme.spacing(2)}
onClick={() => navigate("/login")}
sx={{ userSelect: "none" }}
>
+6 -16
View File
@@ -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) {
@@ -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" ? (
<Box>
<Typography textAlign="center">No incidents recorded yet.</Typography>
<Box sx={{ ...sharedStyles }}>
<Typography textAlign="center" color={theme.palette.text.secondary}>
No incidents recorded yet.
</Typography>
</Box>
) : checks?.length === 0 ? (
<Box>
<Typography textAlign="center">
<Box sx={{ ...sharedStyles }}>
<Typography textAlign="center" color={theme.palette.text.secondary}>
The monitor you have selected has no recorded incidents yet.
</Typography>
</Box>
@@ -139,6 +146,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
<TableCell>Monitor Name</TableCell>
<TableCell>Status</TableCell>
<TableCell>Date & Time</TableCell>
<TableCell>Status Code</TableCell>
<TableCell>Message</TableCell>
</TableRow>
</TableHead>
@@ -159,7 +167,10 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
<TableCell>
{new Date(check.createdAt).toLocaleString()}
</TableCell>
<TableCell>{check.statusCode}</TableCell>
<TableCell>
{check.statusCode ? check.statusCode : "N/A"}
</TableCell>
<TableCell>{check.message}</TableCell>
</TableRow>
);
})}
-19
View File
@@ -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;
}
+43 -31
View File
@@ -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 (
<>
<Stack direction="row" alignItems="center" gap={theme.gap.medium}>
<Stack direction="row" alignItems="center" gap={theme.spacing(6)}>
<Skeleton variant="rounded" width={150} height={34} />
<Skeleton variant="rounded" width="15%" height={34} />
<Skeleton
@@ -48,16 +53,15 @@ const Incidents = () => {
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 (
<Stack className="incidents" pt={theme.gap.xl} gap={theme.gap.large}>
<Stack className="incidents" pt={theme.spacing(21)} gap={theme.spacing(12)}>
{loading ? (
<SkeletonLayout />
) : (
<>
<Stack direction="row" alignItems="center" gap={theme.gap.medium}>
<Typography display="inline-block" component="h1">
<Stack direction="row" alignItems="center" gap={theme.spacing(6)}>
<Typography
display="inline-block"
component="h1"
color={theme.palette.text.secondary}
>
Incidents for
</Typography>
<Select
@@ -94,35 +102,39 @@ const Incidents = () => {
value={selectedMonitor}
onChange={handleSelect}
items={Object.values(monitors)}
sx={{
backgroundColor: theme.palette.background.main,
}}
/>
<ButtonGroup sx={{ ml: "auto" }}>
<ButtonGroup
sx={{
ml: "auto",
"& .MuiButtonBase-root, & .MuiButtonBase-root:hover": {
borderColor: theme.palette.border.light,
},
}}
>
<Button
level="secondary"
label="All"
variant="group"
filled={(filter === "all").toString()}
onClick={() => setFilter("all")}
sx={{
backgroundColor:
filter === "all" && theme.palette.otherColors.fillGray,
}}
/>
>
All
</Button>
<Button
level="secondary"
label="Down"
variant="group"
filled={(filter === "down").toString()}
onClick={() => setFilter("down")}
sx={{
backgroundColor:
filter === "down" && theme.palette.otherColors.fillGray,
}}
/>
>
Down
</Button>
<Button
level="secondary"
label="Cannot Resolve"
variant="group"
filled={(filter === "resolve").toString()}
onClick={() => setFilter("resolve")}
sx={{
backgroundColor:
filter === "resolve" && theme.palette.otherColors.fillGray,
}}
/>
>
Cannot Resolve
</Button>
</ButtonGroup>
</Stack>
<IncidentTable
-10
View File
@@ -1,7 +1,3 @@
.integrations h1.MuiTypography-root,
.integrations p.MuiTypography-root {
color: var(--env-var-color-5);
}
.integrations h1.MuiTypography-root {
font-size: var(--env-var-font-size-large);
font-weight: 600;
@@ -12,9 +8,3 @@
.integrations button {
height: var(--env-var-height-2);
}
.integrations .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);
}
+26 -12
View File
@@ -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 }) => {
<Stack
direction="row"
justifyContent="space-between"
gap={theme.gap.large}
p={theme.gap.ml}
pl={theme.gap.large}
gap={theme.spacing(12)}
p={theme.spacing(8)}
pl={theme.spacing(12)}
height="100%"
border={1}
borderColor={theme.palette.border.light}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.background.main}
>
{icon}
<Stack gap={theme.gap.xs} flex={1}>
<Stack gap={theme.spacing(2)} flex={1}>
<Typography component="h1">{header}</Typography>
<Typography
sx={{
@@ -43,12 +46,14 @@ const IntegrationsComponent = ({ icon, header, info, onClick }) => {
</Typography>
</Stack>
<Button
label="Add"
level="primary"
variant="contained"
color="primary"
onClick={onClick}
sx={{ alignSelf: "center" }}
disabled={true}
/>
>
Add
</Button>
</Stack>
</Grid>
);
@@ -108,12 +113,21 @@ const Integrations = () => {
];
return (
<Stack className="integrations" pt={theme.gap.xl} gap={theme.gap.xs}>
<Stack
className="integrations"
pt={theme.spacing(20)}
gap={theme.spacing(2)}
sx={{
"& h1, & p": {
color: theme.palette.text.secondary,
},
}}
>
<Typography component="h1">Integrations</Typography>
<Typography mb={theme.gap.large}>
<Typography mb={theme.spacing(12)}>
Connect BlueWave Uptime to your favorite service.
</Typography>
<Grid container spacing={theme.gap.large}>
<Grid container spacing={theme.spacing(12)}>
{integrations.map((integration, index) => (
<IntegrationsComponent
key={index}
@@ -1,7 +1,6 @@
import { Box, Stack, Typography } from "@mui/material";
import { Box, Button, Stack, Typography } from "@mui/material";
import "./index.css";
import { useState } from "react";
import Button from "../../../Components/Button";
import Back from "../../../assets/icons/left-arrow-long.svg?react";
import Select from "../../../Components/Inputs/Select";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
@@ -13,6 +12,7 @@ import Field from "../../../Components/Inputs/Field";
import { maintenanceWindowValidation } from "../../../Validation/validation";
import { logger } from "../../../Utils/Logger";
import { createToast } from "../../../Utils/toastUtils";
import { useTheme } from "@emotion/react";
const directory = {
title: "Create a maintenance window",
@@ -56,13 +56,15 @@ const durationOptions = [
];
const CreateNewMaintenanceWindow = () => {
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: (
<Stack className="duration-config" gap={2} direction="row">
<Stack
className="duration-config"
gap={theme.spacing(5)}
direction="row"
>
<Field
id="duration-value"
placeholder="60"
@@ -178,14 +184,14 @@ const CreateNewMaintenanceWindow = () => {
),
},
{
title: "Friendly name",
title: "Display name",
component: (
<Field
id="friendly-name"
id="display-name"
placeholder="Maintanence at __ : __ for ___ minutes"
value={values.friendlyName}
onChange={(e) => handleChange(e, "friendlyName")}
error={errors.friendlyName}
value={values.displayName}
onChange={(e) => handleChange(e, "displayName")}
error={errors.displayName}
/>
),
},
@@ -195,7 +201,7 @@ const CreateNewMaintenanceWindow = () => {
<Stack
className="add-monitors-fields"
sx={{ width: "60%", maxWidth: "380px" }}
gap={2}
gap={theme.spacing(5)}
>
<Field
id="add-monitors"
@@ -208,7 +214,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 (
<div className="create-maintenance-window">
<Stack gap={3}>
<Stack gap={theme.spacing(10)}>
<Button
id="btn-back"
variant="contained"
color="secondary"
sx={{
width: "100px",
height: "30px",
gap: "10px",
backgroundColor: "var(--env-var-color-32)",
color: "var(--env-var-color-5)",
}}
label="Back"
level="tertiary"
img={<Back />}
/>
>
<Back />
Back
</Button>
<Box>
<Typography
sx={{
fontSize: "var(--env-var-font-size-large)",
fontWeight: "600",
color: "var(--env-var-color-5)",
fontWeight: 600,
color: theme.palette.text.secondary,
}}
>
{directory.title}
@@ -252,14 +257,14 @@ const CreateNewMaintenanceWindow = () => {
</Box>
<Stack
className="maintenance-options"
gap={5}
paddingY={4}
paddingX={8}
paddingBottom={10}
gap={theme.spacing(20)}
paddingY={theme.spacing(15)}
paddingX={theme.spacing(20)}
sx={{
border: "1px solid var(--env-var-color-16)",
borderRadius: "var(--env-var-radius-1)",
backgroundColor: "var(--env-var-color-0)",
border: 1,
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
backgroundColor: theme.palette.background.main,
}}
>
{configOptions.map((item, index) => (
@@ -271,26 +276,19 @@ const CreateNewMaintenanceWindow = () => {
</Stack>
<Stack justifyContent="end" direction="row" marginTop={3}>
<Button
variant="text"
color="info"
sx={{
"&:hover": {
backgroundColor: "transparent",
boxShadow: "none",
},
}}
level="tertiary"
label="Cancel"
/>
<Button
sx={{
"&:hover": {
backgroundColor: "#1570EF",
boxShadow: "none",
},
}}
level="primary"
label="Create"
onClick={handleSubmit}
/>
>
Cancel
</Button>
<Button variant="contained" color="primary" onClick={handleSubmit}>
Create
</Button>
</Stack>
</Stack>
</div>
+2 -2
View File
@@ -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;
}
+21 -3
View File
@@ -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 (
<div className="maintenance">
<Box
className="maintenance"
sx={{
':has(> [class*="fallback__"])': {
position: "relative",
border: 1,
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
borderStyle: "dashed",
backgroundColor: theme.palette.background.main,
overflow: "hidden",
},
}}
>
<Fallback
title="maintenance window"
checks={[
@@ -12,8 +29,9 @@ const Maintenance = () => {
"Stop sending alerts in maintenance windows",
]}
link="/maintenance/create"
isAdmin={isAdmin}
/>
</div>
</Box>
);
};
+2 -59
View File
@@ -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;
}
+145 -138
View File
@@ -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 (
<>
<Skeleton variant="rounded" width="15%" height={34} />
<Stack gap={theme.gap.xl} mt={theme.gap.medium}>
<Stack direction="row" gap={theme.gap.small} mt={theme.gap.small}>
<Skeleton
variant="circular"
style={{ minWidth: 24, minHeight: 24 }}
/>
<Box width="80%">
<Skeleton
variant="rounded"
width="50%"
height={24}
sx={{ mb: theme.gap.small }}
/>
<Skeleton variant="rounded" width="50%" height={18} />
</Box>
<Stack
direction="row"
gap={theme.gap.medium}
sx={{
ml: "auto",
alignSelf: "flex-end",
}}
>
<Skeleton variant="rounded" width={150} height={34} />
</Stack>
</Stack>
<Skeleton variant="rounded" width="100%" height={200} />
<Skeleton variant="rounded" width="100%" height={200} />
<Skeleton variant="rounded" width="100%" height={200} />
<Stack direction="row" justifyContent="flex-end">
<Skeleton variant="rounded" width="15%" height={34} />
</Stack>
</Stack>
</>
);
};
/**
* 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 (
<Stack className="configure-monitor" gap={theme.gap.large}>
{loading ? (
<Stack className="configure-monitor" gap={theme.spacing(12)}>
{Object.keys(monitor).length === 0 ? (
<SkeletonLayout />
) : (
<>
@@ -225,20 +207,25 @@ const Configure = () => {
component="form"
noValidate
spellCheck="false"
gap={theme.gap.large}
gap={theme.spacing(12)}
flex={1}
>
<Stack direction="row" gap={theme.gap.xs}>
<Stack direction="row" gap={theme.spacing(2)}>
<PulseDot
color={
monitor?.status
? theme.label.up.dotColor
: theme.label.down.dotColor
? theme.palette.success.main
: theme.palette.error.main
}
/>
<Box>
{parsedUrl?.host ? (
<Typography component="h1" mb={theme.gap.xs} lineHeight={1}>
<Typography
component="h1"
mb={theme.spacing(2)}
lineHeight={1}
color={theme.palette.text.primary}
>
{parsedUrl.host || "..."}
</Typography>
) : (
@@ -246,11 +233,11 @@ const Configure = () => {
)}
<Typography
component="span"
lineHeight={theme.gap.large}
lineHeight={theme.spacing(12)}
sx={{
color: monitor?.status
? "var(--env-var-color-17)"
: "var(--env-var-color-24)",
? theme.palette.success.main
: theme.palette.error.text,
}}
>
Your site is {monitor?.status ? "up" : "down"}.
@@ -262,45 +249,51 @@ const Configure = () => {
ml: "auto",
}}
>
<Button
level="tertiary"
label="Pause"
animate="rotate180"
img={<PauseCircleOutlineIcon />}
<LoadingButton
variant="contained"
color="secondary"
loading={isLoading}
sx={{
backgroundColor: theme.palette.otherColors.fillGray,
pl: theme.gap.small,
pr: theme.gap.medium,
mr: theme.gap.medium,
backgroundColor: theme.palette.background.main,
px: theme.spacing(5),
mr: theme.spacing(6),
"& svg": {
mr: theme.gap.xs,
mr: theme.spacing(2),
},
}}
/>
onClick={handlePause}
>
{monitor?.isActive ? (
<>
<PauseCircleOutlineIcon />
Pause
</>
) : (
<>
<PlayCircleOutlineRoundedIcon />
Resume
</>
)}
</LoadingButton>
<Button
level="error"
label="Remove"
sx={{
boxShadow: "none",
px: theme.gap.ml,
}}
variant="contained"
color="error"
sx={{ px: theme.spacing(8) }}
onClick={() => setIsOpen(true)}
/>
>
Remove
</Button>
</Box>
</Stack>
<Stack
className="config-box"
direction="row"
justifyContent="space-between"
>
<ConfigBox>
<Box>
<Typography component="h2">General settings</Typography>
<Typography component="p" sx={{ mt: theme.gap.xs }}>
<Typography component="p">
Here you can select the URL of the host, together with the
type of monitor.
</Typography>
</Box>
<Stack gap={theme.gap.xl}>
<Stack gap={theme.spacing(20)}>
<Field
type={monitor?.type === "http" ? "url" : "text"}
https={protocol === "https"}
@@ -314,7 +307,7 @@ const Configure = () => {
<Field
type="text"
id="monitor-name"
label="Friendly name"
label="Display name"
isOptional={true}
placeholder="Google"
value={monitor?.name || ""}
@@ -322,20 +315,16 @@ const Configure = () => {
error={errors["name"]}
/>
</Stack>
</Stack>
<Stack
className="config-box"
direction="row"
justifyContent="space-between"
>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">Incident notifications</Typography>
<Typography component="p" mt={theme.gap.xs}>
<Typography component="p">
When there is an incident, notify users.
</Typography>
</Box>
<Stack gap={theme.gap.medium}>
<Typography component="p" mt={theme.gap.small}>
<Stack gap={theme.spacing(6)}>
<Typography component="p">
When there is a new incident,
</Typography>
<Checkbox
@@ -368,7 +357,7 @@ const Configure = () => {
{monitor?.notifications?.some(
(notification) => notification.type === "emails"
) ? (
<Box mx={`calc(${theme.gap.ml} * 2)`}>
<Box mx={theme.spacing(16)}>
<Field
id="notify-email-list"
type="text"
@@ -376,7 +365,7 @@ const Configure = () => {
value=""
onChange={() => logger.warn("disabled")}
/>
<Typography mt={theme.gap.small}>
<Typography mt={theme.spacing(4)}>
You can separate multiple emails with a comma
</Typography>
</Box>
@@ -384,16 +373,12 @@ const Configure = () => {
""
)}
</Stack>
</Stack>
<Stack
className="config-box"
direction="row"
justifyContent="space-between"
>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">Advanced settings</Typography>
</Box>
<Stack gap={theme.gap.xl}>
<Stack gap={theme.spacing(20)}>
<Select
id="monitor-interval-configure"
label="Check frequency"
@@ -402,14 +387,17 @@ const Configure = () => {
items={frequencies}
/>
</Stack>
</Stack>
</ConfigBox>
<Stack direction="row" justifyContent="flex-end" mt="auto">
<Button
level="primary"
label="Save"
sx={{ px: theme.gap.large }}
<LoadingButton
variant="contained"
color="primary"
loading={isLoading}
sx={{ px: theme.spacing(12) }}
onClick={handleSubmit}
/>
>
Save
</LoadingButton>
</Stack>
</Stack>
</>
@@ -422,41 +410,60 @@ const Configure = () => {
disablePortal
>
<Stack
gap={theme.gap.xs}
gap={theme.spacing(2)}
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 400,
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",
},
}}
>
<Typography id="modal-delete-monitor" component="h2">
<Typography
id="modal-delete-monitor"
component="h2"
fontSize={16}
color={theme.palette.text.primary}
fontWeight={600}
>
Do you really want to delete this monitor?
</Typography>
<Typography id="delete-monitor-confirmation">
<Typography
id="delete-monitor-confirmation"
color={theme.palette.text.tertiary}
>
Once deleted, this monitor cannot be retrieved.
</Typography>
<Stack
direction="row"
gap={theme.gap.small}
mt={theme.gap.large}
gap={theme.spacing(4)}
mt={theme.spacing(12)}
justifyContent="flex-end"
>
<Button
level="tertiary"
label="Cancel"
variant="text"
color="info"
onClick={() => setIsOpen(false)}
/>
<Button level="error" label="Delete" onClick={handleRemove} />
>
Cancel
</Button>
<LoadingButton
variant="contained"
color="error"
loading={isLoading}
onClick={handleRemove}
>
Delete
</LoadingButton>
</Stack>
</Stack>
</Modal>
@@ -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 (
<>
<Skeleton variant="rounded" width="15%" height={34} />
<Stack gap={theme.spacing(20)} mt={theme.spacing(6)}>
<Stack direction="row" gap={theme.spacing(4)} mt={theme.spacing(4)}>
<Skeleton
variant="circular"
style={{ minWidth: 24, minHeight: 24 }}
/>
<Box width="80%">
<Skeleton
variant="rounded"
width="50%"
height={24}
sx={{ mb: theme.spacing(4) }}
/>
<Skeleton variant="rounded" width="50%" height={18} />
</Box>
<Stack
direction="row"
gap={theme.spacing(6)}
sx={{
ml: "auto",
alignSelf: "flex-end",
}}
>
<Skeleton variant="rounded" width={150} height={34} />
</Stack>
</Stack>
<Skeleton variant="rounded" width="100%" height={200} />
<Skeleton variant="rounded" width="100%" height={200} />
<Skeleton variant="rounded" width="100%" height={200} />
<Stack direction="row" justifyContent="flex-end">
<Skeleton variant="rounded" width="15%" height={34} />
</Stack>
</Stack>
</>
);
};
export default SkeletonLayout;
@@ -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;
}
+60 -116
View File
@@ -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 (
<Box className="create-monitor">
<Box
className="create-monitor"
sx={{
"& h1": {
color: theme.palette.text.primary,
},
}}
>
<Breadcrumbs
list={[
{ name: "monitors", path: "/monitors" },
@@ -156,14 +164,14 @@ const CreateMonitor = () => {
onSubmit={handleCreateMonitor}
noValidate
spellCheck="false"
gap={theme.gap.large}
mt={theme.gap.medium}
gap={theme.spacing(12)}
mt={theme.spacing(6)}
>
<Typography component="h1">
<Typography
component="span"
fontSize="inherit"
color={theme.palette.otherColors.bluishGray}
color={theme.palette.text.secondary}
>
Create your{" "}
</Typography>
@@ -171,15 +179,15 @@ const CreateMonitor = () => {
monitor
</Typography>
</Typography>
<Stack className="config-box">
<ConfigBox>
<Box>
<Typography component="h2">General settings</Typography>
<Typography component="p" mt={theme.gap.xs}>
<Typography component="p">
Here you can select the URL of the host, together with the type of
monitor.
</Typography>
</Box>
<Stack gap={theme.gap.xl}>
<Stack gap={theme.spacing(15)}>
<Field
type={monitor.type === "http" ? "url" : "text"}
id="monitor-url"
@@ -193,7 +201,7 @@ const CreateMonitor = () => {
<Field
type="text"
id="monitor-name"
label="Friendly name"
label="Display name"
isOptional={true}
placeholder="Google"
value={monitor.name}
@@ -201,17 +209,17 @@ const CreateMonitor = () => {
error={errors["name"]}
/>
</Stack>
</Stack>
<Stack className="config-box">
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">Checks to perform</Typography>
<Typography component="p" mt={theme.gap.xs}>
<Typography component="p">
You can always add or remove checks after adding your site.
</Typography>
</Box>
<Stack gap={theme.gap.large}>
<Stack gap={theme.gap.medium}>
<RadioButton
<Stack gap={theme.spacing(12)}>
<Stack gap={theme.spacing(6)}>
<Radio
id="monitor-checks-http"
title="Website monitoring"
desc="Use HTTP(s) to monitor your website or API endpoint."
@@ -223,29 +231,25 @@ const CreateMonitor = () => {
{monitor.type === "http" ? (
<ButtonGroup sx={{ ml: "32px" }}>
<Button
level="secondary"
label="HTTPS"
variant="group"
filled={https.toString()}
onClick={() => setHttps(true)}
sx={{
backgroundColor:
https && theme.palette.otherColors.fillGray,
}}
/>
>
HTTPS
</Button>
<Button
level="secondary"
label="HTTP"
variant="group"
filled={(!https).toString()}
onClick={() => setHttps(false)}
sx={{
backgroundColor:
!https && theme.palette.otherColors.fillGray,
}}
/>
>
HTTP
</Button>
</ButtonGroup>
) : (
""
)}
</Stack>
<RadioButton
<Radio
id="monitor-checks-ping"
title="Ping monitoring"
desc="Check whether your server is available or not."
@@ -256,7 +260,11 @@ const CreateMonitor = () => {
/>
{errors["type"] ? (
<Box className="error-container">
<Typography component="p" className="input-error">
<Typography
component="p"
className="input-error"
color={theme.palette.error.text}
>
{errors["type"]}
</Typography>
</Box>
@@ -264,15 +272,15 @@ const CreateMonitor = () => {
""
)}
</Stack>
</Stack>
<Stack className="config-box">
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">Incident notifications</Typography>
<Typography component="p" mt={theme.gap.xs}>
<Typography component="p">
When there is an incident, notify users.
</Typography>
</Box>
<Stack gap={theme.gap.medium}>
<Stack gap={theme.spacing(6)}>
<Typography component="p">When there is a new incident,</Typography>
<Checkbox
id="notify-sms"
@@ -302,7 +310,7 @@ const CreateMonitor = () => {
{monitor.notifications.some(
(notification) => notification.type === "emails"
) ? (
<Box mx={`calc(${theme.gap.ml} * 2)`}>
<Box mx={theme.spacing(16)}>
<Field
id="notify-email-list"
type="text"
@@ -310,7 +318,7 @@ const CreateMonitor = () => {
value=""
onChange={() => logger.warn("disabled")}
/>
<Typography mt={theme.gap.small}>
<Typography mt={theme.spacing(4)}>
You can separate multiple emails with a comma
</Typography>
</Box>
@@ -318,12 +326,12 @@ const CreateMonitor = () => {
""
)}
</Stack>
</Stack>
<Stack className="config-box">
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">Advanced settings</Typography>
</Box>
<Stack gap={theme.gap.large}>
<Stack gap={theme.spacing(12)}>
<Select
id="monitor-interval"
label="Check frequency"
@@ -331,81 +339,17 @@ const CreateMonitor = () => {
onChange={(event) => handleChange(event, "interval")}
items={frequencies}
/>
{/* TODO */}
{/* <FlexibileTextField
id="monitor-settings-retries"
title="Maximum retries before the service is marked as down"
type="number"
value={advancedSettings.retries}
onChange={(event) =>
handleChange(event, "retries", setAdvancedSettings)
}
/>
<FlexibileTextField
id="monitor-settings-codes"
title="Accepted status codes"
type="number"
value={advancedSettings.codes}
onChange={(event) =>
handleChange(event, "codes", setAdvancedSettings)
}
/>
<FlexibileTextField
id="monitor-settings-redirects"
title="Maximum redirects"
type="number"
value={advancedSettings.redirects}
onChange={(event) =>
handleChange(event, "redirects", setAdvancedSettings)
}
/> */}
</Stack>
</Stack>
{/* TODO */}
{/*
<ConfigBox
leftLayout={
<div className="config-box-desc">
<div className="config-box-desc-title">Proxy settings</div>
</div>
}
rightLayout={
<div className="proxy-setting-config">
<CustomizableCheckBox
id="monitor-proxy-enable"
title="Enable proxy"
isChecked={proxy.enabled}
handleChange={() => handleCheck("enabled", setProxy)}
/>
<FlexibileTextField
id="monitor-proxy-protocol"
title="Proxy protocol"
value={proxy.protocol}
onChange={(event) => handleChange(event, "protocol", setProxy)}
/>
<FlexibileTextField
id="monitor-proxy-address"
title="Proxy address"
value={proxy.address}
onChange={(event) => handleChange(event, "address", setProxy)}
/>
<FlexibileTextField
id="monitor-proxy-port"
title="Proxy port"
value={proxy.proxy_port}
onChange={(event) => handleChange(event, "proxy_port", setProxy)}
/>
</div>
}
/> */}
</ConfigBox>
<Stack direction="row" justifyContent="flex-end">
<Button
id="create-monitor-btn"
level="primary"
label="Create monitor"
variant="contained"
color="primary"
onClick={handleCreateMonitor}
disabled={Object.keys(errors).length !== 0 && true}
/>
>
Create monitor
</Button>
</Stack>
</Stack>
</Box>
@@ -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 }) => {
<TableRow>
<TableCell>Status</TableCell>
<TableCell>Date & Time</TableCell>
<TableCell>Status Code</TableCell>
<TableCell>Message</TableCell>
</TableRow>
</TableHead>
@@ -127,7 +121,10 @@ const PaginationTable = ({ monitorId, dateRange }) => {
<TableCell>
{new Date(check.createdAt).toLocaleString()}
</TableCell>
<TableCell>{check.statusCode}</TableCell>
<TableCell>
{check.statusCode ? check.statusCode : "N/A"}
</TableCell>
<TableCell>{check.message}</TableCell>
</TableRow>
);
})}
+1 -38
View File
@@ -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);
}
+122 -80
View File
@@ -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 (
<Box className="stat-box">
<Typography variant="h6" mb={1} fontWeight={500}>
<Box
className="stat-box"
flex="20%"
minWidth="100px"
px={theme.spacing(8)}
py={theme.spacing(4)}
border={1}
borderColor={theme.palette.border.light}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.background.main}
>
<Typography
variant="h6"
mb={theme.spacing(2)}
fontSize={14}
fontWeight={500}
color={theme.palette.primary.main}
sx={{
"& span": {
color: theme.palette.text.accent,
fontSize: 13,
fontStyle: "italic",
},
}}
>
{title}
</Typography>
<Typography variant="h4" fontWeight={500}>
<Typography
variant="h4"
fontWeight={500}
fontSize={13}
color={theme.palette.text.secondary}
>
{value}
</Typography>
</Box>
@@ -47,8 +82,8 @@ const SkeletonLayout = () => {
return (
<>
<Skeleton variant="rounded" width="20%" height={34} />
<Stack gap={theme.gap.xl} mt={theme.gap.medium}>
<Stack direction="row" gap={theme.gap.small} mt={theme.gap.small}>
<Stack gap={theme.spacing(20)} mt={theme.spacing(6)}>
<Stack direction="row" gap={theme.spacing(4)} mt={theme.spacing(4)}>
<Skeleton
variant="circular"
style={{ minWidth: 24, minHeight: 24 }}
@@ -59,7 +94,7 @@ const SkeletonLayout = () => {
variant="rounded"
width="50%"
height={18}
sx={{ mt: theme.gap.small }}
sx={{ mt: theme.spacing(4) }}
/>
</Box>
<Skeleton
@@ -72,7 +107,7 @@ const SkeletonLayout = () => {
<Stack
direction="row"
justifyContent="space-between"
gap={theme.gap.large}
gap={theme.spacing(12)}
>
<Skeleton variant="rounded" width="100%" height={80} />
<Skeleton variant="rounded" width="100%" height={80} />
@@ -82,7 +117,7 @@ const SkeletonLayout = () => {
<Stack
direction="row"
justifyContent="space-between"
mb={theme.gap.ml}
mb={theme.spacing(8)}
>
<Skeleton
variant="rounded"
@@ -96,7 +131,7 @@ const SkeletonLayout = () => {
<Skeleton variant="rounded" width="100%" height="100%" />
</Box>
</Box>
<Stack gap={theme.gap.ml}>
<Stack gap={theme.spacing(8)}>
<Skeleton variant="rounded" width="20%" height={24} />
<Skeleton variant="rounded" width="100%" height={200} />
<Skeleton variant="rounded" width="100%" height={50} />
@@ -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 (
<Box className="monitor-details">
@@ -169,26 +207,33 @@ const DetailsPage = () => {
{ name: "details", path: `/monitors/${monitorId}` },
]}
/>
<Stack gap={theme.gap.large} mt={theme.gap.large}>
<Stack direction="row" gap={theme.gap.xs}>
<Stack gap={theme.spacing(12)} mt={theme.spacing(12)}>
<Stack direction="row" gap={theme.spacing(2)}>
<PulseDot
color={
monitor?.status
? theme.label.up.dotColor
: theme.label.down.dotColor
? theme.palette.success.main
: theme.palette.error.main
}
/>
<Box>
<Typography component="h1" sx={{ lineHeight: 1 }}>
<Typography
component="h1"
color={theme.palette.text.primary}
lineHeight={1}
>
{monitor.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>
<Typography mt={theme.gap.small}>
<Typography
mt={theme.spacing(4)}
color={theme.palette.text.tertiary}
>
<Typography
component="span"
sx={{
color: monitor?.status
? "var(--env-var-color-17)"
: "var(--env-var-color-24)",
? theme.palette.success.main
: theme.palette.success.text,
}}
>
Your site is {monitor?.status ? "up" : "down"}.
@@ -196,34 +241,31 @@ const DetailsPage = () => {
Checking every {formatDurationRounded(monitor?.interval)}.
</Typography>
</Box>
<Button
level="tertiary"
label="Configure"
animate="rotate90"
img={
<SettingsIcon
style={{
minWidth: theme.gap.mlplus,
minHeight: theme.gap.mlplus,
}}
/>
}
onClick={() => navigate(`/monitors/configure/${monitorId}`)}
sx={{
ml: "auto",
alignSelf: "flex-end",
backgroundColor: "#f4f4f4",
px: theme.gap.medium,
"& svg": {
mr: "6px",
},
}}
/>
{isAdmin && (
<Button
variant="contained"
color="secondary"
onClick={() => 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,
},
},
}}
>
<SettingsIcon /> Configure
</Button>
)}
</Stack>
<Stack
direction="row"
justifyContent="space-between"
gap={theme.gap.large}
gap={theme.spacing(12)}
flexWrap="wrap"
>
<StatBox
@@ -276,51 +318,46 @@ const DetailsPage = () => {
<Stack
direction="row"
justifyContent="space-between"
mb={theme.gap.ml}
mb={theme.spacing(8)}
>
<Typography component="h2" alignSelf="flex-end">
<Typography
component="h2"
alignSelf="flex-end"
color={theme.palette.text.secondary}
>
Response Times
</Typography>
<ButtonGroup>
<Button
level="secondary"
label="Day"
variant="group"
filled={(dateRange === "day").toString()}
onClick={() => setDateRange("day")}
sx={{
backgroundColor:
dateRange === "day" &&
theme.palette.otherColors.fillGray,
}}
/>
>
Day
</Button>
<Button
level="secondary"
label="Week"
variant="group"
filled={(dateRange === "week").toString()}
onClick={() => setDateRange("week")}
sx={{
backgroundColor:
dateRange === "week" &&
theme.palette.otherColors.fillGray,
}}
/>
>
Week
</Button>
<Button
level="secondary"
label="Month"
variant="group"
filled={(dateRange === "month").toString()}
onClick={() => setDateRange("month")}
sx={{
backgroundColor:
dateRange === "month" &&
theme.palette.otherColors.fillGray,
}}
/>
>
Month
</Button>
</ButtonGroup>
</Stack>
<Box
p={theme.gap.mlplus}
pb={theme.gap.xs}
backgroundColor={theme.palette.otherColors.white}
p={theme.spacing(10)}
pb={theme.spacing(2)}
backgroundColor={theme.palette.background.main}
border={1}
borderColor={theme.palette.otherColors.graishWhite}
borderRadius={`${theme.shape.borderRadius}px`}
borderColor={theme.palette.border.light}
borderRadius={theme.shape.borderRadius}
sx={{ height: "250px" }}
>
<MonitorDetailsAreaChart
@@ -328,8 +365,10 @@ const DetailsPage = () => {
/>
</Box>
</Box>
<Stack gap={theme.gap.ml}>
<Typography component="h2">History</Typography>
<Stack gap={theme.spacing(8)}>
<Typography component="h2" color={theme.palette.text.secondary}>
History
</Typography>
<PaginationTable monitorId={monitorId} dateRange={dateRange} />
</Stack>
</Stack>
@@ -339,4 +378,7 @@ const DetailsPage = () => {
);
};
DetailsPage.propTypes = {
isAdmin: PropTypes.bool,
};
export default DetailsPage;
@@ -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 = (
<Box sx={{ ...sharedStyles, top: 8 }}>
<Arrow />
</Box>
);
} else if (title === "down") {
color = theme.palette.error.text;
icon = (
<Box sx={{ ...sharedStyles, transform: "rotate(180deg)", top: 5 }}>
<Arrow />
</Box>
);
} else if (title === "paused") {
color = theme.palette.warning.main;
icon = (
<Box sx={{ ...sharedStyles, top: 12, right: 12 }}>
<ClockSnooze />
</Box>
);
}
return (
<Box
position="relative"
flex={1}
border={1}
borderColor={theme.palette.border.light}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.background.main}
px={theme.spacing(12)}
py={theme.spacing(8)}
overflow="hidden"
sx={{
"&:hover": {
backgroundColor: theme.palette.background.accent,
},
}}
>
<Box
sx={{
position: "absolute",
top: "-10%",
left: "5%",
pointerEvents: "none",
"& svg g g:last-of-type path": {
stroke: theme.palette.other.grid,
},
}}
>
<Background />
</Box>
<Box
textTransform="uppercase"
fontSize={15}
letterSpacing={0.5}
color={theme.palette.text.secondary}
mb={theme.spacing(8)}
sx={{ opacity: 0.6 }}
>
{title}
</Box>
{icon}
<Stack
direction="row"
alignItems="flex-start"
fontSize={36}
fontWeight={600}
color={color}
gap="2px"
>
{value}
<Typography
component="span"
fontSize={20}
fontWeight={300}
color={theme.palette.text.secondary}
sx={{ opacity: 0.3 }}
>
#
</Typography>
</Stack>
</Box>
);
};
StatusBox.propTypes = {
title: PropTypes.oneOf(["up", "down", "paused"]).isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
};
export default StatusBox;
@@ -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 (
<>
<IconButton
aria-label="monitor actions"
onClick={(event) => {
event.stopPropagation();
openMenu(
event,
monitor._id,
monitor.type === "ping" ? null : monitor.url
);
}}
sx={{
"&:focus": {
outline: "none",
},
"& svg path": {
stroke: theme.palette.other.icon,
},
}}
>
<Settings />
</IconButton>
<Menu
className="actions-menu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={(e) => 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 ? (
<MenuItem
onClick={(e) => {
closeMenu(e);
e.stopPropagation();
window.open(actions.url, "_blank", "noreferrer");
}}
>
Open site
</MenuItem>
) : (
""
)}
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/monitors/${actions.id}`);
}}
>
Details
</MenuItem>
{/* TODO - pass monitor id to Incidents page */}
<MenuItem disabled>Incidents</MenuItem>
{isAdmin && (
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/monitors/configure/${actions.id}`);
}}
>
Configure
</MenuItem>
)}
{isAdmin && (
<MenuItem
onClick={(e) => {
e.stopPropagation();
openRemove(e);
}}
>
Remove
</MenuItem>
)}
</Menu>
<Modal
aria-labelledby="modal-delete-monitor"
aria-describedby="delete-monitor-confirmation"
open={isOpen}
onClose={(e) => {
e.stopPropagation();
setIsOpen(false);
}}
>
<Stack
gap={theme.spacing(2)}
color={theme.palette.text.secondary}
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 400,
bgcolor: theme.palette.background.main,
border: 1,
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
boxShadow: 24,
p: theme.spacing(15),
"&:focus": {
outline: "none",
},
}}
>
<Typography id="modal-delete-monitor" component="h2" fontWeight={600}>
Do you really want to delete this monitor?
</Typography>
<Typography id="delete-monitor-confirmation" fontSize={13}>
Once deleted, this monitor cannot be retrieved.
</Typography>
<Stack
direction="row"
gap={theme.spacing(4)}
mt={theme.spacing(12)}
justifyContent="flex-end"
>
<Button
variant="text"
color="info"
onClick={(e) => {
e.stopPropagation();
setIsOpen(false);
}}
>
Cancel
</Button>
<Button
variant="contained"
color="error"
onClick={(e) => {
e.stopPropagation(e);
handleRemove(e);
}}
>
Delete
</Button>
</Stack>
</Stack>
</Modal>
</>
);
};
ActionsMenu.propTypes = {
monitor: PropTypes.shape({
_id: PropTypes.string,
url: PropTypes.string,
type: PropTypes.string,
}).isRequired,
isAdmin: PropTypes.bool,
};
export default ActionsMenu;
@@ -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 (
<Stack
alignItems="center"
backgroundColor={theme.palette.background.main}
p={theme.spacing(30)}
gap={theme.spacing(2)}
border={1}
borderRadius={theme.shape.borderRadius}
borderColor={theme.palette.border.light}
color={theme.palette.text.secondary}
>
<Typography component="h2">No monitors found</Typography>
<Typography>
It looks like you don’t have any monitors set up yet.
</Typography>
{isAdmin && (
<Button
variant="contained"
color="primary"
onClick={() => {
navigate("/monitors/create");
}}
sx={{ mt: theme.spacing(12) }}
>
Create your first monitor
</Button>
)}
</Stack>
);
};
Fallback.propTypes = {
isAdmin: PropTypes.bool,
};
export default Fallback;
+60
View File
@@ -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 (
<Box className="host">
<Box
display="inline-block"
position="relative"
sx={{
fontWeight: 500,
"&:before": {
position: "absolute",
content: `""`,
width: "4px",
height: "4px",
borderRadius: "50%",
backgroundColor: "gray",
opacity: 0.8,
right: "-10px",
top: "42%",
},
}}
>
{params.title}
</Box>
<Typography
component="span"
sx={{
color: params.percentageColor,
fontWeight: 500,
ml: "15px",
}}
>
{params.percentage}%
</Typography>
<Box sx={{ opacity: 0.6 }}>{params.url}</Box>
</Box>
);
};
Host.propTypes = {
params: PropTypes.shape({
title: PropTypes.string,
percentageColor: PropTypes.string,
percentage: PropTypes.number,
}).isRequired,
};
export default Host;
+30
View File
@@ -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;
}
+151
View File
@@ -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 (
<Stack className="monitors" gap={theme.spacing(12)}>
{loading ? (
<SkeletonLayout />
) : (
<>
<Box>
<Breadcrumbs list={[{ name: `monitors`, path: "/monitors" }]} />
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
mt={theme.spacing(5)}
>
<Greeting type="uptime" />
{monitorState.monitors?.length !== 0 && (
<Button
variant="contained"
color="primary"
onClick={() => {
navigate("/monitors/create");
}}
sx={{ fontWeight: 500 }}
>
Create monitor
</Button>
)}
</Stack>
</Box>
{isAdmin && monitorState.monitors?.length === 0 && (
<Fallback isAdmin={isAdmin} />
)}
{monitorState.monitors?.length !== 0 && (
<>
<Stack
gap={theme.spacing(12)}
direction="row"
justifyContent="space-between"
>
<StatusBox title="up" value={monitorStats.up} />
<StatusBox title="down" value={monitorStats.down} />
<StatusBox title="paused" value={monitorStats.paused} />
</Stack>
<Box
flex={1}
px={theme.spacing(16)}
py={theme.spacing(12)}
border={1}
borderColor={theme.palette.border.light}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.background.main}
>
<Stack
direction="row"
alignItems="center"
mb={theme.spacing(12)}
>
<Typography
component="h2"
color={theme.palette.text.secondary}
letterSpacing={-0.5}
>
Actively monitoring
</Typography>
<Box
className="current-monitors-counter"
color={theme.palette.text.primary}
border={1}
borderColor={theme.palette.border.light}
backgroundColor={theme.palette.background.accent}
>
{monitorState.monitors.length}
</Box>
{/* TODO - add search bar */}
</Stack>
<BasicTable data={data} paginated={true} table={"monitors"} />
</Box>
</>
)}
</>
)}
</Stack>
);
};
Monitors.propTypes = {
isAdmin: PropTypes.bool,
};
export default Monitors;
@@ -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: (
<Box width="max-content">
Status
<span>
<ArrowDownwardRoundedIcon />
</span>
</Box>
),
},
{ 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: <Host params={params} /> },
{
id: idx + 1,
data: (
<StatusLabel
status={params.status}
text={params.status}
customStyles={{ textTransform: "capitalize" }}
/>
),
},
{ id: idx + 2, data: <BarChart checks={reversedChecks} /> },
{
id: idx + 3,
data: (
<span style={{ textTransform: "uppercase" }}>{monitor.type}</span>
),
},
{
id: idx + 4,
data: <ActionsMenu monitor={monitor} isAdmin={isAdmin} />,
},
],
};
});
return data;
};
@@ -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 (
<>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Skeleton variant="rounded" width="50%" height={36} />
<Skeleton variant="rounded" width="15%" height={36} />
</Stack>
<Stack
gap={theme.spacing(12)}
direction="row"
justifyContent="space-between"
>
<Skeleton variant="rounded" width="100%" height={100} />
<Skeleton variant="rounded" width="100%" height={100} />
<Skeleton variant="rounded" width="100%" height={100} />
</Stack>
<Stack
gap={theme.spacing(12)}
p={theme.spacing(20)}
backgroundColor="#f9fafb"
>
<Skeleton variant="rounded" width="50%" height={25} />
<Skeleton variant="rounded" width="100%" height={300} />
<Skeleton variant="rounded" width="100%" height={100} />
</Stack>
</>
);
};
export default SkeletonLayout;
-63
View File
@@ -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);
}
-625
View File
@@ -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 (
<>
<IconButton
aria-label="monitor actions"
onClick={(event) => {
event.stopPropagation();
openMenu(
event,
monitor._id,
monitor.type === "ping" ? null : monitor.url
);
}}
sx={{
"&:focus": {
outline: "none",
},
}}
>
<Settings />
</IconButton>
<Menu
className="actions-menu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={(e) => closeMenu(e)}
>
{actions.url !== null ? (
<MenuItem
onClick={(e) => {
closeMenu(e);
e.stopPropagation();
window.open(actions.url, "_blank", "noreferrer");
}}
>
Open site
</MenuItem>
) : (
""
)}
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/monitors/${actions.id}`);
}}
>
Details
</MenuItem>
{/* TODO - pass monitor id to Incidents page */}
<MenuItem disabled>Incidents</MenuItem>
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/monitors/configure/${actions.id}`);
}}
>
Configure
</MenuItem>
<MenuItem
onClick={(e) => {
e.stopPropagation();
openRemove(e);
}}
>
Remove
</MenuItem>
</Menu>
<Modal
aria-labelledby="modal-delete-monitor"
aria-describedby="delete-monitor-confirmation"
open={isOpen}
onClose={(e) => {
e.stopPropagation();
setIsOpen(false);
}}
disablePortal
>
<Stack
gap={theme.gap.xs}
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 400,
bgcolor: "white",
border: "solid 1px #f2f2f2",
borderRadius: `${theme.shape.borderRadius}px`,
boxShadow: 24,
p: "30px",
"&:focus": {
outline: "none",
},
}}
>
<Typography id="modal-delete-monitor" component="h2">
Do you really want to delete this monitor?
</Typography>
<Typography id="delete-monitor-confirmation">
Once deleted, this monitor cannot be retrieved.
</Typography>
<Stack
direction="row"
gap={theme.gap.small}
mt={theme.gap.large}
justifyContent="flex-end"
>
<Button
level="tertiary"
label="Cancel"
onClick={(e) => {
e.stopPropagation();
setIsOpen(false);
}}
/>
<Button
level="error"
label="Delete"
onClick={(e) => handleRemove(e)}
/>
</Stack>
</Stack>
</Modal>
</>
);
};
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 (
<Box className="host">
<Box
display="inline-block"
position="relative"
sx={{
fontWeight: 500,
"&:before": {
position: "absolute",
content: `""`,
width: "4px",
height: "4px",
borderRadius: "50%",
backgroundColor: "gray",
opacity: 0.8,
right: "-10px",
top: "42%",
},
}}
>
{params.title}
</Box>
<Typography
component="span"
sx={{
color: params.percentageColor,
fontWeight: 500,
ml: "15px",
}}
>
{params.percentage}%
</Typography>
<Box sx={{ opacity: 0.6 }}>{params.url}</Box>
</Box>
);
};
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 = (
<Box sx={{ ...sharedStyles, top: 8 }}>
<Arrow />
</Box>
);
} else if (title === "down") {
color = theme.pie.red.stroke;
icon = (
<Box sx={{ ...sharedStyles, transform: "rotate(180deg)", top: 5 }}>
<Arrow />
</Box>
);
} else if (title === "paused") {
color = theme.pie.yellow.stroke;
icon = (
<Box sx={{ ...sharedStyles, top: 12, right: 12 }}>
<ClockSnooze />
</Box>
);
}
return (
<Box
position="relative"
flex={1}
border={1}
borderColor={theme.palette.otherColors.graishWhite}
borderRadius={`${theme.shape.borderRadius}px`}
backgroundColor={theme.palette.otherColors.white}
px={theme.gap.large}
py={theme.gap.ml}
overflow="hidden"
sx={{
"&:hover": {
backgroundColor: "#f9fafb",
},
"&:after": {
position: "absolute",
content: `""`,
backgroundImage: `url(${background})`,
width: "400px",
height: "200px",
top: "-10%",
left: "5%",
zIndex: 10000,
pointerEvents: "none",
},
}}
>
<Box
textTransform="uppercase"
fontSize={15}
letterSpacing={0.5}
color={theme.palette.otherColors.bluishGray}
mb={theme.gap.ml}
sx={{ opacity: 0.6 }}
>
{title}
</Box>
{icon}
<Stack
direction="row"
alignItems="flex-start"
fontSize={36}
fontWeight={600}
color={color}
gap="2px"
>
{value}
<Typography
component="span"
fontSize={20}
fontWeight={300}
color={theme.palette.otherColors.bluishGray}
sx={{ opacity: 0.3 }}
>
#
</Typography>
</Stack>
</Box>
);
};
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 (
<>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Skeleton variant="rounded" width="50%" height={36} />
<Skeleton variant="rounded" width="15%" height={36} />
</Stack>
<Stack
gap={theme.gap.large}
direction="row"
justifyContent="space-between"
>
<Skeleton variant="rounded" width="100%" height={100} />
<Skeleton variant="rounded" width="100%" height={100} />
<Skeleton variant="rounded" width="100%" height={100} />
</Stack>
<Stack gap={theme.gap.large} p={theme.gap.xl} backgroundColor="#f9fafb">
<Skeleton variant="rounded" width="50%" height={25} />
<Skeleton variant="rounded" width="100%" height={300} />
<Skeleton variant="rounded" width="100%" height={100} />
</Stack>
</>
);
};
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: (
<Box width="max-content">
Status
<span>
<ArrowDownwardRoundedIcon />
</span>
</Box>
),
},
{ 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: <Host params={params} /> },
{
id: idx + 1,
data: (
<StatusLabel
status={params.status}
text={params.status}
customStyles={{ textTransform: "capitalize" }}
/>
),
},
{ id: idx + 2, data: <BarChart checks={reversedChecks} /> },
{
id: idx + 3,
data: (
<span style={{ textTransform: "uppercase" }}>{monitor.type}</span>
),
},
{
id: idx + 4,
data: <ActionsMenu monitor={monitor} />,
},
],
};
});
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 (
<Stack className="monitors" gap={theme.gap.large}>
{loading ? (
<SkeletonLayout />
) : (
<>
<Box>
<Breadcrumbs list={[{ name: `monitors`, path: "/monitors" }]} />
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
mt={theme.gap.medium}
>
<Box>
<Typography component="h1" lineHeight={1}>
<Typography
component="span"
fontSize="inherit"
color={theme.palette.otherColors.bluishGray}
>
Good {greeting},{" "}
</Typography>
<Typography
component="span"
fontSize="inherit"
fontWeight="inherit"
>
{authState.user.firstName} {emoji}
</Typography>
</Typography>
<Typography
sx={{ opacity: 0.8 }}
lineHeight={1}
fontWeight={300}
>
Here’s an overview of your uptime monitors.
</Typography>
</Box>
{monitorState.monitors?.length !== 0 && (
<Button
level="primary"
label="Create monitor"
onClick={() => {
navigate("/monitors/create");
}}
sx={{ fontWeight: 500, alignSelf: "flex-end" }}
/>
)}
</Stack>
</Box>
{monitorState.monitors?.length === 0 ? (
<Stack
alignItems="center"
backgroundColor={theme.palette.otherColors.white}
p={theme.gap.xxl}
gap={theme.gap.xs}
border={1}
borderRadius={`${theme.shape.borderRadius}px`}
borderColor={theme.palette.otherColors.graishWhite}
>
<Typography component="h2">No monitors found</Typography>
<Typography>
It looks like you don’t have any monitors set up yet.
</Typography>
<Button
level="primary"
label="Create your first monitor"
onClick={() => {
navigate("/monitors/create");
}}
sx={{ mt: theme.gap.large }}
/>
</Stack>
) : (
<>
<Stack
gap={theme.gap.large}
direction="row"
justifyContent="space-between"
>
<StatusBox title="up" value={up} />
<StatusBox title="down" value={down} />
<StatusBox title="paused" value={0} />
</Stack>
<Box
flex={1}
py={theme.gap.large}
px={theme.gap.lgplus}
border={1}
borderColor={theme.palette.otherColors.graishWhite}
backgroundColor={theme.palette.otherColors.white}
sx={{
borderRadius: `${theme.shape.borderRadius}px`,
}}
>
<Stack direction="row" alignItems="center" mb={theme.gap.ml}>
<Typography component="h2" letterSpacing={-0.5}>
Actively monitoring
</Typography>
<Box className="current-monitors-counter">
{monitorState.monitors.length}
</Box>
{/* TODO - add search bar */}
</Stack>
<BasicTable data={data} paginated={true} table={"monitors"} />
</Box>
</>
)}
</>
)}
</Stack>
);
};
export default Monitors;
+35
View File
@@ -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,
},
}));
-15
View File
@@ -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);
}
+21 -12
View File
@@ -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 (
<Stack className="not-found-page" justifyContent="center">
<Stack gap="20px" alignItems="center">
<Stack height="100vh" justifyContent="center">
<Stack
gap={theme.spacing(2)}
alignItems="center"
color={theme.palette.text.secondary}
>
<img src={NotFoundSvg} alt="404" style={{ maxHeight: "25rem" }} />
<Typography component="h1">{title}</Typography>
<Typography>{desc}</Typography>
<Typography component="h1" fontSize={16} fontWeight={600}>
{title}
</Typography>
<Typography fontSize={13}>{desc}</Typography>
<Button
label="Go to the main dashboard"
level="primary"
sx={{ mt: "24px" }}
variant="contained"
color="primary"
sx={{ mt: theme.spacing(10) }}
onClick={() => navigate("/")}
/>
>
Go to the main dashboard
</Button>
</Stack>
</Stack>
);
+1 -23
View File
@@ -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;
}
+98 -57
View File
@@ -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 (
<>
<Skeleton variant="rounded" width="15%" height={34} />
<Stack gap={theme.gap.xl} mt={theme.gap.medium} maxWidth="1000px">
<Stack direction="row" gap={theme.gap.small} mt={theme.gap.small}>
<Stack gap={theme.spacing(20)} mt={theme.spacing(6)} maxWidth="1000px">
<Stack direction="row" gap={theme.spacing(4)} mt={theme.spacing(4)}>
<Skeleton
variant="circular"
style={{ minWidth: 24, minHeight: 24 }}
@@ -43,13 +42,13 @@ const SkeletonLayout = () => {
variant="rounded"
width="50%"
height={24}
sx={{ mb: theme.gap.small }}
sx={{ mb: theme.spacing(4) }}
/>
<Skeleton variant="rounded" width="50%" height={18} />
</Box>
<Stack
direction="row"
gap={theme.gap.medium}
gap={theme.spacing(6)}
sx={{
ml: "auto",
alignSelf: "flex-end",
@@ -127,7 +126,7 @@ const PageSpeedConfigure = () => {
);
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 (
<Stack className="configure-pagespeed" gap={theme.gap.large}>
<Stack className="configure-pagespeed" gap={theme.spacing(12)}>
{loading ? (
<SkeletonLayout />
) : (
@@ -165,21 +164,22 @@ const PageSpeedConfigure = () => {
spellCheck="false"
onSubmit={handleSave}
flex={1}
gap={theme.gap.large}
gap={theme.spacing(12)}
>
<Stack direction="row" gap={theme.gap.xs}>
<Stack direction="row" gap={theme.spacing(2)}>
<PulseDot
color={
monitor?.status
? theme.label.up.dotColor
: theme.label.down.dotColor
? theme.palette.success.main
: theme.palette.error.main
}
/>
<Box>
<Typography
component="h1"
mb={theme.gap.xs}
mb={theme.spacing(2)}
sx={{ lineHeight: 1 }}
color={theme.palette.text.primary}
>
{monitor?.url}
</Typography>
@@ -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 = () => {
</Box>
<Box alignSelf="flex-end" ml="auto">
<Button
level="tertiary"
label="Pause"
animate="rotate180"
img={<PauseCircleOutlineIcon />}
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,
},
},
}}
/>
>
<PauseCircleOutlineIcon />
Pause
</Button>
<Button
level="error"
label="Remove"
sx={{
boxShadow: "none",
px: theme.gap.ml,
ml: theme.gap.medium,
}}
variant="contained"
color="error"
onClick={() => setIsOpen(true)}
/>
sx={{
ml: theme.spacing(6),
}}
>
Remove
</Button>
</Box>
</Stack>
<Stack gap={theme.gap.xl}>
<Stack
border={1}
borderColor={theme.palette.border.light}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.background.main}
p={theme.spacing(20)}
pl={theme.spacing(15)}
gap={theme.spacing(20)}
sx={{
"& h3, & p": {
color: theme.palette.text.secondary,
},
}}
>
<Stack direction="row">
<Typography component="h3">Monitor friendly name</Typography>
<Typography component="h3">Monitor display name</Typography>
<Field
type="text"
id="monitor-name"
@@ -233,7 +250,14 @@ const PageSpeedConfigure = () => {
error={errors.name}
/>
</Stack>
<Stack direction="row">
<Stack
direction="row"
sx={{
".MuiInputBase-root:has(> .Mui-disabled)": {
backgroundColor: theme.palette.background.accent,
},
}}
>
<Typography component="h3">URL</Typography>
<Field
type="url"
@@ -259,8 +283,11 @@ const PageSpeedConfigure = () => {
Incidents notifications{" "}
<Typography component="span">(coming soon)</Typography>
</Typography>
<Stack className="section-disabled">
<Typography mb={theme.gap.small}>
<Stack
className="section-disabled"
backgroundColor={theme.palette.background.fill}
>
<Typography mb={theme.spacing(4)}>
When there is a new incident,
</Typography>
<Checkbox
@@ -279,7 +306,7 @@ const PageSpeedConfigure = () => {
label="Notify via email to following emails"
isChecked={false}
/>
<Box mx={`calc(${theme.gap.ml} * 2)`}>
<Box mx={theme.spacing(16)}>
<Field
id="notify-emails-list"
placeholder="notifications@gmail.com"
@@ -287,7 +314,7 @@ const PageSpeedConfigure = () => {
onChange={() => logger.warn("disabled")}
error=""
/>
<Typography mt={theme.gap.small}>
<Typography mt={theme.spacing(4)}>
You can separate multiple emails with a comma
</Typography>
</Box>
@@ -297,11 +324,13 @@ const PageSpeedConfigure = () => {
<Stack direction="row" justifyContent="flex-end" mt="auto">
<Button
type="submit"
level="primary"
label="Save"
variant="contained"
color="primary"
onClick={handleSave}
sx={{ px: theme.gap.large, mt: theme.gap.large }}
/>
sx={{ px: theme.spacing(12) }}
>
Save
</Button>
</Stack>
</Stack>
</>
@@ -314,41 +343,53 @@ const PageSpeedConfigure = () => {
disablePortal
>
<Stack
gap={theme.gap.xs}
gap={theme.spacing(2)}
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 400,
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",
},
}}
>
<Typography id="modal-delete-pagespeed-monitor" component="h2">
<Typography
id="modal-delete-pagespeed-monitor"
component="h2"
color={theme.palette.text.secondary}
>
Do you really want to delete this monitor?
</Typography>
<Typography id="delete-pagespeed-monitor-confirmation">
<Typography
id="delete-pagespeed-monitor-confirmation"
color={theme.palette.text.tertiary}
>
Once deleted, this monitor cannot be retrieved.
</Typography>
<Stack
direction="row"
gap={theme.gap.small}
mt={theme.gap.large}
gap={theme.spacing(4)}
mt={theme.spacing(12)}
justifyContent="flex-end"
>
<Button
level="tertiary"
label="Cancel"
variant="text"
color="info"
onClick={() => setIsOpen(false)}
/>
<Button level="error" label="Delete" onClick={handleRemove} />
>
Cancel
</Button>
<Button variant="contained" color="error" onClick={handleRemove}>
Delete
</Button>
</Stack>
</Stack>
</Modal>
@@ -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;
}
@@ -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 (
<Stack className="create-page-speed" gap={theme.gap.mlplus}>
<Stack className="create-page-speed" gap={theme.spacing(6)}>
<Breadcrumbs
list={[
{ name: "pagespeed", path: "/pagespeed" },
@@ -110,13 +110,49 @@ const CreatePageSpeed = () => {
noValidate
spellCheck="false"
onSubmit={handleCreate}
gap={theme.gap.large}
gap={theme.spacing(12)}
flex={1}
>
<Typography component="h1">Create pagespeed monitor</Typography>
<Stack gap={theme.gap.xl}>
<Typography
component="h1"
lineHeight={1}
fontSize={21}
color={theme.palette.text.primary}
>
<Typography
component="span"
fontSize="inherit"
color={theme.palette.text.secondary}
>
Create your{" "}
</Typography>
<Typography component="span" fontSize="inherit" fontWeight="inherit">
pagespeed{" "}
</Typography>
<Typography
component="span"
fontSize="inherit"
color={theme.palette.text.secondary}
>
monitor
</Typography>
</Typography>
<Stack
border={1}
borderColor={theme.palette.border.light}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.background.main}
p={theme.spacing(20)}
pl={theme.spacing(15)}
gap={theme.spacing(20)}
sx={{
"& h3, & p": {
color: theme.palette.text.secondary,
},
}}
>
<Stack direction="row">
<Typography component="h3">Monitor friendly name</Typography>
<Typography component="h3">Monitor display name</Typography>
<Field
type="text"
id="monitor-name"
@@ -151,8 +187,11 @@ const CreatePageSpeed = () => {
Incidents notifications{" "}
<Typography component="span">(coming soon)</Typography>
</Typography>
<Stack className="section-disabled">
<Typography mb={theme.gap.small}>
<Stack
className="section-disabled"
backgroundColor={theme.palette.background.fill}
>
<Typography mb={theme.spacing(4)}>
When there is a new incident,
</Typography>
<Checkbox
@@ -171,7 +210,7 @@ const CreatePageSpeed = () => {
label="Notify via email to following emails"
isChecked={false}
/>
<Box mx={`calc(${theme.gap.ml} * 2)`}>
<Box mx={theme.spacing(16)}>
<Field
id="notify-emails-list"
placeholder="notifications@gmail.com"
@@ -179,7 +218,7 @@ const CreatePageSpeed = () => {
onChange={() => logger.warn("disabled")}
error=""
/>
<Typography mt={theme.gap.small}>
<Typography mt={theme.spacing(4)}>
You can separate multiple emails with a comma
</Typography>
</Box>
@@ -189,11 +228,13 @@ const CreatePageSpeed = () => {
<Stack direction="row" justifyContent="flex-end" mt="auto">
<Button
type="submit"
level="primary"
label="Create"
variant="contained"
color="primary"
onClick={handleCreate}
sx={{ px: theme.gap.large, mt: theme.gap.large }}
/>
sx={{ px: theme.spacing(12), mt: theme.spacing(12) }}
>
Create
</Button>
</Stack>
</Stack>
</Stack>
@@ -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;
}
+135 -61
View File
@@ -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 }) => {
<Stack
className="stat-box"
direction="row"
gap={theme.gap.small}
pt={theme.gap.ml}
px={theme.gap.ml}
pb={theme.gap.mlplus}
gap={theme.spacing(4)}
pt={theme.spacing(8)}
px={theme.spacing(8)}
pb={theme.spacing(10)}
border={1}
borderColor={theme.palette.border.light}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.background.main}
minWidth="200px"
>
{icon}
<Box>
<Typography variant="h6" mb={theme.gap.medium}>
<Typography
variant="h6"
color={theme.palette.primary.main}
mb={theme.spacing(6)}
>
{title}
</Typography>
<Typography variant="h4">{value}</Typography>
<Typography variant="h4" color={theme.palette.text.secondary}>
{value}
</Typography>
</Box>
</Stack>
);
@@ -151,7 +161,7 @@ const SkeletonLayout = () => {
return (
<>
<Skeleton variant="rounded" width="15%" height={34} />
<Stack direction="row" gap={theme.gap.small}>
<Stack direction="row" gap={theme.spacing(4)}>
<Skeleton variant="circular" style={{ minWidth: 24, minHeight: 24 }} />
<Box width="85%">
<Skeleton variant="rounded" width="50%" height={24} />
@@ -159,7 +169,7 @@ const SkeletonLayout = () => {
variant="rounded"
width="50%"
height={18}
sx={{ mt: theme.gap.small }}
sx={{ mt: theme.spacing(4) }}
/>
</Box>
<Skeleton
@@ -172,7 +182,7 @@ const SkeletonLayout = () => {
<Stack
direction="row"
justifyContent="space-between"
gap={theme.gap.xl}
gap={theme.spacing(20)}
flexWrap="wrap"
>
<Skeleton variant="rounded" width="30%" height={90} sx={{ flex: 1 }} />
@@ -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 (
<Stack className="page-speed-details" gap={theme.gap.large}>
<Stack className="page-speed-details" gap={theme.spacing(12)}>
{loading ? (
<SkeletonLayout />
) : (
@@ -324,54 +368,58 @@ const PageSpeedDetails = () => {
{ name: "details", path: `/pagespeed/${monitorId}` },
]}
/>
<Stack direction="row" gap={theme.gap.xs}>
<Stack direction="row" gap={theme.spacing(2)}>
<PulseDot
color={
monitor?.status
? theme.label.up.dotColor
: theme.label.down.dotColor
? theme.palette.success.main
: theme.palette.error.main
}
/>
<Box>
<Typography
component="h1"
mb={theme.gap.xs}
mb={theme.spacing(2)}
color={theme.palette.text.primary}
sx={{ lineHeight: 1 }}
>
{monitor?.url}
</Typography>
<Typography
component="span"
sx={{ color: "var(--env-var-color-17)" }}
color={
monitor?.status
? theme.palette.success.main
: theme.palette.error.text
}
>
Your pagespeed monitor is live.
</Typography>
</Box>
<Button
level="tertiary"
label="Configure"
animate="rotate90"
img={
<SettingsIcon
style={{ width: theme.gap.mlplus, height: theme.gap.mlplus }}
/>
}
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,
},
},
}}
/>
>
<SettingsIcon />
Configure
</Button>
</Stack>
<Stack
direction="row"
justifyContent="space-between"
gap={theme.gap.large}
gap={theme.spacing(12)}
flexWrap="wrap"
>
<StatBox
@@ -412,19 +460,39 @@ const PageSpeedDetails = () => {
value={formatDurationRounded(monitor?.interval)}
></StatBox>
</Stack>
<Typography component="h2">Score history</Typography>
<Box height="300px">
<Typography component="h2" color={theme.palette.text.secondary}>
Score history
</Typography>
<Box
height="300px"
sx={{
...sharedStyles,
}}
>
<PageSpeedLineChart pageSpeedChecks={data} />
</Box>
<Typography component="h2">Performance report</Typography>
<Stack direction="row" alignItems="center" overflow="hidden" flex={1}>
<Typography component="h2" color={theme.palette.text.secondary}>
Performance report
</Typography>
<Stack
direction="row"
alignItems="center"
overflow="hidden"
flex={1}
sx={{
"& p": {
color: theme.palette.text.secondary,
},
...sharedStyles,
}}
>
<Stack
alignItems="center"
textAlign="center"
minWidth="300px"
flex={1}
px={theme.gap.xl}
py={theme.gap.ml}
px={theme.spacing(20)}
py={theme.spacing(8)}
>
<Box onMouseLeave={() => setExpand(false)}>
{expand ? (
@@ -514,7 +582,7 @@ const PageSpeedDetails = () => {
</PieChart>
)}
</Box>
<Typography mt={theme.gap.medium}>
<Typography mt={theme.spacing(6)}>
Values are estimated and may vary.{" "}
<Typography
component="span"
@@ -530,21 +598,23 @@ const PageSpeedDetails = () => {
</Typography>
</Stack>
<Box
px={theme.gap.xl}
py={theme.gap.ml}
px={theme.spacing(20)}
py={theme.spacing(8)}
height="100%"
flex={1}
sx={{
borderLeft: `solid 1px ${theme.palette.otherColors.graishWhite}`,
borderLeft: 1,
borderLeftColor: theme.palette.border.light,
}}
>
<Typography
mb={theme.gap.medium}
pb={theme.gap.ml}
color={theme.palette.secondary.main}
mb={theme.spacing(6)}
pb={theme.spacing(8)}
color={theme.palette.text.secondary}
textAlign="center"
sx={{
borderBottom: `solid 1px ${theme.palette.otherColors.graishWhite}`,
borderBottom: 1,
borderBottomColor: theme.palette.border.light,
borderBottomStyle: "dashed",
}}
>
@@ -569,8 +639,8 @@ const PageSpeedDetails = () => {
<Stack
direction="row"
flexWrap="wrap"
pt={theme.gap.ml}
gap={theme.gap.ml}
pt={theme.spacing(8)}
gap={theme.spacing(8)}
>
{Object.keys(audits).map((key) => {
if (key === "_id") return;
@@ -581,8 +651,8 @@ const PageSpeedDetails = () => {
let shape = (
<Box
sx={{
width: theme.gap.medium,
height: theme.gap.medium,
width: theme.spacing(6),
height: theme.spacing(6),
borderRadius: "50%",
backgroundColor: metricParams.stroke,
}}
@@ -592,8 +662,8 @@ const PageSpeedDetails = () => {
shape = (
<Box
sx={{
width: theme.gap.medium,
height: theme.gap.medium,
width: theme.spacing(6),
height: theme.spacing(6),
backgroundColor: metricParams.stroke,
}}
></Box>
@@ -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
}`,
}}
></Box>
);
@@ -630,7 +704,7 @@ const PageSpeedDetails = () => {
className="metric"
key={`${key}-box`}
direction="row"
gap={theme.gap.small}
gap={theme.spacing(4)}
>
{shape}
<Box>
@@ -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}
+3 -19
View File
@@ -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);
}
+84 -40
View File
@@ -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,
},
}}
>
<Stack
direction="row"
gap={theme.gap.medium}
p={theme.gap.ml}
gap={theme.spacing(6)}
p={theme.spacing(8)}
onClick={() => 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 },
}}
>
<PageSpeedIcon style={{ width: theme.gap.ml, height: theme.gap.ml }} />
<PageSpeedIcon
style={{ width: theme.spacing(8), height: theme.spacing(8) }}
/>
<Box flex={1}>
<Stack direction="row" justifyContent="space-between">
<Typography component="h2" mb={theme.gap.xs}>
<Typography
component="h2"
mb={theme.spacing(2)}
color={theme.palette.primary.main}
>
{data.name}
</Typography>
<StatusLabel
@@ -42,8 +60,10 @@ const Card = ({ data }) => {
text={data.status ? "Live (collecting data)" : "Inactive"}
/>
</Stack>
<Typography>{data.url.replace(/^https?:\/\//, "")}</Typography>
<Typography mt={theme.gap.large}>
<Typography fontSize={13}>
{data.url.replace(/^https?:\/\//, "")}
</Typography>
<Typography mt={theme.spacing(12)}>
<Typography component="span" fontWeight={600}>
Last checked:{" "}
</Typography>
@@ -71,11 +91,11 @@ const SkeletonLayout = () => {
const theme = useTheme();
return (
<Stack gap={theme.gap.xs}>
<Stack gap={theme.spacing(2)}>
<Stack
direction="row"
justifyContent="space-between"
mb={theme.gap.large}
mb={theme.spacing(12)}
>
<Box width="80%">
<Skeleton variant="rounded" width="25%" height={24} />
@@ -83,7 +103,7 @@ const SkeletonLayout = () => {
variant="rounded"
width="50%"
height={19.5}
sx={{ mt: theme.gap.xs }}
sx={{ mt: theme.spacing(2) }}
/>
</Box>
<Skeleton
@@ -93,7 +113,7 @@ const SkeletonLayout = () => {
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
<Stack direction="row" flexWrap="wrap" gap={theme.gap.large}>
<Stack direction="row" flexWrap="wrap" gap={theme.spacing(12)}>
<Skeleton
variant="rounded"
width="100%"
@@ -123,17 +143,17 @@ const SkeletonLayout = () => {
);
};
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 (
<Box className="page-speed" pt={theme.gap.xl}>
<Box
className="page-speed"
sx={{
':has(> [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 ? (
<SkeletonLayout />
) : monitors?.length !== 0 ? (
<Stack gap={theme.gap.xs}>
<Stack
direction="row"
justifyContent="space-between"
mb={theme.gap.large}
>
<Box>
<Typography component="h1">All page speed monitors</Typography>
<Typography mt={theme.gap.xs}>
Click on one of the monitors to get more site speed information.
</Typography>
</Box>
<Button
level="primary"
label="Create new"
onClick={() => navigate("/pagespeed/create")}
/>
</Stack>
<Grid container spacing={theme.gap.large}>
<Box
sx={{
"& p": {
color: theme.palette.text.secondary,
},
}}
>
<Box mb={theme.spacing(12)}>
<Breadcrumbs list={[{ name: `pagespeed`, path: "/pagespeed" }]} />
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
mt={theme.spacing(5)}
>
<Greeting type="pagespeed" />
<Button
variant="contained"
color="primary"
onClick={() => navigate("/pagespeed/create")}
>
Create new
</Button>
</Stack>
</Box>
<Grid container spacing={theme.spacing(12)}>
{monitors?.map((monitor) => (
<Card data={monitor} key={`monitor-${monitor._id}`} />
))}
</Grid>
</Stack>
</Box>
) : (
<Fallback
title="page speed"
title="pagespeed monitor"
checks={[
"Report on the user experience of a page",
"Help analyze webpage speed",
"Give suggestions on how the page can be improved",
]}
link="/pagespeed/create"
isAdmin={isAdmin}
/>
)}
</Box>
);
};
PageSpeed.propTypes = {
isAdmin: PropTypes.bool,
};
export default PageSpeed;
Loaded 100 of 138 files, more files were not shown because too many files have changed in this diff. Show more