138 files changed
+4847
-3746
No files matched your search
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 +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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 @@
|
||||
|
||||
+41
-16
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
@@ -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%;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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'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" }}
|
||||
>
|
||||
|
||||
@@ -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'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
@@ -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't have an account? —
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
ml={theme.gap.xs}
|
||||
color={theme.palette.primary.main}
|
||||
ml={theme.spacing(2)}
|
||||
onClick={() => {
|
||||
navigate("/register");
|
||||
}}
|
||||
|
||||
@@ -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" }}
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
@@ -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" }}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
},
|
||||
}));
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 +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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
Reference in new issue
Block a user