Merge branch 'develop' into fix/axios-refactor

This commit is contained in:
Alexander Holliday authored and GitHub committed 2024-08-19 12:57:12 -07:00
commit 4304b7ae55
28 files changed
+876 -445

No files matched your search

@@ -0,0 +1,9 @@
## Describe your changes
## Issue Number if applicable
## Checklist before requesting a review
- [ ] I have performed a self-review of my code
- [ ] I have inlcuded the issue # in the PR if applicable
- [ ] I have labelled the PR correctly
@@ -0,0 +1,31 @@
.MuiBreadcrumbs-root {
height: 36px;
}
.MuiBreadcrumbs-root svg {
width: 16px;
height: 16px;
}
.MuiBreadcrumbs-root .MuiBreadcrumbs-li a {
font-size: var(--env-var-font-size-medium);
color: var(--env-var-color-2);
opacity: 0.8;
font-weight: 400;
}
.MuiBreadcrumbs-root .MuiBreadcrumbs-li:hover a {
color: var(--env-var-color-2);
}
.MuiBreadcrumbs-root .MuiBreadcrumbs-li:not(:last-child):hover a {
opacity: 1;
background-color: #e3e3e3;
}
.MuiBreadcrumbs-root .MuiBreadcrumbs-li:not(:last-child) {
cursor: pointer;
}
.MuiBreadcrumbs-root .MuiBreadcrumbs-li:last-child a {
font-weight: 500;
opacity: 1;
cursor: default;
}
.MuiBreadcrumbs-root .MuiBreadcrumbs-separator {
margin: 0;
}
@@ -0,0 +1,63 @@
import PropTypes from "prop-types";
import { Box, Breadcrumbs as MUIBreadcrumbs } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router";
import ArrowRight from "../../assets/icons/right-arrow.svg?react";
import "./index.css";
/**
* Breadcrumbs component that displays a list of breadcrumb items.
*
* @param {Object} props
* @param {Array} props.list - Array of breadcrumb items. Each item should have `name` and `path` properties.
* @param {string} props.list.name - The name to display for the breadcrumb.
* @param {string} props.list.path - The path to navigate to when the breadcrumb is clicked.
*
* @returns {JSX.Element} The rendered Breadcrumbs component.
*/
const Breadcrumbs = ({ list }) => {
const theme = useTheme();
const navigate = useNavigate();
return (
<MUIBreadcrumbs
separator={<ArrowRight />}
aria-label="breadcrumb"
p={theme.gap.small}
width="fit-content"
backgroundColor={theme.palette.otherColors.fillGray}
borderRadius={`${theme.shape.borderRadius}px`}
lineHeight="18px"
>
{list.map((item, index) => {
return (
<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`}
onClick={() => navigate(item.path)}
>
{item.name}
</Box>
);
})}
</MUIBreadcrumbs>
);
};
Breadcrumbs.propTypes = {
list: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
}).isRequired
).isRequired,
};
export default Breadcrumbs;
+45 -4
View File
@@ -1,4 +1,5 @@
aside .selected-path,
.sidebar-popup .selected-path,
aside .MuiListItemButton-root:hover {
background-color: var(--env-var-color-13);
}
@@ -52,7 +53,7 @@ aside
> .MuiListItemButton-root::before {
content: "";
position: absolute;
left: -8px;
left: -7px;
top: 0;
height: 100%;
width: 100%;
@@ -65,7 +66,8 @@ aside
> .MuiListItemButton-root:last-child::before {
height: 50%;
}
aside .MuiCollapse-wrapperInner .MuiList-root svg {
aside .MuiCollapse-wrapperInner .MuiList-root svg,
aside .MuiList-root .MuiListItemText-root + svg {
width: 18px;
height: 18px;
}
@@ -73,11 +75,50 @@ aside .MuiCollapse-wrapperInner .MuiList-root svg {
aside .MuiCollapse-wrapperInner .MuiList-root > .MuiListItemButton-root::after {
content: "";
position: absolute;
left: -8px;
left: -7px;
top: 50%;
height: 1px;
width: 7px;
width: 6px;
border-radius: 50%;
background-color: var(--env-var-color-5);
opacity: 0.3;
}
aside {
transition: flex 300ms ease-out;
}
.sidebar-popup .MuiPaper-root {
box-shadow: var(--env-var-shadow-1);
border: solid 1px var(--env-var-color-6);
border-radius: var(--env-var-radius-1);
gap: 1px;
}
.sidebar-popup .MuiList-root {
padding: 6px 6px;
min-width: 100px;
}
.sidebar-popup li.MuiButtonBase-root {
font-size: var(--env-var-font-size-medium);
color: var(--env-var-color-5);
padding: 5px 8px;
border-radius: var(--env-var-radius-1);
}
.sidebar-popup li.MuiButtonBase-root:hover{
background-color: var(--env-var-color-13);
}
.sidebar-popup li.MuiButtonBase-root:has(.MuiBox-root):hover{
background-color: white;
}
.sidebar-popup li.MuiButtonBase-root:has(.MuiBox-root){
padding-bottom: 0;
}
.sidebar-popup .MuiModal-root p.MuiTypography-root {
font-size: var(--env-var-font-size-medium);
color: var(--env-var-color-2);
}
.sidebar-popup svg{
width: 16px;
height: 16px;
opacity: 0.9;
}
+302 -53
View File
@@ -9,6 +9,8 @@ import {
ListItemIcon,
ListItemText,
ListSubheader,
Menu,
MenuItem,
Stack,
Tooltip,
Typography,
@@ -24,6 +26,7 @@ import UserSvg from "../../assets/icons/user.svg?react";
import TeamSvg from "../../assets/icons/user-two.svg?react";
import LogoutSvg from "../../assets/icons/logout.svg?react";
import BWULogo from "../../assets/Images/bwl-logo.svg?react";
import BWULogoAbbreviated from "../../assets/icons/bwu-abbreviated.svg?react";
import Support from "../../assets/icons/support.svg?react";
import Dashboard from "../../assets/icons/dashboard.svg?react";
import Account from "../../assets/icons/user-edit.svg?react";
@@ -36,6 +39,8 @@ import PageSpeed from "../../assets/icons/page-speed.svg?react";
import Settings from "../../assets/icons/settings.svg?react";
import ArrowDown from "../../assets/icons/down-arrow.svg?react";
import ArrowUp from "../../assets/icons/up-arrow.svg?react";
import ArrowRight from "../../assets/icons/right-arrow.svg?react";
import ArrowLeft from "../../assets/icons/left-arrow.svg?react";
import "./index.css";
@@ -82,6 +87,17 @@ function Sidebar() {
const dispatch = useDispatch();
const authState = useSelector((state) => state.auth);
const [open, setOpen] = useState({ Dashboard: false, Account: false });
const [collapsed, setCollapsed] = useState(false);
const [anchorEl, setAnchorEl] = useState(null);
const [popup, setPopup] = useState();
const openPopup = (event, id) => {
setAnchorEl(event.currentTarget);
setPopup(id);
};
const closePopup = () => {
setAnchorEl(null);
};
/**
* Handles logging out the user
@@ -105,10 +121,47 @@ function Sidebar() {
}, []);
return (
<Stack component="aside" gap={theme.gap.medium}>
<Box py={theme.gap.large} pl={theme.gap.lgplus}>
<BWULogo alt="BlueWave Uptime Logo" />
</Box>
<Stack
component="aside"
gap={theme.gap.medium}
sx={{ flex: collapsed ? 0 : 1 }}
>
<Stack
alignItems={collapsed ? "center" : "flex-start"}
py={theme.gap.large}
pl={collapsed ? 0 : theme.gap.lgplus}
>
{collapsed ? (
<BWULogoAbbreviated alt="BlueWave Uptime Logo" />
) : (
<BWULogo alt="BlueWave Uptime Logo" />
)}
<IconButton
sx={{
position: "absolute",
right: 0,
transform: `translate(50%, 50%)`,
backgroundColor: theme.palette.otherColors.fillGray,
border: `solid 1px ${theme.palette.otherColors.graishWhite}`,
p: "5px",
"& svg": {
width: theme.gap.ml,
height: theme.gap.ml,
"& path": {
stroke: theme.palette.otherColors.bluishGray,
},
},
"&:focus": { outline: "none" },
"&:hover": {
backgroundColor: "#e3e3e3",
borderColor: theme.palette.otherColors.graishWhite,
},
}}
onClick={() => setCollapsed(!collapsed)}
>
{collapsed ? <ArrowRight /> : <ArrowLeft />}
</IconButton>
</Stack>
{/* menu */}
<List
component="nav"
@@ -118,28 +171,132 @@ function Sidebar() {
<ListSubheader
component="div"
id="nested-menu-subheader"
sx={{ pt: theme.gap.small }}
sx={{
pt: theme.gap.small,
px: collapsed ? theme.gap.xs : theme.gap.ml,
textAlign: collapsed ? "center" : "left",
}}
>
Menu
</ListSubheader>
}
sx={{ px: collapsed ? theme.gap.xs : theme.gap.ml }}
>
{menu.map((item) =>
item.path ? (
<ListItemButton
className={
location.pathname.includes(item.path) ? "selected-path" : ""
}
<Tooltip
key={item.path}
onClick={() => navigate(`/${item.path}`)}
sx={{
gap: theme.gap.medium,
borderRadius: `${theme.shape.borderRadius}px`,
placement="right"
title={collapsed ? item.name : ""}
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: {
offset: [0, -16],
},
},
],
},
}}
>
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon}</ListItemIcon>
<ListItemText>{item.name}</ListItemText>
</ListItemButton>
<ListItemButton
className={
location.pathname.includes(item.path) ? "selected-path" : ""
}
onClick={() => navigate(`/${item.path}`)}
sx={{
gap: theme.gap.medium,
borderRadius: `${theme.shape.borderRadius}px`,
justifyContent: collapsed ? "center" : "flex-start",
}}
>
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon}</ListItemIcon>
{!collapsed && <ListItemText>{item.name}</ListItemText>}
</ListItemButton>
</Tooltip>
) : collapsed ? (
<React.Fragment key={item.name}>
<Tooltip
placement="right"
title={collapsed ? item.name : ""}
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: {
offset: [0, -16],
},
},
],
},
}}
>
<ListItemButton
className={
Boolean(anchorEl) && popup === item.name
? "selected-path"
: ""
}
onClick={(event) => openPopup(event, item.name)}
sx={{
position: "relative",
gap: theme.gap.medium,
borderRadius: `${theme.shape.borderRadius}px`,
justifyContent: collapsed ? "center" : "flex-start",
}}
>
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon}</ListItemIcon>
{!collapsed && <ListItemText>{item.name}</ListItemText>}
</ListItemButton>
</Tooltip>
<Menu
className="sidebar-popup"
anchorEl={anchorEl}
open={Boolean(anchorEl) && popup === item.name}
onClose={closePopup}
anchorOrigin={{
vertical: "top",
horizontal: "right",
}}
sx={{ ml: theme.gap.small }}
>
{item.nested.map((child) => {
if (
child.name === "Team" &&
authState.user?.role &&
!authState.user.role.includes("admin")
) {
return null;
}
return (
<MenuItem
className={
location.pathname.includes(child.path)
? "selected-path"
: ""
}
key={child.path}
onClick={() => {
navigate(`/${child.path}`);
closePopup();
}}
sx={{
gap: theme.gap.small,
borderRadius: `${theme.shape.borderRadius}px`,
pl: theme.gap.small,
}}
>
{child.icon}
{child.name}
</MenuItem>
);
})}
</Menu>
</React.Fragment>
) : (
<React.Fragment key={item.name}>
<ListItemButton
@@ -152,6 +309,7 @@ function Sidebar() {
sx={{
gap: theme.gap.medium,
borderRadius: `${theme.shape.borderRadius}px`,
justifyContent: collapsed ? "center" : "flex-start",
}}
>
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon}</ListItemIcon>
@@ -210,63 +368,154 @@ function Sidebar() {
<ListSubheader
component="div"
id="nested-other-subheader"
sx={{ pt: theme.gap.small }}
sx={{
pt: theme.gap.small,
px: collapsed ? theme.gap.xs : theme.gap.ml,
textAlign: collapsed ? "center" : "left",
}}
>
Other
</ListSubheader>
}
sx={{ px: collapsed ? theme.gap.xs : theme.gap.ml }}
>
{other.map((item) => (
<ListItemButton
className={
location.pathname.includes(item.path) ? "selected-path" : ""
}
<Tooltip
key={item.path}
onClick={() =>
item.path === "support"
? window.open(
"https://github.com/bluewave-labs/bluewave-uptime",
"_blank",
"noreferrer"
)
: navigate(`/${item.path}`)
}
sx={{
gap: theme.gap.medium,
borderRadius: `${theme.shape.borderRadius}px`,
placement="right"
title={collapsed ? item.name : ""}
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: {
offset: [0, -16],
},
},
],
},
}}
>
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon}</ListItemIcon>
<ListItemText>{item.name}</ListItemText>
</ListItemButton>
<ListItemButton
className={
location.pathname.includes(item.path) ? "selected-path" : ""
}
onClick={() =>
item.path === "support"
? window.open(
"https://github.com/bluewave-labs/bluewave-uptime",
"_blank",
"noreferrer"
)
: navigate(`/${item.path}`)
}
sx={{
gap: theme.gap.medium,
borderRadius: `${theme.shape.borderRadius}px`,
justifyContent: collapsed ? "center" : "flex-start",
}}
>
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon}</ListItemIcon>
{!collapsed && <ListItemText>{item.name}</ListItemText>}
</ListItemButton>
</Tooltip>
))}
</List>
<Divider sx={{ mt: "auto" }} />
<Stack
direction="row"
height="50px"
justifyContent={collapsed ? "center" : "flex-start"}
alignItems="center"
py={theme.gap.small}
px={theme.gap.medium}
gap={theme.gap.xs}
borderRadius={`${theme.shape.borderRadius}px`}
>
<Avatar small={true} />
<Box ml={theme.gap.xs}>
<Typography component="span" fontWeight={500}>
{authState.user?.firstName} {authState.user?.lastName}
</Typography>
<Typography sx={{ textTransform: "capitalize" }}>
{authState.user?.role}
</Typography>
</Box>
<Tooltip title="Log Out">
<IconButton
sx={{ ml: "auto", "&:focus": { outline: "none" } }}
onClick={logout}
>
<LogoutSvg style={{ width: "20px", height: "20px" }} />
</IconButton>
</Tooltip>
{collapsed ? (
<>
<Tooltip
title="Options"
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: {
offset: [0, -10],
},
},
],
},
}}
>
<IconButton
onClick={(event) => openPopup(event, "logout")}
sx={{ p: 0, "&:focus": { outline: "none" } }}
>
<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}>
<Typography component="span" fontWeight={500}>
{authState.user?.firstName} {authState.user?.lastName}
</Typography>
<Typography sx={{ textTransform: "capitalize" }}>
{authState.user?.role}
</Typography>
</Box>
<Tooltip title="Log Out">
<IconButton
sx={{ ml: "auto", "&:focus": { outline: "none" } }}
onClick={logout}
>
<LogoutSvg style={{ width: "20px", height: "20px" }} />
</IconButton>
</Tooltip>
</>
)}
</Stack>
</Stack>
);
-5
View File
@@ -19,7 +19,6 @@
height: calc(100vh - var(--env-var-spacing-2) * 2);
max-width: var(--env-var-side-bar-width);
flex: 1;
border: 1px solid var(--color-border-0);
border-radius: var(--env-var-radius-1);
@@ -27,10 +26,6 @@
padding: var(--env-var-spacing-1) 0;
}
.home-layout aside > *:not(:first-child) {
padding-left: var(--env-var-spacing-1-plus);
padding-right: var(--env-var-spacing-1-plus);
}
.home-layout > div {
min-height: calc(100vh - var(--env-var-spacing-2) * 2);
+30 -2
View File
@@ -4,7 +4,7 @@ import { Box, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { credentials } from "../../Validation/validation";
import { login } from "../../Features/Auth/authSlice";
import { useDispatch } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { createToast } from "../../Utils/toastUtils";
import Button from "../../Components/Button";
import { networkService } from "../../main";
@@ -13,6 +13,7 @@ import background from "../../assets/Images/background_pattern_decorative.png";
import Logo from "../../assets/icons/bwu-icon.svg?react";
import Mail from "../../assets/icons/mail.svg?react";
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
import PropTypes from "prop-types";
import "./index.css";
@@ -63,6 +64,10 @@ const LandingPage = ({ onContinue }) => {
);
};
LandingPage.propTypes = {
onContinue: PropTypes.func.isRequired,
};
/**
* Renders the first step of the login process.
*
@@ -136,6 +141,14 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
);
};
StepOne.propTypes = {
form: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired,
onSubmit: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onBack: PropTypes.func.isRequired,
};
/**
* Renders the second step of the login process, including a password input field.
*
@@ -230,11 +243,22 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
);
};
StepTwo.propTypes = {
form: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired,
onSubmit: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onBack: PropTypes.func.isRequired,
};
const Login = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const theme = useTheme();
const authState = useSelector((state) => state.auth);
const { authToken } = authState;
const idMap = {
"login-email-input": "email",
"login-password-input": "password",
@@ -248,6 +272,10 @@ const Login = () => {
const [step, setStep] = useState(0);
useEffect(() => {
if (authToken) {
navigate("/monitors");
return;
}
networkService
.doesAdminExist()
.then((response) => {
@@ -258,7 +286,7 @@ const Login = () => {
.catch((error) => {
console.log(error);
});
}, [navigate]);
}, [authToken, navigate]);
const handleChange = (event) => {
const { value, id } = event.target;
@@ -2,7 +2,7 @@ import { Box, Stack, Typography } from "@mui/material";
import "./index.css";
import React, { useState } from "react";
import Button from "../../../Components/Button";
import Back from "../../../assets/icons/left-arrow.svg?react";
import Back from "../../../assets/icons/left-arrow-long.svg?react";
import Select from "../../../Components/Inputs/Select";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
+9 -15
View File
@@ -23,8 +23,7 @@
opacity: 0.6;
}
.configure-monitor h6.MuiTypography-root,
.configure-monitor p.MuiTypography-root:has(span),
.configure-monitor p.MuiTypography-root span.MuiTypography-root {
.configure-monitor h1.MuiTypography-root + span.MuiTypography-root {
font-size: var(--env-var-font-size-small-plus);
}
.configure-monitor button.MuiButtonBase-root {
@@ -33,19 +32,20 @@
.configure-monitor button.MuiButtonBase-root {
height: var(--env-var-height-2);
}
.configure-monitor button.MuiButtonBase-root:has(svg) {
line-height: 1;
}
.configure-monitor .config-box {
padding: var(--env-var-spacing-4) 50px;
padding-bottom: 60px;
border: 1px solid var(--env-var-color-16);
border-radius: var(--env-var-radius-1);
background-color: var(--env-var-color-8);
}
.configure-monitor .config-box .MuiBox-root,
.configure-monitor .config-box .MuiStack-root {
.configure-monitor .config-box > .MuiBox-root,
.configure-monitor .config-box > .MuiStack-root {
flex: 1;
padding: 24px 30px;
padding-bottom: 40px;
}
.configure-monitor .config-box > *:first-child {
flex: 0.7;
border-right: solid 1px var(--env-var-color-16);
}
.configure-monitor .MuiStack-root:has(span.MuiTypography-root.input-error) {
position: relative;
@@ -72,12 +72,6 @@
border-color: var(--env-var-color-29);
}
.configure-monitor-form {
display: flex;
flex-direction: column;
gap: var(--env-var-spacing-2);
}
body:has(.configure-monitor) .select-dropdown .MuiMenuItem-root,
.configure-monitor .select-wrapper .select-component > .MuiSelect-select {
text-transform: lowercase;
+36 -62
View File
@@ -5,15 +5,11 @@ import { useEffect, useState } from "react";
import Button from "../../../Components/Button";
import Field from "../../../Components/Inputs/Field";
import { Box, Modal, Skeleton, Stack, Typography } from "@mui/material";
import WestRoundedIcon from "@mui/icons-material/WestRounded";
import GreenCheck from "../../../assets/icons/checkbox-green.svg?react";
import RedCheck from "../../../assets/icons/checkbox-red.svg?react";
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
import { getLastChecked } from "../../../Utils/monitorUtils";
import "./index.css";
import { monitorValidation } from "../../../Validation/validation";
import Select from "../../../Components/Inputs/Select";
import { formatDurationRounded } from "../../../Utils/timeUtils";
import { createToast } from "../../../Utils/toastUtils";
import {
updateUptimeMonitor,
@@ -21,6 +17,8 @@ import {
deleteUptimeMonitor,
} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import Checkbox from "../../../Components/Inputs/Checkbox";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import "./index.css";
/**
* Parses a URL string and returns a URL object.
@@ -96,7 +94,6 @@ const Configure = () => {
const { user, authToken } = useSelector((state) => state.auth);
const { monitors } = useSelector((state) => state.uptimeMonitors);
const [monitor, setMonitor] = useState({});
const [duration, setDuration] = useState(0);
const [errors, setErrors] = useState({});
const { monitorId } = useParams();
@@ -117,7 +114,6 @@ const Configure = () => {
setMonitor({
...data,
});
setDuration(formatDurationRounded(data?.interval));
}, [monitorId, authToken, monitors]);
const handleChange = (event, name) => {
@@ -210,74 +206,54 @@ const Configure = () => {
const parsedUrl = parseUrl(monitor?.url);
const protocol = parsedUrl?.protocol?.replace(":", "") || "";
var lastChecked = formatDurationRounded(getLastChecked(monitor?.checks));
let loading = Object.keys(monitor).length === 0;
return (
<Box className="configure-monitor">
<Stack className="configure-monitor" gap={theme.gap.large}>
{loading ? (
<SkeletonLayout />
) : (
<>
<Button
level="tertiary"
label="Back"
animate="slideLeft"
img={<WestRoundedIcon />}
onClick={() => navigate(-1)}
sx={{
backgroundColor: "#f4f4f4",
px: theme.gap.ml,
"& svg.MuiSvgIcon-root": {
mr: theme.gap.small,
fill: theme.palette.otherColors.slateGray,
},
}}
<Breadcrumbs
list={[
{ name: "monitors", path: "/monitors" },
{ name: "details", path: `/monitors/${monitorId}` },
{ name: "configure", path: `/monitors/configure/${monitorId}` },
]}
/>
<form
className="configure-monitor-form"
<Stack
component="form"
noValidate
spellCheck="false"
style={{
marginTop: theme.gap.medium,
}}
gap={theme.gap.large}
flex={1}
>
<Stack direction="row" gap={theme.gap.small} mt={theme.gap.small}>
<Stack direction="row" gap={theme.gap.small}>
{monitor?.status ? <GreenCheck /> : <RedCheck />}
<Box>
{parsedUrl?.host ? (
<Typography
component="h1"
mb={theme.gap.small}
sx={{ lineHeight: 1 }}
>
<Typography component="h1" mb={theme.gap.xs} lineHeight={1}>
{parsedUrl.host || "..."}
</Typography>
) : (
""
)}
<Typography lineHeight={parsedUrl?.host ? 1 : theme.gap.large}>
<Typography
component="span"
sx={{
color: monitor?.status
? "var(--env-var-color-17)"
: "var(--env-var-color-24)",
}}
>
Your site is {monitor?.status ? "up" : "down"}.
</Typography>{" "}
Checking every {duration}.{" "}
{lastChecked ? `Last time checked ${lastChecked} ago.` : ""}
<Typography
component="span"
lineHeight={theme.gap.large}
sx={{
color: monitor?.status
? "var(--env-var-color-17)"
: "var(--env-var-color-24)",
}}
>
Your site is {monitor?.status ? "up" : "down"}.
</Typography>
</Box>
<Stack
direction="row"
gap={theme.gap.medium}
<Box
sx={{
ml: "auto",
alignSelf: "flex-end",
ml: "auto",
}}
>
<Button
@@ -286,9 +262,10 @@ const Configure = () => {
animate="rotate180"
img={<PauseCircleOutlineIcon />}
sx={{
backgroundColor: "#f4f4f4",
backgroundColor: theme.palette.otherColors.fillGray,
pl: theme.gap.small,
pr: theme.gap.medium,
mr: theme.gap.medium,
"& svg": {
mr: theme.gap.xs,
},
@@ -303,17 +280,16 @@ const Configure = () => {
}}
onClick={() => setIsOpen(true)}
/>
</Stack>
</Box>
</Stack>
<Stack
className="config-box"
direction="row"
justifyContent="space-between"
gap={theme.gap.xxl}
>
<Box>
<Typography component="h2">General settings</Typography>
<Typography component="p" sx={{ mt: theme.gap.small }}>
<Typography component="p" sx={{ mt: theme.gap.xs }}>
Here you can select the URL of the host, together with the
type of monitor.
</Typography>
@@ -345,11 +321,10 @@ const Configure = () => {
className="config-box"
direction="row"
justifyContent="space-between"
gap={theme.gap.xxl}
>
<Box>
<Typography component="h2">Incident notifications</Typography>
<Typography component="p" mt={theme.gap.small}>
<Typography component="p" mt={theme.gap.xs}>
When there is an incident, notify users.
</Typography>
</Box>
@@ -408,7 +383,6 @@ const Configure = () => {
className="config-box"
direction="row"
justifyContent="space-between"
gap={theme.gap.xxl}
>
<Box>
<Typography component="h2">Advanced settings</Typography>
@@ -423,7 +397,7 @@ const Configure = () => {
/>
</Stack>
</Stack>
<Stack direction="row" justifyContent="flex-end">
<Stack direction="row" justifyContent="flex-end" mt="auto">
<Button
level="primary"
label="Save"
@@ -431,7 +405,7 @@ const Configure = () => {
onClick={handleSubmit}
/>
</Stack>
</form>
</Stack>
</>
)}
<Modal
@@ -460,7 +434,7 @@ const Configure = () => {
}}
>
<Typography id="modal-delete-monitor" component="h2">
Do you really want to delete this monitor?
Do you really want to delete this monitor?
</Typography>
<Typography id="delete-monitor-confirmation">
Once deleted, this monitor cannot be retrieved.
@@ -480,7 +454,7 @@ const Configure = () => {
</Stack>
</Stack>
</Modal>
</Box>
</Stack>
);
};
@@ -13,7 +13,9 @@
.create-monitor button.MuiButtonBase-root {
font-size: var(--env-var-font-size-medium);
}
.create-monitor h2.MuiTypography-root,
.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);
@@ -43,23 +45,20 @@
top: 100%;
}
.create-monitor .config-box {
padding: var(--env-var-spacing-4) 50px;
padding-bottom: 60px;
flex-direction: row;
border: 1px solid var(--env-var-color-16);
border-radius: var(--env-var-radius-1);
flex-direction: row;
gap: var(--env-var-spacing-4);
background-color: var(--env-var-color-8);
}
.create-monitor .config-box .MuiBox-root,
.create-monitor .config-box .MuiStack-root {
.create-monitor .config-box > .MuiBox-root,
.create-monitor .config-box > .MuiStack-root {
flex: 1;
padding: 24px 30px;
padding-bottom: 40px;
}
.create-monitor-form {
display: flex;
flex-direction: column;
gap: var(--env-var-spacing-2);
.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 {
@@ -8,11 +8,11 @@ import { monitorValidation } from "../../../Validation/validation";
import { createUptimeMonitor } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import { useNavigate } from "react-router-dom";
import { useTheme } from "@emotion/react";
import WestRoundedIcon from "@mui/icons-material/WestRounded";
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";
const CreateMonitor = () => {
const MS_PER_MINUTE = 60000;
@@ -143,33 +143,26 @@ const CreateMonitor = () => {
return (
<Box className="create-monitor">
<Button
level="tertiary"
label="Back"
animate="slideLeft"
img={<WestRoundedIcon />}
onClick={() => navigate("/monitors")}
sx={{
backgroundColor: theme.palette.otherColors.fillGray,
mb: theme.gap.medium,
px: theme.gap.ml,
"& svg.MuiSvgIcon-root": {
mr: theme.gap.small,
fill: theme.palette.otherColors.slateGray,
},
}}
<Breadcrumbs
list={[
{ name: "monitors", path: "/monitors" },
{ name: "create", path: `/monitors/create` },
]}
/>
<form
<Stack
component="form"
className="create-monitor-form"
onSubmit={handleCreateMonitor}
noValidate
spellCheck="false"
gap={theme.gap.large}
mt={theme.gap.mlplus}
>
<Typography component="h1">Create new monitor</Typography>
<Stack className="config-box">
<Box>
<Typography component="h2">General settings</Typography>
<Typography component="p" mt={theme.gap.small}>
<Typography component="p" mt={theme.gap.xs}>
Here you can select the URL of the host, together with the type of
monitor.
</Typography>
@@ -200,7 +193,7 @@ const CreateMonitor = () => {
<Stack className="config-box">
<Box>
<Typography component="h2">Checks to perform</Typography>
<Typography component="p" mt={theme.gap.small}>
<Typography component="p" mt={theme.gap.xs}>
You can always add or remove checks after adding your site.
</Typography>
</Box>
@@ -249,28 +242,26 @@ const CreateMonitor = () => {
checked={monitor.type === "ping"}
onChange={(event) => handleChange(event)}
/>
<Box className="error-container">
{errors["type"] ? (
{errors["type"] ? (
<Box className="error-container">
<Typography component="p" className="input-error">
{errors["type"]}
</Typography>
) : (
""
)}
</Box>
</Box>
) : (
""
)}
</Stack>
</Stack>
<Stack className="config-box">
<Box>
<Typography component="h2">Incident notifications</Typography>
<Typography component="p" mt={theme.gap.small}>
<Typography component="p" mt={theme.gap.xs}>
When there is an incident, notify users.
</Typography>
</Box>
<Stack gap={theme.gap.medium}>
<Typography component="p" mt={theme.gap.small}>
When there is a new incident,
</Typography>
<Typography component="p">When there is a new incident,</Typography>
<Checkbox
id="notify-sms"
label="Notify via SMS (coming soon)"
@@ -404,7 +395,7 @@ const CreateMonitor = () => {
disabled={Object.keys(errors).length !== 0 && true}
/>
</Stack>
</form>
</Stack>
</Box>
);
};
+11 -31
View File
@@ -7,7 +7,6 @@ import { networkService } from "../../../main";
import MonitorDetailsAreaChart from "../../../Components/Charts/MonitorDetailsAreaChart";
import ButtonGroup from "@mui/material/ButtonGroup";
import Button from "../../../Components/Button";
import WestRoundedIcon from "@mui/icons-material/WestRounded";
import GreenCheck from "../../../assets/icons/checkbox-green.svg?react";
import RedCheck from "../../../assets/icons/checkbox-red.svg?react";
import SettingsIcon from "../../../assets/icons/settings-bold.svg?react";
@@ -18,6 +17,7 @@ import {
} from "../../../Utils/timeUtils";
import "./index.css";
import MonitorDetails60MinChart from "../../../Components/Charts/MonitorDetails60MinChart";
import Breadcrumbs from "../../../Components/Breadcrumbs";
const StatBox = ({ title, value }) => {
return (
@@ -161,39 +161,19 @@ const DetailsPage = () => {
<SkeletonLayout />
) : (
<>
<Button
level="tertiary"
label="Back"
animate="slideLeft"
img={<WestRoundedIcon />}
onClick={() => navigate("/monitors")}
sx={{
backgroundColor: theme.palette.otherColors.fillGray,
px: theme.gap.ml,
"& svg.MuiSvgIcon-root": {
mr: theme.gap.small,
fill: theme.palette.otherColors.slateGray,
},
}}
<Breadcrumbs
list={[
{ name: "monitors", path: "/monitors" },
{ name: "details", path: `/monitors/${monitorId}` },
]}
/>
<Stack gap={theme.gap.xl} mt={theme.gap.medium}>
<Stack
direction="row"
gap={theme.gap.small}
mt={theme.gap.small}
alignItems="baseline"
>
<Stack gap={theme.gap.large} mt={theme.gap.large}>
<Stack direction="row" gap={theme.gap.small}>
{monitor?.status ? <GreenCheck /> : <RedCheck />}
<Box>
<Stack direction="row">
<Typography
component="h1"
sx={{ lineHeight: 1, alignSelf: "flex-end" }}
>
{monitor.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>
<MonitorDetails60MinChart data={monitor.statusBar} />
</Stack>
<Typography component="h1" sx={{ lineHeight: 1 }}>
{monitor.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>
<Typography mt={theme.gap.small}>
<Typography
component="span"
+188 -128
View File
@@ -11,7 +11,6 @@ import ServerStatus from "../../Components/Charts/Servers/ServerStatus";
import { useTheme } from "@emotion/react";
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
import OpenInNewPage from "../../assets/icons/open-in-new-page.svg?react";
import Settings from "../../assets/icons/settings-bold.svg?react";
import BasicTable from "../../Components/BasicTable";
import { StatusLabel } from "../../Components/Label";
import { createToast } from "../../Utils/toastUtils";
@@ -27,6 +26,181 @@ import {
Typography,
} from "@mui/material";
import Settings from "../../assets/icons/settings-bold.svg?react";
import PropTypes from "prop-types";
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={() => 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={() => 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.
@@ -72,6 +246,15 @@ const Host = ({ params }) => {
);
};
Host.propTypes = {
params: PropTypes.shape({
url: PropTypes.string,
title: PropTypes.string,
percentageColor: PropTypes.string,
percentage: PropTypes.number,
}).isRequired,
};
/**
* Renders a skeleton layout.
*
@@ -111,37 +294,6 @@ const Monitors = () => {
const authState = useSelector((state) => state.auth);
const dispatch = useDispatch({});
const [anchorEl, setAnchorEl] = useState(null);
const [actions, setActions] = useState({});
const openMenu = (event, id, url) => {
event.preventDefault();
setAnchorEl(event.currentTarget);
setActions({ id: id, url: url });
};
const closeMenu = () => {
setAnchorEl(null);
};
const [isOpen, setIsOpen] = useState(false);
const openRemove = () => {
closeMenu();
setIsOpen(true);
};
const handleRemove = async (event) => {
event.preventDefault();
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." });
}
};
useEffect(() => {
dispatch(getUptimeMonitorsByUserId(authState.authToken));
}, [authState.authToken, dispatch]);
@@ -191,7 +343,9 @@ const Monitors = () => {
return {
id: monitor._id,
// disabled for now
handleClick: () => navigate(`/monitors/${monitor._id}`),
handleClick: () => {
navigate(`/monitors/${monitor._id}`);
},
data: [
{ id: idx, data: <Host params={params} /> },
{
@@ -213,28 +367,7 @@ const Monitors = () => {
},
{
id: idx + 4,
data: (
<>
<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>
</>
),
data: <ActionsMenu monitor={monitor} />,
},
],
};
@@ -327,79 +460,6 @@ const Monitors = () => {
)}
</>
)}
<Menu
className="actions-menu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={closeMenu}
>
{actions.url !== null ? (
<MenuItem
onClick={() => {
window.open(actions.url, "_blank", "noreferrer");
}}
>
Open site
</MenuItem>
) : (
""
)}
<MenuItem onClick={() => navigate(`/monitors/${actions.id}`)}>
Details
</MenuItem>
{/* TODO - pass monitor id to Incidents page */}
<MenuItem disabled>Incidents</MenuItem>
<MenuItem onClick={() => navigate(`/monitors/configure/${actions.id}`)}>
Configure
</MenuItem>
<MenuItem onClick={openRemove}>Remove</MenuItem>
</Menu>
<Modal
aria-labelledby="modal-delete-monitor"
aria-describedby="delete-monitor-confirmation"
open={isOpen}
onClose={() => 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={() => setIsOpen(false)}
/>
<Button level="error" label="Delete" onClick={handleRemove} />
</Stack>
</Stack>
</Modal>
</Stack>
);
};
@@ -2,7 +2,7 @@
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-default-2);
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);
@@ -60,9 +60,12 @@ body:has(.configure-pagespeed) .select-dropdown .MuiMenuItem-root,
text-transform: lowercase;
}
.configure-pagespeed h3.MuiTypography-root {
flex: 0.7;
}
.configure-pagespeed .field,
.configure-pagespeed .section-disabled,
.configure-pagespeed .select-wrapper,
.configure-pagespeed h3.MuiTypography-root {
.configure-pagespeed .select-wrapper {
flex: 1;
}
+15 -27
View File
@@ -15,9 +15,9 @@ import Field from "../../../Components/Inputs/Field";
import Select from "../../../Components/Inputs/Select";
import Checkbox from "../../../Components/Inputs/Checkbox";
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
import WestRoundedIcon from "@mui/icons-material/WestRounded";
import GreenCheck from "../../../assets/icons/checkbox-green.svg?react";
import RedCheck from "../../../assets/icons/checkbox-red.svg?react";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import "./index.css";
@@ -147,37 +147,25 @@ const PageSpeedConfigure = () => {
let loading = Object.keys(monitor).length === 0;
return (
<Box className="configure-pagespeed">
<Stack className="configure-pagespeed" gap={theme.gap.large}>
{loading ? (
<SkeletonLayout />
) : (
<>
<Button
level="tertiary"
label="Back"
animate="slideLeft"
img={<WestRoundedIcon />}
onClick={() => navigate(-1)}
sx={{
backgroundColor: theme.palette.otherColors.fillGray,
mb: theme.gap.large,
px: theme.gap.ml,
"& svg.MuiSvgIcon-root": {
mr: theme.gap.small,
fill: theme.palette.otherColors.slateGray,
},
}}
<Breadcrumbs
list={[
{ name: "pagespeed", path: "/pagespeed" },
{ name: "details", path: `/pagespeed/${monitorId}` },
{ name: "configure", path: `/pagespeed/configure/${monitorId}` },
]}
/>
<form
<Stack
component="form"
noValidate
spellCheck="false"
onSubmit={handleSave}
style={{
display: "flex",
flexDirection: "column",
gap: theme.gap.large,
maxWidth: "1000px",
}}
flex={1}
gap={theme.gap.large}
>
<Stack direction="row" gap={theme.gap.small}>
{monitor?.status ? <GreenCheck /> : <RedCheck />}
@@ -300,7 +288,7 @@ const PageSpeedConfigure = () => {
</Stack>
</Stack>
</Stack>
<Stack direction="row" justifyContent="flex-end">
<Stack direction="row" justifyContent="flex-end" mt="auto">
<Button
type="submit"
level="primary"
@@ -309,7 +297,7 @@ const PageSpeedConfigure = () => {
sx={{ px: theme.gap.large, mt: theme.gap.large }}
/>
</Stack>
</form>
</Stack>
</>
)}
<Modal
@@ -358,7 +346,7 @@ const PageSpeedConfigure = () => {
</Stack>
</Stack>
</Modal>
</Box>
</Stack>
);
};
@@ -2,10 +2,10 @@
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-default-2);
padding: var(--env-var-font-size-large-plus) var(--env-var-font-size-xlarge);
}
.create-page-speed h1.MuiTypography-root {
font-size: var(--env-var-font-size-large);
font-size: 22px;
color: var(--env-var-color-1);
}
.create-page-speed p.MuiTypography-root,
@@ -50,9 +50,12 @@ body:has(.create-page-speed) .select-dropdown .MuiMenuItem-root,
text-transform: lowercase;
}
.create-page-speed h3.MuiTypography-root {
flex: 0.7;
}
.create-page-speed .field,
.create-page-speed .section-disabled,
.create-page-speed .select-wrapper,
.create-page-speed h3.MuiTypography-root {
.create-page-speed .select-wrapper {
flex: 1;
}
@@ -10,7 +10,7 @@ import Checkbox from "../../../Components/Inputs/Checkbox";
import { monitorValidation } from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
import { createPageSpeed } from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
import WestRoundedIcon from "@mui/icons-material/WestRounded";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import "./index.css";
const CreatePageSpeed = () => {
@@ -97,34 +97,22 @@ const CreatePageSpeed = () => {
};
return (
<Box className="create-page-speed">
<Button
level="tertiary"
label="Back"
animate="slideLeft"
img={<WestRoundedIcon />}
onClick={() => navigate("/pagespeed")}
sx={{
backgroundColor: theme.palette.otherColors.fillGray,
mb: theme.gap.medium,
px: theme.gap.ml,
"& svg.MuiSvgIcon-root": {
mr: theme.gap.small,
fill: theme.palette.otherColors.slateGray,
},
}}
<Stack className="create-page-speed" gap={theme.gap.mlplus}>
<Breadcrumbs
list={[
{ name: "pagespeed", path: "/pagespeed" },
{ name: "create", path: `/pagespeed/create` },
]}
/>
<form
<Stack
component="form"
noValidate
spellCheck="false"
onSubmit={handleCreate}
style={{
display: "flex",
flexDirection: "column",
gap: theme.gap.large,
}}
gap={theme.gap.large}
flex={1}
>
<Typography component="h1">Create a page speed monitor</Typography>
<Typography component="h1">Create pagespeed monitor</Typography>
<Stack gap={theme.gap.xl}>
<Stack direction="row">
<Typography component="h3">Monitor friendly name</Typography>
@@ -197,7 +185,7 @@ const CreatePageSpeed = () => {
</Stack>
</Stack>
</Stack>
<Stack direction="row" justifyContent="flex-end">
<Stack direction="row" justifyContent="flex-end" mt="auto">
<Button
type="submit"
level="primary"
@@ -206,8 +194,8 @@ const CreatePageSpeed = () => {
sx={{ px: theme.gap.large, mt: theme.gap.large }}
/>
</Stack>
</form>
</Box>
</Stack>
</Stack>
);
};
+1 -1
View File
@@ -37,7 +37,7 @@
.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-2);
border-radius: var(--env-var-radius-1);
background-color: var(--env-var-color-8);
min-width: 200px;
}
+38 -32
View File
@@ -11,26 +11,17 @@ import {
} from "../../../Utils/timeUtils";
import { networkService } from "../../../main";
import Button from "../../../Components/Button";
import WestRoundedIcon from "@mui/icons-material/WestRounded";
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";
import IntervalCheckIcon from "../../../assets/icons/interval-check.svg?react";
import GreenCheck from "../../../assets/icons/checkbox-green.svg?react";
import RedCheck from "../../../assets/icons/checkbox-red.svg?react";
import "./index.css";
import PageSpeedLineChart from "../../../Components/Charts/PagespeedLineChart";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import "./index.css";
import PropTypes from "prop-types";
/**
* Displays a box with an icon, title, and value.
*
* @param {Object} props
* @param {ReactNode} props.icon - The icon to display in the box.
* @param {string} props.title - The title text to display above the value.
* @param {string | number} props.value - The value text to display in the box.
* @returns {JSX.Element}
*/
const StatBox = ({ icon, title, value }) => {
const theme = useTheme();
@@ -39,7 +30,8 @@ const StatBox = ({ icon, title, value }) => {
className="stat-box"
direction="row"
gap={theme.gap.small}
p={theme.gap.ml}
pt={theme.gap.ml}
px={theme.gap.ml}
pb={theme.gap.mlplus}
>
{icon}
@@ -53,6 +45,12 @@ const StatBox = ({ icon, title, value }) => {
);
};
StatBox.propTypes = {
icon: PropTypes.element,
title: PropTypes.string,
value: PropTypes.node,
};
/**
* Renders a centered label within a pie chart.
*
@@ -85,6 +83,12 @@ const PieCenterLabel = ({ value, color, setExpand }) => {
);
};
PieCenterLabel.propTypes = {
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
color: PropTypes.string,
setExpand: PropTypes.func,
};
/**
* A component that renders a label on a pie chart slice.
* The label is positioned relative to the center of the pie chart and is optionally highlighted.
@@ -128,6 +132,14 @@ const PieValueLabel = ({ value, startAngle, endAngle, color, highlighted }) => {
);
};
PieValueLabel.propTypes = {
value: PropTypes.number,
startAngle: PropTypes.number,
endAngle: PropTypes.number,
color: PropTypes.string,
highlighted: PropTypes.bool,
};
/**
* Renders a skeleton layout.
*
@@ -195,16 +207,16 @@ const PageSpeedDetails = () => {
null,
null
);
setMonitor(res.data.data);
setAudits(res.data.data.checks[0].audits);
setMonitor(res?.data?.data ?? {});
setAudits(res?.data?.data?.checks?.[0]?.audits ?? []);
} catch (error) {
console.error("Error fetching pagespeed monitor of id: " + monitorId);
console.log(error);
navigate("/not-found");
}
};
fetchMonitor();
}, [monitorId]);
}, [monitorId, authToken, navigate]);
/**
* Weight constants for different performance metrics.
@@ -306,21 +318,11 @@ const PageSpeedDetails = () => {
<SkeletonLayout />
) : (
<>
<Button
level="tertiary"
label="Back"
animate="slideLeft"
img={<WestRoundedIcon />}
onClick={() => navigate("/pagespeed")}
sx={{
width: "fit-content",
backgroundColor: theme.palette.otherColors.fillGray,
px: theme.gap.ml,
"& svg.MuiSvgIcon-root": {
mr: theme.gap.small,
fill: theme.palette.otherColors.slateGray,
},
}}
<Breadcrumbs
list={[
{ name: "pagespeed", path: "/pagespeed" },
{ name: "details", path: `/pagespeed/${monitorId}` },
]}
/>
<Stack direction="row" gap={theme.gap.small}>
{monitor?.status ? <GreenCheck /> : <RedCheck />}
@@ -662,4 +664,8 @@ const PageSpeedDetails = () => {
);
};
PageSpeedDetails.propTypes = {
push: PropTypes.func,
};
export default PageSpeedDetails;
@@ -0,0 +1,4 @@
<svg width="26" height="16" viewBox="0 0 26 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.761364 15V0.454545H5.84659C6.85985 0.454545 7.69555 0.629735 8.35369 0.980114C9.01184 1.32576 9.50189 1.79214 9.82386 2.37926C10.1458 2.96165 10.3068 3.60795 10.3068 4.31818C10.3068 4.94318 10.1955 5.45928 9.97301 5.86648C9.75521 6.27367 9.46638 6.59564 9.10653 6.83239C8.75142 7.06913 8.36553 7.24432 7.94886 7.35795V7.5C8.39394 7.52841 8.84138 7.68466 9.29119 7.96875C9.741 8.25284 10.1174 8.66004 10.4205 9.19034C10.7235 9.72064 10.875 10.3693 10.875 11.1364C10.875 11.8655 10.7093 12.5213 10.3778 13.1037C10.0464 13.6861 9.5232 14.1477 8.80824 14.4886C8.09328 14.8295 7.16288 15 6.01705 15H0.761364ZM2.52273 13.4375H6.01705C7.16761 13.4375 7.98438 13.215 8.46733 12.7699C8.95502 12.3201 9.19886 11.7756 9.19886 11.1364C9.19886 10.6439 9.07339 10.1894 8.82244 9.77273C8.5715 9.35133 8.21402 9.01515 7.75 8.7642C7.28598 8.50852 6.73674 8.38068 6.10227 8.38068H2.52273V13.4375ZM2.52273 6.84659H5.78977C6.32008 6.84659 6.7983 6.74242 7.22443 6.53409C7.6553 6.32576 7.99621 6.0322 8.24716 5.65341C8.50284 5.27462 8.63068 4.82955 8.63068 4.31818C8.63068 3.67898 8.40814 3.13684 7.96307 2.69176C7.51799 2.24195 6.8125 2.01705 5.84659 2.01705H2.52273V6.84659Z" fill="#344054"/>
<path d="M23.3146 0.454545H25.076V10.0852C25.076 11.0795 24.8416 11.9673 24.3729 12.7486C23.9089 13.5251 23.2531 14.1383 22.4055 14.5881C21.558 15.0331 20.5637 15.2557 19.4226 15.2557C18.2815 15.2557 17.2872 15.0331 16.4396 14.5881C15.5921 14.1383 14.9339 13.5251 14.4652 12.7486C14.0012 11.9673 13.7692 11.0795 13.7692 10.0852V0.454545H15.5305V9.94318C15.5305 10.6534 15.6868 11.2855 15.9993 11.8395C16.3118 12.3887 16.7569 12.822 17.3345 13.1392C17.9169 13.4517 18.6129 13.608 19.4226 13.608C20.2322 13.608 20.9283 13.4517 21.5107 13.1392C22.093 12.822 22.5381 12.3887 22.8459 11.8395C23.1584 11.2855 23.3146 10.6534 23.3146 9.94318V0.454545Z" fill="#1570EF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,3 @@
<svg width="18" height="14" viewBox="0 0 18 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 7H1M1 7L7 13M1 7L7 1" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 223 B

+2 -2
View File
@@ -1,3 +1,3 @@
<svg width="18" height="14" viewBox="0 0 18 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 7H1M1 7L7 13M1 7L7 1" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 18L9 12L15 6" stroke="#667085" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 223 B

After

Width:  |  Height:  |  Size: 215 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 18L15 12L9 6" stroke="#667085" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 214 B

+5 -1
View File
@@ -67,7 +67,7 @@
--env-var-width-2: 360px;
--env-var-height-1: 100vh;
--env-var-height-2: 34px;
--env-var-height-2: 36px;
--env-var-nav-bar-height: 70px;
--env-var-side-bar-width: 250px;
@@ -141,6 +141,10 @@ button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.MuiInputBase-root.Mui-disabled input {
cursor: not-allowed;
}
.Toastify__toast-container {
width: auto;
}
+8 -4
View File
@@ -1,4 +1,4 @@
FROM node:20-alpine
FROM node:20-alpine as build
WORKDIR /app
@@ -10,8 +10,12 @@ COPY ./Client .
RUN npm run build
RUN npm install -g serve
# RUN npm install -g serve
CMD ["serve","-s", "dist", "-l", "5173"]
# CMD ["serve","-s", "dist", "-l", "5173"]
EXPOSE 5000
FROM nginx:1.27.1-alpine
COPY --from=build /app/dist /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"]
+1 -1
View File
@@ -3,7 +3,7 @@ services:
client:
image: uptime_client:latest
ports:
- "80:5173"
- "80:80"
depends_on:
- server
server:
+11
View File
@@ -22,6 +22,7 @@ BlueWave Uptime is an open source server monitoring application used to track th
- [x] Ping monitoring
- [x] Incidents at a glance
- [x] Page speed monitoring
- [x] E-mail notifications
- [ ] Scheduled maintenance (in the works)
**Roadmap (short term):**
@@ -59,6 +60,16 @@ Made with [contrib.rocks](https://contrib.rocks).
![Alt](https://repobeats.axiom.co/api/embed/c35d999c82dbb31e967427ea4166c14da4172e73.svg "Repobeats analytics image")
[![Star History Chart](https://api.star-history.com/svg?repos=bluewave-labs/bluewave-uptime&type=Date)](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
Also check other developer and contributor-friendly projects of BlueWave:
- [BlueWave HRM](https://github.com/bluewave-labs/bluewave-hrm)
- [BlueWave Onboarding](https://github.com/bluewave-labs/bluewave-onboarding)
- [BlueWave DataRoom](https://github.com/bluewave-labs/bluewave-dataroom)
- [BlueWave ChatFabrica](https://github.com/bluewave-labs/bluewave-chatfabrica)
## Getting Started
- Clone this repository to your local machine