Merge branch 'develop' into fix/axios-refactor
This commit is contained in:
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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
@@ -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,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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;"]
|
||||
@@ -3,7 +3,7 @@ services:
|
||||
client:
|
||||
image: uptime_client:latest
|
||||
ports:
|
||||
- "80:5173"
|
||||
- "80:80"
|
||||
depends_on:
|
||||
- server
|
||||
server:
|
||||
|
||||
@@ -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).
|
||||
|
||||

|
||||
|
||||
[](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
|
||||
|
||||
Reference in new issue
Block a user