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
|
||||
@@ -10,7 +10,6 @@
|
||||
}
|
||||
.MuiTable-root .host div:nth-child(2) {
|
||||
width: fit-content;
|
||||
font-weight: 700;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
@@ -125,3 +124,68 @@
|
||||
.MuiPaper-root + .MuiPagination-root div.MuiPaginationItem-root {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.MuiTablePagination-root p {
|
||||
color: var(--env-var-color-2);
|
||||
font-weight: 500;
|
||||
}
|
||||
.MuiTablePagination-root button,
|
||||
.MuiTablePagination-root .MuiSelect-select {
|
||||
border: solid 1px #ebebeb;
|
||||
border-radius: var(--env-var-radius-1);
|
||||
}
|
||||
.MuiTablePagination-root .MuiTablePagination-select.MuiSelect-select {
|
||||
text-align: left;
|
||||
text-align-last: left;
|
||||
}
|
||||
.MuiTablePagination-root button {
|
||||
min-width: 0;
|
||||
padding: 4px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.MuiTablePagination-root svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
.MuiTablePagination-root svg path {
|
||||
stroke: var(--env-var-color-2);
|
||||
stroke-width: 1.3;
|
||||
}
|
||||
.MuiTablePagination-root .MuiSelect-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: 50%;
|
||||
right: 8%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.MuiTablePagination-root button.Mui-disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
.MuiTablePagination-root button:not(.Mui-disabled):hover {
|
||||
background-color: #f4f4f4;
|
||||
border-color: #f4f4f4;
|
||||
}
|
||||
|
||||
.pagination-dropdown.MuiPaper-root {
|
||||
box-shadow: var(--env-var-shadow-1);
|
||||
border: solid 1px var(--env-var-color-6);
|
||||
border-radius: var(--env-var-radius-1);
|
||||
}
|
||||
.pagination-dropdown ul {
|
||||
padding: 4px;
|
||||
min-width: 100px;
|
||||
}
|
||||
.pagination-dropdown li,
|
||||
body:has(.pagination-dropdown) p:has(+ .MuiTablePagination-root) {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
color: var(--env-var-color-2);
|
||||
padding: 4px;
|
||||
}
|
||||
.pagination-dropdown li.Mui-selected,
|
||||
.pagination-dropdown li.Mui-selected.Mui-focusVisible,
|
||||
.pagination-dropdown li.Mui-selected:hover {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
body:has(.pagination-dropdown) p:has(+ .MuiTablePagination-root) {
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import {
|
||||
TableContainer,
|
||||
Paper,
|
||||
@@ -6,14 +9,86 @@ import {
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
Pagination,
|
||||
PaginationItem,
|
||||
TablePagination,
|
||||
Box,
|
||||
Typography,
|
||||
Stack,
|
||||
} from "@mui/material";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useLocation, useParams } from "react-router-dom";
|
||||
import { setRowsPerPage } from "../../Features/UI/uiSlice";
|
||||
import LeftArrowDouble from "../../assets/icons/left-arrow-double.svg?react";
|
||||
import RightArrowDouble from "../../assets/icons/right-arrow-double.svg?react";
|
||||
import LeftArrow from "../../assets/icons/left-arrow.svg?react";
|
||||
import RightArrow from "../../assets/icons/right-arrow.svg?react";
|
||||
import SelectorVertical from "../../assets/icons/selector-vertical.svg?react";
|
||||
import Button from "../Button";
|
||||
import "./index.css";
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded";
|
||||
/**
|
||||
* Component for pagination actions (first, previous, next, last).
|
||||
*
|
||||
* @component
|
||||
* @param {Object} props
|
||||
* @param {number} props.count - Total number of items.
|
||||
* @param {number} props.page - Current page number.
|
||||
* @param {number} props.rowsPerPage - Number of rows per page.
|
||||
* @param {function} props.onPageChange - Callback function to handle page change.
|
||||
*
|
||||
* @returns {JSX.Element} Pagination actions component.
|
||||
*/
|
||||
const TablePaginationActions = (props) => {
|
||||
const { count, page, rowsPerPage, onPageChange } = props;
|
||||
|
||||
const handleFirstPageButtonClick = (event) => {
|
||||
onPageChange(event, 0);
|
||||
};
|
||||
const handleBackButtonClick = (event) => {
|
||||
onPageChange(event, page - 1);
|
||||
};
|
||||
const handleNextButtonClick = (event) => {
|
||||
onPageChange(event, page + 1);
|
||||
};
|
||||
const handleLastPageButtonClick = (event) => {
|
||||
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ flexShrink: 0, ml: "24px" }}>
|
||||
<Button
|
||||
level="secondary"
|
||||
label=""
|
||||
img={<LeftArrowDouble />}
|
||||
onClick={handleFirstPageButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="first page"
|
||||
/>
|
||||
<Button
|
||||
level="secondary"
|
||||
label=""
|
||||
img={<LeftArrow />}
|
||||
onClick={handleBackButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="previous page"
|
||||
/>
|
||||
<Button
|
||||
level="secondary"
|
||||
label=""
|
||||
img={<RightArrow />}
|
||||
onClick={handleNextButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="next page"
|
||||
/>
|
||||
<Button
|
||||
level="secondary"
|
||||
label=""
|
||||
img={<RightArrowDouble />}
|
||||
onClick={handleLastPageButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="last page"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* BasicTable Component
|
||||
@@ -68,9 +143,13 @@ import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded";
|
||||
* <BasicTable data={data} rows={rows} paginated={true} />
|
||||
*/
|
||||
|
||||
const BasicTable = ({ data, paginated, reversed, rowsPerPage = 5 }) => {
|
||||
// Add headers to props validation
|
||||
|
||||
const BasicTable = ({ data, paginated, reversed }) => {
|
||||
const theme = useTheme();
|
||||
const location = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const uiState = useSelector((state) => state.ui);
|
||||
let table = location.pathname.split("/").pop();
|
||||
let rowsPerPage = uiState[table].rowsPerPage;
|
||||
const [page, setPage] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -81,6 +160,16 @@ const BasicTable = ({ data, paginated, reversed, rowsPerPage = 5 }) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
dispatch(
|
||||
setRowsPerPage({
|
||||
value: parseInt(event.target.value, 10),
|
||||
table: table,
|
||||
})
|
||||
);
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
let displayData = [];
|
||||
|
||||
if (data && data.rows) {
|
||||
@@ -94,35 +183,15 @@ const BasicTable = ({ data, paginated, reversed, rowsPerPage = 5 }) => {
|
||||
return <div>No data</div>;
|
||||
}
|
||||
|
||||
let paginationComponent = <></>;
|
||||
if (paginated === true && data.rows.length > rowsPerPage) {
|
||||
paginationComponent = (
|
||||
<Pagination
|
||||
count={Math.ceil(data.rows.length / rowsPerPage)}
|
||||
page={page + 1}
|
||||
onChange={(event, value) => handleChangePage(event, value - 1)}
|
||||
shape="rounded"
|
||||
renderItem={(item) => (
|
||||
<PaginationItem
|
||||
slots={{
|
||||
previous: ArrowBackRoundedIcon,
|
||||
next: ArrowForwardRoundedIcon,
|
||||
}}
|
||||
{...item}
|
||||
sx={{
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"& .MuiTouchRipple-root": {
|
||||
pointerEvents: "none",
|
||||
display: "none",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Helper function to calculate the range of displayed rows.
|
||||
* @returns {string}
|
||||
*/
|
||||
const getRange = () => {
|
||||
let start = page * rowsPerPage + 1;
|
||||
let end = Math.min(page * rowsPerPage + rowsPerPage, data.rows.length);
|
||||
return `${start} - ${end}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -154,8 +223,59 @@ const BasicTable = ({ data, paginated, reversed, rowsPerPage = 5 }) => {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{paginationComponent}
|
||||
{paginated && (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Typography sx={{ opacity: 0.7 }}>
|
||||
Showing {getRange()} of {data.rows.length} monitor(s)
|
||||
</Typography>
|
||||
<TablePagination
|
||||
component="div"
|
||||
count={data.rows.length}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
rowsPerPage={rowsPerPage}
|
||||
rowsPerPageOptions={[5, 10, 15, 25]}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
ActionsComponent={TablePaginationActions}
|
||||
labelRowsPerPage="Rows per page"
|
||||
labelDisplayedRows={({ page, count }) =>
|
||||
`Page ${page + 1} of ${Math.max(
|
||||
0,
|
||||
Math.ceil(count / rowsPerPage)
|
||||
)}`
|
||||
}
|
||||
slotProps={{
|
||||
select: {
|
||||
MenuProps: {
|
||||
keepMounted: true,
|
||||
PaperProps: {
|
||||
className: "pagination-dropdown",
|
||||
},
|
||||
transformOrigin: { vertical: "bottom", horizontal: "left" },
|
||||
anchorOrigin: { vertical: "top", horizontal: "left" },
|
||||
sx: { mt: "-4px" },
|
||||
},
|
||||
inputProps: { id: "pagination-dropdown" },
|
||||
IconComponent: SelectorVertical,
|
||||
sx: {
|
||||
ml: theme.gap.small,
|
||||
mr: theme.gap.large,
|
||||
minWidth: theme.gap.xl,
|
||||
textAlign: "left",
|
||||
"&.Mui-focused > div": {
|
||||
backgroundColor: theme.palette.otherColors.white,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={{ mt: theme.gap.medium }}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -22,9 +22,10 @@
|
||||
|
||||
.server-status-tile-title {
|
||||
font-size: var(--font-size-0);
|
||||
font-weight: bold;
|
||||
font-weight: 500;
|
||||
margin-bottom: calc(var(--spacing-general-1) / 2);
|
||||
color: var(--env-var-color-1);
|
||||
color: var(--env-var-color-5);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.server-status-tile-value {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
color: var(--env-var-color-5);
|
||||
}
|
||||
.select-wrapper h3.MuiTypography-root{
|
||||
.select-wrapper h3.MuiTypography-root {
|
||||
font-weight: 500;
|
||||
}
|
||||
.MuiInputBase-root:has(> .MuiSelect-select) fieldset {
|
||||
@@ -24,3 +24,23 @@
|
||||
.MuiInputBase-root:not(.Mui-focused):has(> .MuiSelect-select):hover fieldset {
|
||||
border-color: var(--env-var-color-29);
|
||||
}
|
||||
|
||||
.select-dropdown.MuiPaper-root {
|
||||
box-shadow: var(--env-var-shadow-1);
|
||||
border: solid 1px var(--env-var-color-6);
|
||||
border-radius: var(--env-var-radius-1);
|
||||
}
|
||||
.select-dropdown .MuiList-root {
|
||||
min-width: 100px;
|
||||
}
|
||||
.select-dropdown li {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
color: var(--env-var-color-5);
|
||||
padding: 5px 8px;
|
||||
border-radius: var(--env-var-radius-1);
|
||||
}
|
||||
.select-dropdown li.Mui-selected,
|
||||
.select-dropdown li.Mui-selected.Mui-focusVisible,
|
||||
.select-dropdown li.Mui-selected:hover {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
aside .selected-path,
|
||||
.sidebar-popup .selected-path,
|
||||
aside .MuiListItemButton-root:hover {
|
||||
background-color: var(--env-var-color-13);
|
||||
}
|
||||
@@ -52,12 +53,12 @@ aside
|
||||
> .MuiListItemButton-root::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
left: -7px;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-left: solid 1px var(--env-var-color-5);
|
||||
opacity: 0.3;
|
||||
opacity: 0.2;
|
||||
}
|
||||
aside
|
||||
.MuiCollapse-wrapperInner
|
||||
@@ -65,15 +66,59 @@ aside
|
||||
> .MuiListItemButton-root:last-child::before {
|
||||
height: 50%;
|
||||
}
|
||||
aside .MuiCollapse-wrapperInner .MuiList-root svg,
|
||||
aside .MuiList-root .MuiListItemText-root + svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
aside .MuiCollapse-wrapperInner .MuiList-root > .MuiListItemButton-root::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: 50%;
|
||||
height: 1px;
|
||||
width: 7px;
|
||||
transform: translateY(-50%);
|
||||
height: 3px;
|
||||
width: 3px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--env-var-color-5);
|
||||
opacity: 0.3;
|
||||
background-color: #d6d9dd;
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -17,6 +19,7 @@ import { useLocation, useNavigate } from "react-router";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { clearAuthState } from "../../Features/Auth/authSlice";
|
||||
import { toggleSidebar } from "../../Features/UI/uiSlice";
|
||||
import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import Avatar from "../Avatar";
|
||||
import LockSvg from "../../assets/icons/lock.svg?react";
|
||||
@@ -24,6 +27,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 +40,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";
|
||||
|
||||
@@ -81,7 +87,19 @@ function Sidebar() {
|
||||
const location = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const { sidebar } = useSelector((state) => state.ui);
|
||||
let collapsed = sidebar.collapsed;
|
||||
const [open, setOpen] = useState({ Dashboard: false, Account: 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 +123,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={() => dispatch(toggleSidebar())}
|
||||
>
|
||||
{collapsed ? <ArrowRight /> : <ArrowLeft />}
|
||||
</IconButton>
|
||||
</Stack>
|
||||
{/* menu */}
|
||||
<List
|
||||
component="nav"
|
||||
@@ -118,28 +173,134 @@ 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],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
disableInteractive
|
||||
>
|
||||
<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],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
disableInteractive
|
||||
>
|
||||
<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 +313,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>
|
||||
@@ -183,7 +345,7 @@ function Sidebar() {
|
||||
key={child.path}
|
||||
onClick={() => navigate(`/${child.path}`)}
|
||||
sx={{
|
||||
gap: theme.gap.medium,
|
||||
gap: theme.gap.small,
|
||||
borderRadius: `${theme.shape.borderRadius}px`,
|
||||
pl: theme.gap.small,
|
||||
}}
|
||||
@@ -210,63 +372,156 @@ 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],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
disableInteractive
|
||||
>
|
||||
<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],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
disableInteractive
|
||||
>
|
||||
<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>
|
||||
);
|
||||
|
||||
@@ -98,16 +98,17 @@ const PasswordPanel = () => {
|
||||
|
||||
return (
|
||||
<TabPanel value="password">
|
||||
<form
|
||||
<Stack
|
||||
component="form"
|
||||
onSubmit={handleSubmit}
|
||||
className="edit-password-form"
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={theme.gap.xl}
|
||||
>
|
||||
<div className="edit-password-form__wrapper">
|
||||
<Stack>
|
||||
<Stack direction="row">
|
||||
<Box flex={0.9}>
|
||||
<Typography component="h1">Current password</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Field
|
||||
type="password"
|
||||
id="edit-current-password"
|
||||
@@ -117,11 +118,11 @@ const PasswordPanel = () => {
|
||||
onChange={handleChange}
|
||||
error={errors[idToName["edit-current-password"]]}
|
||||
/>
|
||||
</div>
|
||||
<div className="edit-password-form__wrapper">
|
||||
<Stack>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Box flex={0.9}>
|
||||
<Typography component="h1">New password</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Field
|
||||
type="password"
|
||||
id="edit-new-password"
|
||||
@@ -131,11 +132,11 @@ const PasswordPanel = () => {
|
||||
onChange={handleChange}
|
||||
error={errors[idToName["edit-new-password"]]}
|
||||
/>
|
||||
</div>
|
||||
<div className="edit-password-form__wrapper">
|
||||
<Stack>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Box flex={0.9}>
|
||||
<Typography component="h1">Confirm new password</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Field
|
||||
type="password"
|
||||
id="edit-confirm-password"
|
||||
@@ -145,33 +146,32 @@ const PasswordPanel = () => {
|
||||
onChange={handleChange}
|
||||
error={errors[idToName["edit-confirm-password"]]}
|
||||
/>
|
||||
</div>
|
||||
<div className="edit-password-form__wrapper">
|
||||
<Stack></Stack>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Box flex={0.9}></Box>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Alert
|
||||
variant="warning"
|
||||
body="New password must contain at least 8 characters and must have at least one uppercase letter, one number and one symbol."
|
||||
/>
|
||||
</Box>
|
||||
</div>
|
||||
<Divider aria-hidden="true" width="0" />
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<Box width="fit-content">
|
||||
<ButtonSpinner
|
||||
level="primary"
|
||||
label="Save"
|
||||
onClick={handleSubmit}
|
||||
isLoading={isLoading}
|
||||
loadingText="Saving..."
|
||||
disabled={Object.keys(errors).length !== 0 && true}
|
||||
sx={{
|
||||
paddingX: theme.gap.xl,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</form>
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<ButtonSpinner
|
||||
level="primary"
|
||||
label="Save"
|
||||
onClick={handleSubmit}
|
||||
isLoading={isLoading}
|
||||
loadingText="Saving..."
|
||||
disabled={Object.keys(errors).length !== 0 && true}
|
||||
sx={{
|
||||
paddingX: theme.gap.large,
|
||||
width: "fit-content",
|
||||
mt: theme.gap.xl,
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</TabPanel>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@ import ProgressUpload from "../../ProgressBars";
|
||||
import { formatBytes } from "../../../Utils/fileUtils";
|
||||
import { clearUptimeMonitorState } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
|
||||
/**
|
||||
* ProfilePanel component displays a form for editing user profile information
|
||||
@@ -216,11 +217,17 @@ const ProfilePanel = () => {
|
||||
|
||||
return (
|
||||
<TabPanel value="profile">
|
||||
<form className="edit-profile-form" noValidate spellCheck="false">
|
||||
<div className="edit-profile-form__wrapper">
|
||||
<Stack>
|
||||
<Stack
|
||||
component="form"
|
||||
className="edit-profile-form"
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={theme.gap.xl}
|
||||
>
|
||||
<Stack direction="row">
|
||||
<Box flex={0.9}>
|
||||
<Typography component="h1">First name</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Field
|
||||
id="edit-first-name"
|
||||
value={localData.firstName}
|
||||
@@ -229,11 +236,11 @@ const ProfilePanel = () => {
|
||||
onChange={handleChange}
|
||||
error={errors[idToName["edit-first-name"]]}
|
||||
/>
|
||||
</div>
|
||||
<div className="edit-profile-form__wrapper">
|
||||
<Stack>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Box flex={0.9}>
|
||||
<Typography component="h1">Last name</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Field
|
||||
id="edit-last-name"
|
||||
placeholder="Enter your last name"
|
||||
@@ -242,11 +249,11 @@ const ProfilePanel = () => {
|
||||
onChange={handleChange}
|
||||
error={errors[idToName["edit-last-name"]]}
|
||||
/>
|
||||
</div>
|
||||
<div className="edit-profile-form__wrapper">
|
||||
<Stack>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Stack flex={0.9}>
|
||||
<Typography component="h1">Email</Typography>
|
||||
<Typography component="p">
|
||||
<Typography component="p" sx={{ opacity: 0.6 }}>
|
||||
This is your current email address — it cannot be changed.
|
||||
</Typography>
|
||||
</Stack>
|
||||
@@ -255,20 +262,19 @@ const ProfilePanel = () => {
|
||||
value={user.email}
|
||||
placeholder="Enter your email"
|
||||
autoComplete="email"
|
||||
// TODO - add onChange
|
||||
onChange={() => console.log("Disabled.")}
|
||||
onChange={() => logger.warn("disabled")}
|
||||
// error={errors[idToName["edit-email"]]}
|
||||
disabled={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="edit-profile-form__wrapper">
|
||||
<Stack>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Stack flex={0.9}>
|
||||
<Typography component="h1">Your photo</Typography>
|
||||
<Typography component="p">
|
||||
<Typography component="p" sx={{ opacity: 0.6 }}>
|
||||
This photo will be displayed in your profile page.
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack className="row-stack" direction="row" alignItems="center">
|
||||
<Stack direction="row" alignItems="center" flex={1}>
|
||||
<Avatar
|
||||
src={
|
||||
localData?.deleteProfileImage
|
||||
@@ -293,10 +299,9 @@ const ProfilePanel = () => {
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
</Stack>
|
||||
<Divider
|
||||
aria-hidden="true"
|
||||
className="short-divider"
|
||||
width="0"
|
||||
sx={{ marginY: theme.spacing(1) }}
|
||||
/>
|
||||
@@ -312,31 +317,33 @@ const ProfilePanel = () => {
|
||||
Object.keys(errors).length !== 0 && !errors?.picture && true
|
||||
}
|
||||
sx={{
|
||||
paddingX: "40px",
|
||||
paddingX: theme.gap.large,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</form>
|
||||
<Divider aria-hidden="true" />
|
||||
<form className="delete-profile-form" noValidate spellCheck="false">
|
||||
<div className="delete-profile-form__wrapper">
|
||||
<Stack direction="column" gap="15px">
|
||||
<Typography component="h1">Delete account</Typography>
|
||||
<Typography component="p">
|
||||
Note that deleting your account will remove all data from our
|
||||
system. This is permanent and non-recoverable.
|
||||
</Typography>
|
||||
<Box sx={{ mt: theme.spacing(1) }}>
|
||||
<Button
|
||||
level="error"
|
||||
label="Delete account"
|
||||
onClick={() => setIsOpen("delete")}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</div>
|
||||
</form>
|
||||
</Stack>
|
||||
<Divider aria-hidden="true" sx={{ marginY: theme.spacing(6) }} />
|
||||
<Stack
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={theme.gap.small}
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">Delete account</Typography>
|
||||
<Typography component="p" sx={{ opacity: 0.6 }}>
|
||||
Note that deleting your account will remove all data from our
|
||||
system. This is permanent and non-recoverable.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
level="error"
|
||||
label="Delete account"
|
||||
onClick={() => setIsOpen("delete")}
|
||||
sx={{ width: "fit-content", mt: theme.gap.small }}
|
||||
/>
|
||||
</Stack>
|
||||
{/* TODO - Update ModalPopup Component with @mui for reusability */}
|
||||
<Modal
|
||||
aria-labelledby="modal-delete-account"
|
||||
@@ -398,7 +405,7 @@ const ProfilePanel = () => {
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 400,
|
||||
width: 450,
|
||||
bgcolor: "white",
|
||||
border: "solid 1px #f2f2f2",
|
||||
borderRadius: `${theme.shape.borderRadius}px`,
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useEffect, useState } from "react";
|
||||
import EditSvg from "../../../assets/icons/edit.svg?react";
|
||||
import Field from "../../Inputs/Field";
|
||||
import { credentials } from "../../../Validation/validation";
|
||||
import axiosInstance from "../../../Utils/axiosConfig";
|
||||
import { networkService } from "../../../main";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { useSelector } from "react-redux";
|
||||
import BasicTable from "../../BasicTable";
|
||||
@@ -52,10 +52,7 @@ const TeamPanel = () => {
|
||||
useEffect(() => {
|
||||
const fetchTeam = async () => {
|
||||
try {
|
||||
const response = await axiosInstance.get("/auth/users", {
|
||||
headers: { Authorization: `Bearer ${authToken}` },
|
||||
});
|
||||
|
||||
const response = await networkService.getAllUsers(authToken);
|
||||
setMembers(response.data.data);
|
||||
} catch (error) {
|
||||
createToast({
|
||||
@@ -134,9 +131,6 @@ const TeamPanel = () => {
|
||||
};
|
||||
const handleRename = () => {};
|
||||
|
||||
//TODO - implement save team function
|
||||
const handleSaveTeam = () => {};
|
||||
|
||||
// INVITE MEMBER
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
@@ -179,13 +173,10 @@ const TeamPanel = () => {
|
||||
setErrors((prev) => ({ ...prev, email: error.details[0].message }));
|
||||
} else
|
||||
try {
|
||||
await axiosInstance.post(
|
||||
"/auth/invite",
|
||||
{
|
||||
email: toInvite.email,
|
||||
role: toInvite.role,
|
||||
},
|
||||
{ headers: { Authorization: `Bearer ${authToken}` } }
|
||||
await networkService.requestInvitationToken(
|
||||
authToken,
|
||||
toInvite.email,
|
||||
toInvite.role
|
||||
);
|
||||
|
||||
closeInviteModal();
|
||||
@@ -206,12 +197,11 @@ const TeamPanel = () => {
|
||||
|
||||
return (
|
||||
<TabPanel value="team">
|
||||
<form className="edit-organization-form">
|
||||
<Stack component="form">
|
||||
<Box sx={{ alignSelf: "flex-start" }}>
|
||||
<Typography component="h1">Organization name</Typography>
|
||||
</Box>
|
||||
<Stack
|
||||
className="row-stack"
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
alignItems="center"
|
||||
@@ -251,21 +241,13 @@ const TeamPanel = () => {
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</form>
|
||||
<Divider
|
||||
aria-hidden="true"
|
||||
className="short-divider"
|
||||
sx={{ marginY: theme.spacing(4) }}
|
||||
/>
|
||||
<form
|
||||
className="edit-team-form"
|
||||
</Stack>
|
||||
<Divider aria-hidden="true" sx={{ marginY: theme.spacing(4) }} />
|
||||
<Stack
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: theme.gap.large,
|
||||
}}
|
||||
gap={theme.gap.large}
|
||||
>
|
||||
<Typography component="h1">Team members</Typography>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
@@ -313,22 +295,7 @@ const TeamPanel = () => {
|
||||
/>
|
||||
</Stack>
|
||||
<BasicTable data={tableData} paginated={false} reversed={true} />
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<Box width="fit-content">
|
||||
<ButtonSpinner
|
||||
level="primary"
|
||||
label="Save"
|
||||
onClick={handleSaveTeam}
|
||||
isLoading={false}
|
||||
loadingText="Saving..."
|
||||
disabled={true}
|
||||
sx={{
|
||||
paddingX: "40px",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</form>
|
||||
</Stack>
|
||||
<Modal
|
||||
aria-labelledby="modal-invite-member"
|
||||
aria-describedby="invite-member-to-team"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import axiosInstance from "../../Utils/axiosConfig";
|
||||
import { networkService } from "../../main";
|
||||
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
|
||||
@@ -14,7 +14,7 @@ export const register = createAsyncThunk(
|
||||
"auth/register",
|
||||
async (form, thunkApi) => {
|
||||
try {
|
||||
const res = await axiosInstance.post("/auth/register", form);
|
||||
const res = await networkService.registerUser(form);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response.data) {
|
||||
@@ -31,7 +31,7 @@ export const register = createAsyncThunk(
|
||||
|
||||
export const login = createAsyncThunk("auth/login", async (form, thunkApi) => {
|
||||
try {
|
||||
const res = await axiosInstance.post(`/auth/login`, form);
|
||||
const res = await networkService.loginUser(form);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
@@ -51,16 +51,13 @@ export const update = createAsyncThunk(
|
||||
const { authToken: token, localData: form } = data;
|
||||
const user = jwtDecode(token);
|
||||
try {
|
||||
//1.5s delay to show loading spinner
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
|
||||
const fd = new FormData();
|
||||
form.firstName && fd.append("firstName", form.firstName);
|
||||
form.lastName && fd.append("lastName", form.lastName);
|
||||
form.password && fd.append("password", form.password);
|
||||
form.newPassword && fd.append("newPassword", form.newPassword);
|
||||
if (form.file && form.file !== "") {
|
||||
const imageResult = await axiosInstance.get(form.file, {
|
||||
const imageResult = await networkService.get(form.file, {
|
||||
responseType: "blob",
|
||||
baseURL: "",
|
||||
});
|
||||
@@ -69,12 +66,8 @@ export const update = createAsyncThunk(
|
||||
form.deleteProfileImage &&
|
||||
fd.append("deleteProfileImage", form.deleteProfileImage);
|
||||
|
||||
const res = await axiosInstance.put(`/auth/user/${user._id}`, fd, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
const res = await networkService.updateUser(token, user._id, fd);
|
||||
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
@@ -95,9 +88,7 @@ export const deleteUser = createAsyncThunk(
|
||||
const user = jwtDecode(data);
|
||||
|
||||
try {
|
||||
const res = await axiosInstance.delete(`/auth/user/${user._id}`, {
|
||||
headers: { Authorization: `Bearer ${data}` },
|
||||
});
|
||||
const res = await networkService.deleteUser(data, user._id);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
@@ -116,7 +107,7 @@ export const forgotPassword = createAsyncThunk(
|
||||
"auth/forgotPassword",
|
||||
async (form, thunkApi) => {
|
||||
try {
|
||||
const res = await axiosInstance.post("/auth/recovery/request", form);
|
||||
const res = await networkService.forgotPassword(form);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response.data) {
|
||||
@@ -136,13 +127,8 @@ export const setNewPassword = createAsyncThunk(
|
||||
async (data, thunkApi) => {
|
||||
const { token, form } = data;
|
||||
try {
|
||||
await axiosInstance.post("/auth/recovery/validate", {
|
||||
recoveryToken: token,
|
||||
});
|
||||
const res = await axiosInstance.post("/auth/recovery/reset", {
|
||||
...form,
|
||||
recoveryToken: token,
|
||||
});
|
||||
await networkService.validateRecoveryToken(token);
|
||||
const res = await networkService.setNewPassword(token, form);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response.data) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import axiosInstance from "../../Utils/axiosConfig";
|
||||
import { networkService } from "../../main";
|
||||
const initialState = {
|
||||
isLoading: false,
|
||||
monitors: [],
|
||||
@@ -13,32 +13,7 @@ export const createPageSpeed = createAsyncThunk(
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitor } = data;
|
||||
|
||||
const res = await axiosInstance.post(`/monitors`, monitor, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const getPageSpeedMonitors = createAsyncThunk(
|
||||
"pageSpeedMonitors/getPageSpeedMonitors",
|
||||
async (token, thunkApi) => {
|
||||
try {
|
||||
const res = await axiosInstance.get("/monitors");
|
||||
const res = await networkService.createMonitor(authToken, monitor);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
@@ -58,14 +33,16 @@ export const getPageSpeedByUserId = createAsyncThunk(
|
||||
async (token, thunkApi) => {
|
||||
const user = jwtDecode(token);
|
||||
try {
|
||||
const res = await axiosInstance.get(
|
||||
`/monitors/user/${user._id}?limit=25&type=pagespeed&sortOrder=desc`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
const res = await networkService.getMonitorsByUserId(
|
||||
token,
|
||||
user._id,
|
||||
25,
|
||||
["pagespeed"],
|
||||
null,
|
||||
"desc",
|
||||
false
|
||||
);
|
||||
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
@@ -91,15 +68,10 @@ export const updatePageSpeed = createAsyncThunk(
|
||||
interval: monitor.interval,
|
||||
// notifications: monitor.notifications,
|
||||
};
|
||||
const res = await axiosInstance.put(
|
||||
`/monitors/${monitor._id}`,
|
||||
updatedFields,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
const res = await networkService.updateMonitor(
|
||||
authToken,
|
||||
monitor._id,
|
||||
updatedFields
|
||||
);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
@@ -120,12 +92,10 @@ export const deletePageSpeed = createAsyncThunk(
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitor } = data;
|
||||
const res = await axiosInstance.delete(`/monitors/${monitor._id}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const res = await networkService.deleteMonitorById(
|
||||
authToken,
|
||||
monitor._id
|
||||
);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
@@ -153,25 +123,6 @@ const pageSpeedMonitorSlice = createSlice({
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
// *****************************************************
|
||||
// All Monitors
|
||||
// *****************************************************
|
||||
.addCase(getPageSpeedMonitors.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getPageSpeedMonitors.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
state.monitors = action.payload.data;
|
||||
})
|
||||
.addCase(getPageSpeedMonitors.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Getting montiors failed";
|
||||
})
|
||||
// *****************************************************
|
||||
// Monitors by userId
|
||||
// *****************************************************
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
// Initial state for UI settings.
|
||||
// Add more settings as needed (e.g., theme preferences, user settings)
|
||||
const initialState = {
|
||||
monitors: {
|
||||
rowsPerPage: 5,
|
||||
},
|
||||
team: {
|
||||
rowsPerPage: 5,
|
||||
},
|
||||
sidebar: {
|
||||
collapsed: false,
|
||||
},
|
||||
};
|
||||
|
||||
const uiSlice = createSlice({
|
||||
name: "ui",
|
||||
initialState,
|
||||
reducers: {
|
||||
setRowsPerPage: (state, action) => {
|
||||
state[action.payload.table].rowsPerPage = action.payload.value;
|
||||
},
|
||||
toggleSidebar: (state) => {
|
||||
state.sidebar.collapsed = !state.sidebar.collapsed;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default uiSlice.reducer;
|
||||
export const { setRowsPerPage, toggleSidebar } = uiSlice.actions;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import axiosInstance from "../../Utils/axiosConfig";
|
||||
import { networkService } from "../../main";
|
||||
const initialState = {
|
||||
isLoading: false,
|
||||
monitors: [],
|
||||
@@ -13,32 +13,7 @@ export const createUptimeMonitor = createAsyncThunk(
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitor } = data;
|
||||
|
||||
const res = await axiosInstance.post(`/monitors`, monitor, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const getUptimeMonitors = createAsyncThunk(
|
||||
"monitors/getMonitors",
|
||||
async (token, thunkApi) => {
|
||||
try {
|
||||
const res = await axiosInstance.get("/monitors");
|
||||
const res = await networkService.createMonitor(authToken, monitor);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
@@ -58,13 +33,14 @@ export const getUptimeMonitorsByUserId = createAsyncThunk(
|
||||
async (token, thunkApi) => {
|
||||
const user = jwtDecode(token);
|
||||
try {
|
||||
const res = await axiosInstance.get(
|
||||
`/monitors/user/${user._id}?limit=25&type=http&type=ping&sortOrder=desc&normalize=true`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
const res = await networkService.getMonitorsByUserId(
|
||||
token,
|
||||
user._id,
|
||||
25,
|
||||
["http", "ping"],
|
||||
null,
|
||||
"desc",
|
||||
true
|
||||
);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
@@ -91,15 +67,10 @@ export const updateUptimeMonitor = createAsyncThunk(
|
||||
interval: monitor.interval,
|
||||
notifications: monitor.notifications,
|
||||
};
|
||||
const res = await axiosInstance.put(
|
||||
`/monitors/${monitor._id}`,
|
||||
updatedFields,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
const res = await networkService.updateMonitor(
|
||||
authToken,
|
||||
monitor._id,
|
||||
updatedFields
|
||||
);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
@@ -120,12 +91,10 @@ export const deleteUptimeMonitor = createAsyncThunk(
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitor } = data;
|
||||
const res = await axiosInstance.delete(`/monitors/${monitor._id}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const res = await networkService.deleteMonitorById(
|
||||
authToken,
|
||||
monitor._id
|
||||
);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
@@ -153,25 +122,6 @@ const uptimeMonitorsSlice = createSlice({
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
// *****************************************************
|
||||
// All Monitors
|
||||
// *****************************************************
|
||||
.addCase(getUptimeMonitors.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getUptimeMonitors.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
state.monitors = action.payload.data;
|
||||
})
|
||||
.addCase(getUptimeMonitors.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Getting uptime monitors failed";
|
||||
})
|
||||
// *****************************************************
|
||||
// Monitors by userId
|
||||
// *****************************************************
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import axiosInstance from "../Utils/axiosConfig";
|
||||
|
||||
import { logger } from "../Utils/Logger";
|
||||
import { networkService } from "../main";
|
||||
|
||||
const withAdminCheck = (WrappedComponent) => {
|
||||
const WithAdminCheck = (props) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
axiosInstance
|
||||
.get("/auth/users/admin")
|
||||
networkService
|
||||
.doesAdminExist()
|
||||
.then((response) => {
|
||||
if (response.data.data === true) {
|
||||
navigate("/login");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
logger.error(error);
|
||||
});
|
||||
}, [navigate]);
|
||||
return <WrappedComponent {...props} isAdmin={true} />;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -15,18 +15,10 @@
|
||||
.account h1.MuiTypography-root {
|
||||
font-weight: 600;
|
||||
}
|
||||
.account .MuiStack-root:not(.table-stack) {
|
||||
flex: 1;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
}
|
||||
.account .MuiTabPanel-root {
|
||||
padding: 0;
|
||||
margin-top: 50px;
|
||||
}
|
||||
.account .MuiDivider-root:not(.short-divider) {
|
||||
margin: 50px 0;
|
||||
}
|
||||
.account .MuiDivider-root,
|
||||
.account .MuiButtonGroup-root button {
|
||||
border-color: var(--env-var-color-16);
|
||||
@@ -34,50 +26,14 @@
|
||||
.account button:not(.MuiIconButton-root) {
|
||||
height: 34px;
|
||||
}
|
||||
.account [class$="-form"] {
|
||||
width: inherit;
|
||||
}
|
||||
.account [class$="__wrapper"],
|
||||
.account [class$="__wrapper compact"] {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.edit-profile-form__wrapper > .MuiStack-root:not(.row-stack),
|
||||
.edit-password-form__wrapper > .MuiStack-root:not(.row-stack) {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.edit-profile-form__wrapper p.MuiTypography-root,
|
||||
.delete-profile-form__wrapper p.MuiTypography-root {
|
||||
opacity: 0.6;
|
||||
.account .field {
|
||||
flex: 1;
|
||||
}
|
||||
#modal-delete-account,
|
||||
#modal-edit-org-name,
|
||||
#modal-invite-member {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
}
|
||||
.edit-profile-form__wrapper:not(:first-of-type),
|
||||
.edit-password-form__wrapper:not(:first-of-type),
|
||||
.edit-team-form__wrapper:not(:first-of-type) {
|
||||
margin-top: var(--env-var-spacing-4);
|
||||
}
|
||||
.edit-team-form__wrapper.compact {
|
||||
margin-top: var(--env-var-spacing-3);
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* for testing, will remove later */
|
||||
@media only screen and (max-width: 1600px) {
|
||||
.edit-profile-form__wrapper .MuiStack-root:not(.row-stack),
|
||||
.edit-password-form__wrapper .MuiStack-root:not(.row-stack),
|
||||
.edit-organization-form__wrapper .MuiStack-root:not(.row-stack) {
|
||||
margin-right: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.account .MuiStack-root:has(span.MuiTypography-root.input-error) {
|
||||
position: relative;
|
||||
@@ -87,22 +43,9 @@
|
||||
top: 100%;
|
||||
}
|
||||
|
||||
.edit-profile-form__wrapper .MuiFormControl-root,
|
||||
.edit-password-form__wrapper .MuiFormControl-root {
|
||||
width: 100%;
|
||||
.account .MuiTable-root .MuiTableBody-root .MuiTableCell-root {
|
||||
color: var(--env-var-color-25);
|
||||
}
|
||||
.edit-profile-form__wrapper .MuiStack-root:has(.MuiFormControl-root),
|
||||
.edit-password-form__wrapper .MuiStack-root:has(.MuiFormControl-root) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.edit-organization-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.edit-team-form .MuiTableCell-root {
|
||||
color: var(--env-var-color-25) !important;
|
||||
}
|
||||
.edit-team-form .MuiTableBody-root .MuiTableCell-root {
|
||||
padding: var(--env-var-spacing-1-plus) var(--env-var-spacing-2) !important;
|
||||
.account .MuiTableBody-root .MuiTableCell-root {
|
||||
padding: var(--env-var-spacing-1-plus) var(--env-var-spacing-2);
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Box, Tab, useTheme } from "@mui/material";
|
||||
import TabContext from "@mui/lab/TabContext";
|
||||
import TabList from "@mui/lab/TabList";
|
||||
import "./index.css";
|
||||
import ProfilePanel from "../../Components/TabPanels/Account/ProfilePanel";
|
||||
import PasswordPanel from "../../Components/TabPanels/Account/PasswordPanel";
|
||||
import TeamPanel from "../../Components/TabPanels/Account/TeamPanel";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useSelector } from "react-redux";
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
* Account component renders a settings page with tabs for Profile, Password, and Team settings.
|
||||
|
||||
@@ -145,7 +145,7 @@ const CheckEmail = () => {
|
||||
}}
|
||||
/>
|
||||
<Typography sx={{ alignSelf: "center", mb: theme.gap.medium }}>
|
||||
Didn't receive the email?{" "}
|
||||
Didn't receive the email?{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
onClick={resendToken}
|
||||
|
||||
@@ -131,7 +131,7 @@ const ForgotPassword = () => {
|
||||
<Key alt="password key icon" />
|
||||
<Typography component="h1">Forgot password?</Typography>
|
||||
<Typography>
|
||||
No worries, we'll send you reset instructions.
|
||||
No worries, we'll send you reset instructions.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box width="100%" textAlign="left">
|
||||
|
||||
@@ -4,16 +4,17 @@ 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 axiosInstance from "../../Utils/axiosConfig";
|
||||
import { networkService } from "../../main";
|
||||
import Field from "../../Components/Inputs/Field";
|
||||
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 { logger } from "../../Utils/Logger";
|
||||
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,17 +272,21 @@ const Login = () => {
|
||||
const [step, setStep] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
axiosInstance
|
||||
.get("/auth/users/admin")
|
||||
if (authToken) {
|
||||
navigate("/monitors");
|
||||
return;
|
||||
}
|
||||
networkService
|
||||
.doesAdminExist()
|
||||
.then((response) => {
|
||||
if (response.data.data === false) {
|
||||
navigate("/register");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
logger.error(error);
|
||||
});
|
||||
}, [navigate]);
|
||||
}, [authToken, navigate]);
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
@@ -399,7 +427,9 @@ const Login = () => {
|
||||
)}
|
||||
</Stack>
|
||||
<Box textAlign="center" p={theme.gap.large}>
|
||||
<Typography display="inline-block">Don't have an account? —</Typography>
|
||||
<Typography display="inline-block">
|
||||
Don't have an account? —
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
ml={theme.gap.xs}
|
||||
|
||||
@@ -15,8 +15,9 @@ import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
import Check from "../../../Components/Check/Check";
|
||||
import Button from "../../../Components/Button";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import axiosInstance from "../../../Utils/axiosConfig";
|
||||
import { networkService } from "../../../main";
|
||||
import "../index.css";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
|
||||
/**
|
||||
* Displays the initial landing page.
|
||||
@@ -68,6 +69,11 @@ const LandingPage = ({ isAdmin, onSignup }) => {
|
||||
);
|
||||
};
|
||||
|
||||
LandingPage.propTypes = {
|
||||
isAdmin: PropTypes.bool,
|
||||
onSignup: PropTypes.func,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the first step of the sign up process.
|
||||
*
|
||||
@@ -151,6 +157,14 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
);
|
||||
};
|
||||
|
||||
StepOne.propTypes = {
|
||||
form: PropTypes.object,
|
||||
errors: PropTypes.object,
|
||||
onSubmit: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
onBack: PropTypes.func,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the second step of the sign up process.
|
||||
*
|
||||
@@ -223,6 +237,14 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
);
|
||||
};
|
||||
|
||||
StepTwo.propTypes = {
|
||||
form: PropTypes.object,
|
||||
errors: PropTypes.object,
|
||||
onSubmit: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
onBack: PropTypes.func,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the third step of the sign up process.
|
||||
*
|
||||
@@ -368,6 +390,14 @@ const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
);
|
||||
};
|
||||
|
||||
StepThree.propTypes = {
|
||||
form: PropTypes.object,
|
||||
errors: PropTypes.object,
|
||||
onSubmit: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
onBack: PropTypes.func,
|
||||
};
|
||||
|
||||
const Register = ({ isAdmin }) => {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
@@ -397,19 +427,16 @@ const Register = ({ isAdmin }) => {
|
||||
const fetchInvite = async () => {
|
||||
if (token !== undefined) {
|
||||
try {
|
||||
const res = await axiosInstance.post(`/auth/invite/verify`, {
|
||||
token,
|
||||
});
|
||||
const res = await networkService.verifyInvitationToken(token);
|
||||
const { role, email } = res.data.data;
|
||||
console.log(role);
|
||||
setForm({ ...form, email, role });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
logger.error(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
fetchInvite();
|
||||
}, [token]);
|
||||
}, [token, form]);
|
||||
|
||||
/**
|
||||
* Validates the form data against the validation schema.
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/* Home Page Styles*/
|
||||
@@ -1,16 +0,0 @@
|
||||
import DropdownTeamMember from "../../Components/DropdownTeamMember";
|
||||
import Search from "../../Components/Search";
|
||||
import "./index.css";
|
||||
import DashboardMenu from "../../Components/DashboardMenu";
|
||||
import Integrations from "../../Components/Integrations";
|
||||
|
||||
const Home = () => {
|
||||
const token = localStorage.getItem("token");
|
||||
return (
|
||||
<>
|
||||
<div>Home</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
@@ -18,8 +18,9 @@ import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import axiosInstance from "../../../Utils/axiosConfig";
|
||||
import { networkService } from "../../../main";
|
||||
import { StatusLabel } from "../../../Components/Label";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
|
||||
const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
const { authToken, user } = useSelector((state) => state.auth);
|
||||
@@ -31,10 +32,10 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setPaginationController({
|
||||
...paginationController,
|
||||
setPaginationController((prevPaginationController) => ({
|
||||
...prevPaginationController,
|
||||
page: 0,
|
||||
});
|
||||
}));
|
||||
}, [filter, selectedMonitor]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -43,21 +44,34 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let url = `/checks/${selectedMonitor}?sortOrder=desc&filter=${filter}&page=${paginationController.page}&rowsPerPage=${paginationController.rowsPerPage}`;
|
||||
|
||||
let res;
|
||||
if (selectedMonitor === "0") {
|
||||
url = `/checks/user/${user._id}?sortOrder=desc&filter=${filter}&page=${paginationController.page}&rowsPerPage=${paginationController.rowsPerPage}`;
|
||||
res = await networkService.getChecksByUser(
|
||||
authToken,
|
||||
user._id,
|
||||
"desc",
|
||||
null,
|
||||
null,
|
||||
filter,
|
||||
paginationController.page,
|
||||
paginationController.rowsPerPage
|
||||
);
|
||||
} else {
|
||||
res = await networkService.getChecksByMonitor(
|
||||
authToken,
|
||||
selectedMonitor,
|
||||
"desc",
|
||||
null,
|
||||
null,
|
||||
filter,
|
||||
paginationController.page,
|
||||
paginationController.rowsPerPage
|
||||
);
|
||||
}
|
||||
|
||||
const res = await axiosInstance.get(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
setChecks(res.data.data.checks);
|
||||
setChecksCount(res.data.data.checksCount);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
logger.error(error);
|
||||
}
|
||||
};
|
||||
fetchPage();
|
||||
@@ -72,7 +86,6 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
]);
|
||||
|
||||
const handlePageChange = (_, newPage) => {
|
||||
console.log(newPage);
|
||||
setPaginationController({
|
||||
...paginationController,
|
||||
page: newPage - 1, // 0-indexed
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState, useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { ButtonGroup, Stack, Skeleton, Typography } from "@mui/material";
|
||||
import Button from "../../Components/Button";
|
||||
import axiosInstance from "../../Utils/axiosConfig";
|
||||
import { networkService } from "../../main";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import Select from "../../Components/Inputs/Select";
|
||||
import IncidentTable from "./IncidentTable";
|
||||
@@ -48,13 +48,14 @@ const Incidents = () => {
|
||||
useEffect(() => {
|
||||
const fetchMonitors = async () => {
|
||||
setLoading(true);
|
||||
const res = await axiosInstance.get(
|
||||
`/monitors/user/${authState.user._id}?status=false&limit=1`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authState.authToken}`,
|
||||
},
|
||||
}
|
||||
const res = await networkService.getMonitorsByUserId(
|
||||
authState.authToken,
|
||||
authState.user._id,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
// Reduce to a lookup object for 0(1) lookup
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import "./index.css";
|
||||
import React, { useState } from "react";
|
||||
import { 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";
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import Fallback from "../../Components/Fallback";
|
||||
import "./index.css";
|
||||
|
||||
const Maintenance = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div className="maintenance">
|
||||
<Fallback
|
||||
|
||||
@@ -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,9 @@ import {
|
||||
deleteUptimeMonitor,
|
||||
} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import Checkbox from "../../../Components/Inputs/Checkbox";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import "./index.css";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
|
||||
/**
|
||||
* Parses a URL string and returns a URL object.
|
||||
@@ -96,7 +95,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();
|
||||
|
||||
@@ -111,14 +109,13 @@ const Configure = () => {
|
||||
useEffect(() => {
|
||||
const data = monitors.find((monitor) => monitor._id === monitorId);
|
||||
if (!data) {
|
||||
console.error("Error fetching monitor of id: " + monitorId);
|
||||
navigate("/not-found");
|
||||
logger.error("Error fetching monitor of id: " + monitorId);
|
||||
navigate("/not-found", { replace: true });
|
||||
}
|
||||
setMonitor({
|
||||
...data,
|
||||
});
|
||||
setDuration(formatDurationRounded(data?.interval));
|
||||
}, [monitorId, authToken, monitors]);
|
||||
}, [monitorId, authToken, monitors, navigate]);
|
||||
|
||||
const handleChange = (event, name) => {
|
||||
let { value, id } = event.target;
|
||||
@@ -210,74 +207,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 +263,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 +281,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 +322,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>
|
||||
@@ -362,7 +338,7 @@ const Configure = () => {
|
||||
label="Notify via SMS (coming soon)"
|
||||
isChecked={false}
|
||||
value=""
|
||||
onChange={() => console.log("disabled")}
|
||||
onChange={() => logger.warn("disabled")}
|
||||
isDisabled={true}
|
||||
/>
|
||||
<Checkbox
|
||||
@@ -381,7 +357,7 @@ const Configure = () => {
|
||||
label="Also notify via email to multiple addresses (coming soon)"
|
||||
isChecked={false}
|
||||
value=""
|
||||
onChange={() => console.log("disabled")}
|
||||
onChange={() => logger.warn("disabled")}
|
||||
isDisabled={true}
|
||||
/>
|
||||
{monitor?.notifications?.some(
|
||||
@@ -393,7 +369,7 @@ const Configure = () => {
|
||||
type="text"
|
||||
placeholder="name@gmail.com"
|
||||
value=""
|
||||
onChange={() => console.log("disabled")}
|
||||
onChange={() => logger.warn("disabled")}
|
||||
/>
|
||||
<Typography mt={theme.gap.small}>
|
||||
You can separate multiple emails with a comma
|
||||
@@ -408,7 +384,6 @@ const Configure = () => {
|
||||
className="config-box"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.gap.xxl}
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h2">Advanced settings</Typography>
|
||||
@@ -423,7 +398,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 +406,7 @@ const Configure = () => {
|
||||
onClick={handleSubmit}
|
||||
/>
|
||||
</Stack>
|
||||
</form>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
<Modal
|
||||
@@ -460,7 +435,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 +455,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 {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import "./index.css";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import RadioButton from "../../../Components/RadioButton";
|
||||
import Button from "../../../Components/Button";
|
||||
import { Box, ButtonGroup, Stack, Typography } from "@mui/material";
|
||||
@@ -8,11 +8,12 @@ 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";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
|
||||
const CreateMonitor = () => {
|
||||
const MS_PER_MINUTE = 60000;
|
||||
@@ -143,33 +144,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 +194,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,34 +243,32 @@ 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)"
|
||||
isChecked={false}
|
||||
value=""
|
||||
onChange={() => console.log("disabled")}
|
||||
onChange={() => logger.warn("disabled")}
|
||||
isDisabled={true}
|
||||
/>
|
||||
<Checkbox
|
||||
@@ -293,7 +285,7 @@ const CreateMonitor = () => {
|
||||
label="Also notify via email to multiple addresses (coming soon)"
|
||||
isChecked={false}
|
||||
value=""
|
||||
onChange={() => console.log("disabled")}
|
||||
onChange={() => logger.warn("disabled")}
|
||||
isDisabled={true}
|
||||
/>
|
||||
{monitor.notifications.some(
|
||||
@@ -305,7 +297,7 @@ const CreateMonitor = () => {
|
||||
type="text"
|
||||
placeholder="name@gmail.com"
|
||||
value=""
|
||||
onChange={() => console.log("disabled")}
|
||||
onChange={() => logger.warn("disabled")}
|
||||
/>
|
||||
<Typography mt={theme.gap.small}>
|
||||
You can separate multiple emails with a comma
|
||||
@@ -404,7 +396,7 @@ const CreateMonitor = () => {
|
||||
disabled={Object.keys(errors).length !== 0 && true}
|
||||
/>
|
||||
</Stack>
|
||||
</form>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,10 +13,11 @@ import {
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import axiosInstance from "../../../../Utils/axiosConfig";
|
||||
import { networkService } from "../../../../main";
|
||||
import { StatusLabel } from "../../../../Components/Label";
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded";
|
||||
import { logger } from "../../../../Utils/Logger";
|
||||
|
||||
const PaginationTable = ({ monitorId, dateRange }) => {
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
@@ -28,27 +29,29 @@ const PaginationTable = ({ monitorId, dateRange }) => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setPaginationController({
|
||||
...paginationController,
|
||||
setPaginationController((prevPaginationController) => ({
|
||||
...prevPaginationController,
|
||||
page: 0,
|
||||
});
|
||||
}));
|
||||
}, [dateRange]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPage = async () => {
|
||||
try {
|
||||
const res = await axiosInstance.get(
|
||||
`/checks/${monitorId}?sortOrder=desc&dateRange=${dateRange}&page=${paginationController.page}&rowsPerPage=${paginationController.rowsPerPage}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
}
|
||||
const res = await networkService.getChecksByMonitor(
|
||||
authToken,
|
||||
monitorId,
|
||||
"desc",
|
||||
null,
|
||||
dateRange,
|
||||
null,
|
||||
paginationController.page,
|
||||
paginationController.rowsPerPage
|
||||
);
|
||||
setChecks(res.data.data.checks);
|
||||
setChecksCount(res.data.data.checksCount);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
logger.error(error);
|
||||
}
|
||||
};
|
||||
fetchPage();
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Box, Skeleton, Stack, Typography, useTheme } from "@mui/material";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import axiosInstance from "../../../Utils/axiosConfig";
|
||||
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";
|
||||
@@ -17,7 +16,8 @@ import {
|
||||
formatDurationRounded,
|
||||
} from "../../../Utils/timeUtils";
|
||||
import "./index.css";
|
||||
import MonitorDetails60MinChart from "../../../Components/Charts/MonitorDetails60MinChart";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
|
||||
const StatBox = ({ title, value }) => {
|
||||
return (
|
||||
@@ -121,18 +121,19 @@ const DetailsPage = () => {
|
||||
|
||||
const fetchMonitor = useCallback(async () => {
|
||||
try {
|
||||
const res = await axiosInstance.get(
|
||||
`/monitors/stats/${monitorId}?dateRange=${dateRange}&numToDisplay=50&normalize=true`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
}
|
||||
const res = await networkService.getStatsByMonitorId(
|
||||
authToken,
|
||||
monitorId,
|
||||
null,
|
||||
null,
|
||||
dateRange,
|
||||
50,
|
||||
true
|
||||
);
|
||||
setMonitor(res.data.data);
|
||||
setMonitor(res?.data?.data ?? {});
|
||||
} catch (error) {
|
||||
console.error("Error fetching monitor of id: " + monitorId);
|
||||
navigate("/not-found");
|
||||
logger.error(error);
|
||||
navigate("/not-found", { replace: true });
|
||||
}
|
||||
}, [authToken, monitorId, navigate, dateRange]);
|
||||
|
||||
@@ -142,15 +143,15 @@ const DetailsPage = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCertificate = async () => {
|
||||
const res = await axiosInstance.get(
|
||||
`/monitors/certificate/${monitorId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
setCertificateExpiry(res.data.data.certificateDate);
|
||||
try {
|
||||
const res = await networkService.getCertificateExpiry(
|
||||
authToken,
|
||||
monitorId
|
||||
);
|
||||
setCertificateExpiry(res?.data?.data?.certificateDate ?? "N/A");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
fetchCertificate();
|
||||
}, [authToken, monitorId]);
|
||||
@@ -163,39 +164,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"
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
}
|
||||
.monitors p.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.monitors p.MuiTypography-root {
|
||||
color: var(--env-var-color-5);
|
||||
}
|
||||
.monitors .MuiStack-root > button:not(.MuiIconButton-root) {
|
||||
|
||||
@@ -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,180 @@ 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.
|
||||
@@ -52,7 +225,7 @@ const Host = ({ params }) => {
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
mr: "3px",
|
||||
mr: "5px",
|
||||
}}
|
||||
>
|
||||
<OpenInNewPage
|
||||
@@ -72,6 +245,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 +293,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 +342,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 +366,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} />,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -243,7 +375,7 @@ const Monitors = () => {
|
||||
let loading = monitorState.isLoading && monitorState.monitors.length === 0;
|
||||
|
||||
return (
|
||||
<Stack className="monitors" pt={theme.gap.xl} gap={theme.gap.large}>
|
||||
<Stack className="monitors" gap={theme.gap.large}>
|
||||
{loading ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
@@ -262,10 +394,11 @@ const Monitors = () => {
|
||||
{monitorState.monitors?.length !== 0 && (
|
||||
<Button
|
||||
level="primary"
|
||||
label="Create new monitor"
|
||||
label="Create monitor"
|
||||
onClick={() => {
|
||||
navigate("/monitors/create");
|
||||
}}
|
||||
sx={{ fontWeight: 500 }}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -303,10 +436,8 @@ const Monitors = () => {
|
||||
<ServerStatus title="Down" value={down} state="down" />
|
||||
<ServerStatus title="Paused" value={0} state="pause" />
|
||||
</Stack>
|
||||
<Stack
|
||||
gap={theme.gap.large}
|
||||
<Box
|
||||
p={theme.gap.lgplus}
|
||||
flex={1}
|
||||
border={1}
|
||||
borderColor={theme.palette.otherColors.graishWhite}
|
||||
backgroundColor={theme.palette.otherColors.white}
|
||||
@@ -314,7 +445,7 @@ const Monitors = () => {
|
||||
borderRadius: `${theme.shape.borderRadius}px`,
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" alignItems="center">
|
||||
<Stack direction="row" alignItems="center" mb={theme.gap.large}>
|
||||
<Typography component="h2">Current monitors</Typography>
|
||||
<Box className="current-monitors-counter">
|
||||
{monitorState.monitors.length}
|
||||
@@ -322,84 +453,11 @@ const Monitors = () => {
|
||||
{/* TODO - add search bar */}
|
||||
</Stack>
|
||||
<BasicTable data={data} paginated={true} />
|
||||
</Stack>{" "}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ import NotFoundSvg from "../../../src/assets/Images/sushi_404.svg";
|
||||
* So That why we're using JavaScript default parameters instead.
|
||||
*/
|
||||
const DefaultValue = {
|
||||
title: "We cannot find this page",
|
||||
title: "Oh no! You dropped your sushi!",
|
||||
desc: "Either the URL doesn’t exist, or you don’t have access to it.",
|
||||
};
|
||||
|
||||
@@ -27,20 +27,18 @@ const DefaultValue = {
|
||||
* />
|
||||
*
|
||||
* @param {Object} props - The component props.
|
||||
* @param {string} [props.title="We cannot find this page"] - The title of the error page.
|
||||
* @param {string} [props.title="Oh no! You dropped your sushi!"] - The title of the error page.
|
||||
* @param {string} [props.desc="Either the URL doesn’t exist, or you don’t have access to it."] - The description of the error page.
|
||||
* @returns {JSX.Element} The rendered error page component.
|
||||
*/
|
||||
const NotFound = ({ title = DefaultValue.title, desc = DefaultValue.desc }) => {
|
||||
const navigate = useNavigate();
|
||||
console.log("NOT FOUND");
|
||||
return (
|
||||
<Stack className="not-found-page" justifyContent="center">
|
||||
<Stack gap="20px" alignItems="center">
|
||||
<img src={NotFoundSvg} alt="404" style={{ maxHeight: "25rem" }} />
|
||||
<Typography component="h1">{title}</Typography>
|
||||
<Typography>{desc}</Typography>
|
||||
<img src={NotFoundSvg} alt="404" style={{ maxHeight: "25rem" }} />
|
||||
<Typography>Oh no! You dropped your sushi!</Typography>
|
||||
<Button
|
||||
label="Go to the main dashboard"
|
||||
level="primary"
|
||||
|
||||
@@ -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,10 @@ 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 { logger } from "../../../Utils/Logger";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
@@ -92,13 +93,13 @@ const PageSpeedConfigure = () => {
|
||||
useEffect(() => {
|
||||
const data = monitors.find((monitor) => monitor._id === monitorId);
|
||||
if (!data) {
|
||||
console.error("Error fetching pagespeed monitor of id: " + monitorId);
|
||||
navigate("/not-found");
|
||||
logger.error("Error fetching pagespeed monitor of id: " + monitorId);
|
||||
navigate("/not-found", { replace: true });
|
||||
}
|
||||
setMonitor({
|
||||
...data,
|
||||
});
|
||||
}, [monitorId]);
|
||||
}, [monitorId, monitors, navigate]);
|
||||
|
||||
const handleChange = (event, id) => {
|
||||
let { value } = event.target;
|
||||
@@ -147,37 +148,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 />}
|
||||
@@ -290,7 +279,7 @@ const PageSpeedConfigure = () => {
|
||||
id="notify-emails-list"
|
||||
placeholder="notifications@gmail.com"
|
||||
value=""
|
||||
onChange={() => console.log("disabled")}
|
||||
onChange={() => logger.warn("disabled")}
|
||||
error=""
|
||||
/>
|
||||
<Typography mt={theme.gap.small}>
|
||||
@@ -300,7 +289,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 +298,7 @@ const PageSpeedConfigure = () => {
|
||||
sx={{ px: theme.gap.large, mt: theme.gap.large }}
|
||||
/>
|
||||
</Stack>
|
||||
</form>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
<Modal
|
||||
@@ -358,7 +347,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,8 +10,9 @@ 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";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
|
||||
const CreatePageSpeed = () => {
|
||||
const theme = useTheme();
|
||||
@@ -97,34 +98,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>
|
||||
@@ -187,7 +176,7 @@ const CreatePageSpeed = () => {
|
||||
id="notify-emails-list"
|
||||
placeholder="notifications@gmail.com"
|
||||
value=""
|
||||
onChange={() => console.log("disabled")}
|
||||
onChange={() => logger.warn("disabled")}
|
||||
error=""
|
||||
/>
|
||||
<Typography mt={theme.gap.small}>
|
||||
@@ -197,7 +186,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 +195,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;
|
||||
}
|
||||
|
||||
@@ -9,28 +9,20 @@ import {
|
||||
formatDuration,
|
||||
formatDurationRounded,
|
||||
} from "../../../Utils/timeUtils";
|
||||
import axiosInstance from "../../../Utils/axiosConfig";
|
||||
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";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
|
||||
/**
|
||||
* 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 +31,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 +46,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 +84,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 +133,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.
|
||||
*
|
||||
@@ -186,25 +199,25 @@ const PageSpeedDetails = () => {
|
||||
useEffect(() => {
|
||||
const fetchMonitor = async () => {
|
||||
try {
|
||||
const res = await axiosInstance.get(
|
||||
`/monitors/stats/${monitorId}?sortOrder=desc&limit=50`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
}
|
||||
const res = await networkService.getStatsByMonitorId(
|
||||
authToken,
|
||||
monitorId,
|
||||
"desc",
|
||||
50,
|
||||
null,
|
||||
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);
|
||||
navigate("/not-found");
|
||||
logger.error(logger);
|
||||
navigate("/not-found", { replace: true });
|
||||
}
|
||||
};
|
||||
|
||||
fetchMonitor();
|
||||
}, [monitorId]);
|
||||
}, [monitorId, authToken, navigate]);
|
||||
|
||||
/**
|
||||
* Weight constants for different performance metrics.
|
||||
@@ -306,21 +319,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 +665,8 @@ const PageSpeedDetails = () => {
|
||||
);
|
||||
};
|
||||
|
||||
PageSpeedDetails.propTypes = {
|
||||
push: PropTypes.func,
|
||||
};
|
||||
|
||||
export default PageSpeedDetails;
|
||||
@@ -11,6 +11,7 @@ import "./index.css";
|
||||
import Button from "../../Components/Button";
|
||||
import { useNavigate } from "react-router";
|
||||
import { getLastChecked } from "../../Utils/monitorUtils";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const Card = ({ data }) => {
|
||||
const theme = useTheme();
|
||||
@@ -57,6 +58,10 @@ const Card = ({ data }) => {
|
||||
);
|
||||
};
|
||||
|
||||
Card.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a skeleton layout.
|
||||
*
|
||||
@@ -129,7 +134,7 @@ const PageSpeed = () => {
|
||||
);
|
||||
useEffect(() => {
|
||||
dispatch(getPageSpeedByUserId(authToken));
|
||||
}, []);
|
||||
}, [authToken, dispatch]);
|
||||
|
||||
// will show skeletons only on initial load
|
||||
// since monitor state is being added to redux persist, there's no reason to display skeletons on every render
|
||||
|
||||
@@ -4,7 +4,7 @@ import Button from "../../Components/Button";
|
||||
import Field from "../../Components/Inputs/Field";
|
||||
import Link from "../../Components/Link";
|
||||
import Select from "../../Components/Inputs/Select";
|
||||
|
||||
import { logger } from "../../Utils/Logger";
|
||||
import "./index.css";
|
||||
|
||||
const Settings = () => {
|
||||
@@ -40,14 +40,14 @@ const Settings = () => {
|
||||
id="display-timezone"
|
||||
label="Display timezone"
|
||||
value="est"
|
||||
onChange={() => console.log("disabled")}
|
||||
onChange={() => logger.warn("disabled")}
|
||||
items={[{ _id: "est", name: "America / Toronto" }]}
|
||||
/>
|
||||
<Select
|
||||
id="server-timezone"
|
||||
label="Server timezone"
|
||||
value="est"
|
||||
onChange={() => console.log("disabled")}
|
||||
onChange={() => logger.warn("disabled")}
|
||||
items={[{ _id: "est", name: "America / Toronto" }]}
|
||||
/>
|
||||
</Stack>
|
||||
@@ -74,7 +74,7 @@ const Settings = () => {
|
||||
optionalLabel="0 for infinite"
|
||||
placeholder="90"
|
||||
value=""
|
||||
onChange={() => console.log("Disabled")}
|
||||
onChange={() => logger.warn("Disabled")}
|
||||
/>
|
||||
<Box>
|
||||
<Typography>Clear all stats. This is irreversible.</Typography>
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import Fallback from "../../Components/Fallback";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
const Status = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div className="status">
|
||||
<Fallback
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
const LOG_LEVEL = import.meta.env.VITE_APP_LOG_LEVEL;
|
||||
class Logger {
|
||||
constructor(logLevel) {
|
||||
const NO_OP = () => {};
|
||||
|
||||
if (logLevel === "none") {
|
||||
this.error = NO_OP;
|
||||
this.warn = NO_OP;
|
||||
this.log = NO_OP;
|
||||
return;
|
||||
}
|
||||
|
||||
this.error = console.error.bind(console);
|
||||
|
||||
if (logLevel === "error") {
|
||||
this.warn = NO_OP;
|
||||
this.log = NO_OP;
|
||||
return;
|
||||
}
|
||||
this.warn = console.warn.bind(console);
|
||||
|
||||
if (logLevel === "warn") {
|
||||
this.log = NO_OP;
|
||||
return;
|
||||
}
|
||||
this.log = console.log.bind(console);
|
||||
}
|
||||
}
|
||||
|
||||
export const logger = new Logger(LOG_LEVEL);
|
||||
@@ -0,0 +1,432 @@
|
||||
import axios from "axios";
|
||||
const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL;
|
||||
import { logger } from "./Logger";
|
||||
class NetworkService {
|
||||
constructor(store) {
|
||||
this.store = store;
|
||||
this.axiosInstance = axios.create({ baseURL: BASE_URL });
|
||||
this.axiosInstance.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
logger.error(error);
|
||||
if (error.response && error.response.status === 401) {
|
||||
logger.error("Invalid token received");
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* ************************************
|
||||
* Create a new monitor
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {string} authToken - The authorization token to be used in the request header.
|
||||
* @param {Object} monitor - The monitor object to be sent in the request body.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
|
||||
*/
|
||||
async createMonitor(authToken, monitor) {
|
||||
return this.axiosInstance.post(`/monitors`, monitor, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Get all uptime monitors for a user
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {string} authToken - The authorization token to be used in the request header.
|
||||
* @param {string} userId - The ID of the user whose monitors are to be retrieved.
|
||||
* @param {number} [limit] - The maximum number of monitors to retrieve.
|
||||
* @param {Array<string>} [types] - The types of monitors to retrieve.
|
||||
* @param {string} [status] - The status of the monitors to retrieve.
|
||||
* @param {string} [sortOrder] - The order in which to sort the retrieved monitors.
|
||||
* @param {boolean} [normalize] - Whether to normalize the retrieved monitors.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios GET request.
|
||||
*/
|
||||
async getMonitorsByUserId(
|
||||
authToken,
|
||||
userId,
|
||||
limit,
|
||||
types,
|
||||
status,
|
||||
sortOrder,
|
||||
normalize
|
||||
) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (limit) params.append("limit", limit);
|
||||
if (types) {
|
||||
types.forEach((type) => {
|
||||
params.append("type", type);
|
||||
});
|
||||
}
|
||||
if (status) params.append("status", status);
|
||||
if (sortOrder) params.append("sortOrder", sortOrder);
|
||||
if (normalize) params.append("normalize", normalize);
|
||||
|
||||
return this.axiosInstance.get(
|
||||
`/monitors/user/${userId}?${params.toString()}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Get stats for a monitor
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {string} authToken - The authorization token to be used in the request header.
|
||||
* @param {string} monitorId - The ID of the monitor whose statistics are to be retrieved.
|
||||
* @param {string} [sortOrder] - The order in which to sort the retrieved statistics.
|
||||
* @param {number} [limit] - The maximum number of statistics to retrieve.
|
||||
* @param {string} [dateRange] - The date range for which to retrieve statistics.
|
||||
* @param {number} [numToDisplay] - The number of statistics to display.
|
||||
* @param {boolean} [normalize] - Whether to normalize the retrieved statistics.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios GET request.
|
||||
*/
|
||||
async getStatsByMonitorId(
|
||||
authToken,
|
||||
monitorId,
|
||||
sortOrder,
|
||||
limit,
|
||||
dateRange,
|
||||
numToDisplay,
|
||||
normalize
|
||||
) {
|
||||
const params = new URLSearchParams();
|
||||
if (sortOrder) params.append("sortOrder", sortOrder);
|
||||
if (limit) params.append("limit", limit);
|
||||
if (dateRange) params.append("dateRange", dateRange);
|
||||
if (numToDisplay) params.append("numToDisplay", numToDisplay);
|
||||
if (normalize) params.append("normalize", normalize);
|
||||
|
||||
return this.axiosInstance.get(
|
||||
`/monitors/stats/${monitorId}?${params.toString()}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Updates a single monitor
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {string} authToken - The authorization token to be used in the request header.
|
||||
* @param {string} monitorId - The ID of the monitor to be updated.
|
||||
* @param {Object} updatedFields - The fields to be updated for the monitor.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios PUT request.
|
||||
*/
|
||||
async updateMonitor(authToken, monitorId, updatedFields) {
|
||||
return this.axiosInstance.put(`/monitors/${monitorId}`, updatedFields, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Deletes a single monitor by its ID
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {string} authToken - The authorization token to be used in the request header.
|
||||
* @param {string} monitorId - The ID of the monitor to be deleted.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios DELETE request.
|
||||
*/
|
||||
async deleteMonitorById(authToken, monitorId) {
|
||||
return this.axiosInstance.delete(`/monitors/${monitorId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Gets the certificate expiry for a monitor
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {string} authToken - The authorization token to be used in the request header.
|
||||
* @param {string} monitorId - The ID of the monitor whose certificate expiry is to be retrieved.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios GET request.
|
||||
*
|
||||
*/
|
||||
async getCertificateExpiry(authToken, monitorId) {
|
||||
return this.axiosInstance.get(`/monitors/certificate/${monitorId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Registers a new user
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {Object} form - The form data for the new user to be registered.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
|
||||
*/
|
||||
async registerUser(form) {
|
||||
return this.axiosInstance.post(`/auth/register`, form);
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Logs in a user
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {Object} form - The form data for the user to be logged in.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
|
||||
*
|
||||
*/
|
||||
async loginUser(form) {
|
||||
return this.axiosInstance.post(`/auth/login`, form);
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Updates a user
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {string} authToken - The authorization token to be used in the request header.
|
||||
* @param {string} userId - The ID of the user to be updated.
|
||||
* @param {Object} form - The form data for the user to be updated.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios PUT request.
|
||||
*
|
||||
*/
|
||||
async updateUser(authToken, userId, form) {
|
||||
return this.axiosInstance.put(`/auth/user/${userId}`, form, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Forgot password request
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {Object} form - The form data for the password recovery request.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
|
||||
*
|
||||
*/
|
||||
async forgotPassword(form) {
|
||||
return this.axiosInstance.post(`/auth/recovery/request`, form);
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Validates a recovery token
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {string} recoveryToken - The recovery token to be validated.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
|
||||
*
|
||||
*/
|
||||
async validateRecoveryToken(recoveryToken) {
|
||||
return this.axiosInstance.post("/auth/recovery/validate", {
|
||||
recoveryToken,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Requests password recovery
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {Object} form - The form data for the password recovery request.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
|
||||
*
|
||||
*/
|
||||
async setNewPassword(recoveryToken, form) {
|
||||
return this.axiosInstance.post("/auth/recovery/reset", {
|
||||
...form,
|
||||
recoveryToken,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Checks if an admin user exists
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios GET request.
|
||||
*
|
||||
*/
|
||||
async doesAdminExist() {
|
||||
return this.axiosInstance.get("/auth/users/admin");
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Get all users
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {string} authToken - The authorization token to be used in the request header.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios GET request.
|
||||
*
|
||||
*/
|
||||
async getAllUsers(authToken) {
|
||||
return this.axiosInstance.get("/auth/users", {
|
||||
headers: { Authorization: `Bearer ${authToken}` },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Requests an invitation token
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {string} authToken - The authorization token to be used in the request header.
|
||||
* @param {string} email - The email of the user to be invited.
|
||||
* @param {string} role - The role of the user to be invited.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
|
||||
*
|
||||
*/
|
||||
async requestInvitationToken(authToken, email, role) {
|
||||
return this.axiosInstance.post(
|
||||
`/auth/invite`,
|
||||
{ email, role },
|
||||
{
|
||||
headers: { Authorization: `Bearer ${authToken}` },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Verifies an invitation token
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {string} token - The invitation token to be verified.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
|
||||
*
|
||||
*/
|
||||
async verifyInvitationToken(token) {
|
||||
return this.axiosInstance.post(`/auth/invite/verify`, {
|
||||
token,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Get all checks for a given monitor
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {string} authToken - The authorization token to be used in the request header.
|
||||
* @param {string} monitorId - The ID of the monitor.
|
||||
* @param {string} sortOrder - The order in which to sort the checks.
|
||||
* @param {number} limit - The maximum number of checks to retrieve.
|
||||
* @param {string} dateRange - The range of dates for which to retrieve checks.
|
||||
* @param {string} filter - The filter to apply to the checks.
|
||||
* @param {number} page - The page number to retrieve in a paginated list.
|
||||
* @param {number} rowsPerPage - The number of rows per page in a paginated list.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios GET request.
|
||||
*
|
||||
*/
|
||||
|
||||
async getChecksByMonitor(
|
||||
authToken,
|
||||
monitorId,
|
||||
sortOrder,
|
||||
limit,
|
||||
dateRange,
|
||||
filter,
|
||||
page,
|
||||
rowsPerPage
|
||||
) {
|
||||
const params = new URLSearchParams();
|
||||
if (sortOrder) params.append("sortOrder", sortOrder);
|
||||
if (limit) params.append("limit", limit);
|
||||
if (dateRange) params.append("dateRange", dateRange);
|
||||
if (filter) params.append("filter", filter);
|
||||
if (page) params.append("page", page);
|
||||
if (rowsPerPage) params.append("rowsPerPage", rowsPerPage);
|
||||
|
||||
return this.axiosInstance.get(`/checks/${monitorId}?${params.toString()}`, {
|
||||
headers: { Authorization: `Bearer ${authToken}` },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Get all checks for a given user
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {string} authToken - The authorization token to be used in the request header.
|
||||
* @param {string} userId - The ID of the user.
|
||||
* @param {string} sortOrder - The order in which to sort the checks.
|
||||
* @param {number} limit - The maximum number of checks to retrieve.
|
||||
* @param {string} dateRange - The range of dates for which to retrieve checks.
|
||||
* @param {string} filter - The filter to apply to the checks.
|
||||
* @param {number} page - The page number to retrieve in a paginated list.
|
||||
* @param {number} rowsPerPage - The number of rows per page in a paginated list.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios GET request.
|
||||
*
|
||||
*/
|
||||
async getChecksByUser(
|
||||
authToken,
|
||||
userId,
|
||||
sortOrder,
|
||||
limit,
|
||||
dateRange,
|
||||
filter,
|
||||
page,
|
||||
rowsPerPage
|
||||
) {
|
||||
const params = new URLSearchParams();
|
||||
if (sortOrder) params.append("sortOrder", sortOrder);
|
||||
if (limit) params.append("limit", limit);
|
||||
if (dateRange) params.append("dateRange", dateRange);
|
||||
if (filter) params.append("filter", filter);
|
||||
if (page) params.append("page", page);
|
||||
if (rowsPerPage) params.append("rowsPerPage", rowsPerPage);
|
||||
return this.axiosInstance.get(
|
||||
`/checks/user/${userId}?${params.toString()}`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${authToken}` },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default NetworkService;
|
||||
@@ -1,27 +0,0 @@
|
||||
import axios from "axios";
|
||||
import { clearAuthState } from "../Features/Auth/authSlice";
|
||||
const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL;
|
||||
|
||||
let store;
|
||||
|
||||
export const injectStore = (s) => {
|
||||
store = s;
|
||||
};
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: BASE_URL,
|
||||
});
|
||||
|
||||
axiosInstance.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
console.error(error);
|
||||
if (error.response && error.response.status === 401) {
|
||||
console.log("Invalid token revoked");
|
||||
store.dispatch(clearAuthState());
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default axiosInstance;
|
||||
@@ -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="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 17L13 12L18 7M11 17L6 12L11 7" stroke="#667085" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 232 B |
@@ -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="M6 17L11 12L6 7M13 17L18 12L13 7" stroke="#667085" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 231 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 |
@@ -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="M7 15L12 20L17 15M7 9L12 4L17 9" stroke="#667085" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 230 B |
@@ -22,7 +22,7 @@
|
||||
--env-var-font-familt-1: "Roboto", "Helvetica", "Arial", sans-serif;
|
||||
|
||||
--env-var-color-0: #fff;
|
||||
--env-var-color-1: #101828;
|
||||
--env-var-color-1: #1c2130;
|
||||
--env-var-color-2: #475467;
|
||||
--env-var-color-2-light: #838c99;
|
||||
--env-var-color-3: #1570ef;
|
||||
@@ -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,10 +8,8 @@ import { ThemeProvider } from "@mui/material";
|
||||
import { Provider } from "react-redux";
|
||||
import { persistor, store } from "./store";
|
||||
import { PersistGate } from "redux-persist/integration/react";
|
||||
import { injectStore } from "./Utils/axiosConfig.js";
|
||||
|
||||
injectStore(store);
|
||||
|
||||
import NetworkService from "./Utils/NetworkService.js";
|
||||
export const networkService = new NetworkService(store);
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { configureStore, combineReducers } from "@reduxjs/toolkit";
|
||||
import uptimeMonitorsReducer from "./Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import pageSpeedMonitorReducer from "./Features/PageSpeedMonitor/pageSpeedMonitorSlice";
|
||||
import authReducer from "./Features/Auth/authSlice";
|
||||
import uiReducer from "./Features/UI/uiSlice";
|
||||
import storage from "redux-persist/lib/storage";
|
||||
import { persistReducer, persistStore, createTransform } from "redux-persist";
|
||||
|
||||
@@ -20,7 +21,7 @@ const authTransform = createTransform(
|
||||
const persistConfig = {
|
||||
key: "root",
|
||||
storage,
|
||||
whitielist: ["auth", "monitors", "pageSpeed"],
|
||||
whitielist: ["auth", "monitors", "pageSpeed", "ui"],
|
||||
transforms: [authTransform],
|
||||
};
|
||||
|
||||
@@ -28,6 +29,7 @@ const rootReducer = combineReducers({
|
||||
uptimeMonitors: uptimeMonitorsReducer,
|
||||
auth: authReducer,
|
||||
pageSpeedMonitors: pageSpeedMonitorReducer,
|
||||
ui: uiReducer,
|
||||
});
|
||||
|
||||
const persistedReducer = persistReducer(persistConfig, rootReducer);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<p align="center"><strong>An open source server monitoring application</strong></p>
|
||||
|
||||

|
||||

|
||||
|
||||
BlueWave Uptime is an open source server monitoring application used to track the operational status and performance of servers and websites. It regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time.
|
||||
|
||||
@@ -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,15 @@ 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
|
||||
@@ -180,6 +190,7 @@ SYSTEM_EMAIL_PASSWORD=<system_email_password>
|
||||
|
||||
```
|
||||
VITE_APP_API_BASE_URL="http://localhost:5000/api/v1"
|
||||
VITE_APP_API_LOG_LEVEL="debug"
|
||||
```
|
||||
|
||||
4. In the `Docker` directory run `docker compose up` to run the `docker-compose.yaml` file and start all four images.
|
||||
@@ -200,9 +211,10 @@ That's it, the application is ready to use on port 80.
|
||||
|
||||
##### Environmental Variables <a id="env-vars-client"></a>
|
||||
|
||||
| ENV Variable Name | Required/Optional | Type | Description | Accepted Values |
|
||||
| --------------------- | ----------------- | -------- | ------------------ | --------------- |
|
||||
| VITE_APP_API_BASE_URL | Required | `string` | Base URL of server | {host}/api/v1 |
|
||||
| ENV Variable Name | Required/Optional | Type | Description | Accepted Values |
|
||||
| --------------------- | ----------------- | -------- | ------------------ | ---------------------------------- |
|
||||
| VITE_APP_API_BASE_URL | Required | `string` | Base URL of server | {host}/api/v1 |
|
||||
| VITE_APP_LOG_LEVEL | Optional | `string` | Log level | `"none"`\|`"error"` \| `"warn"` \| |
|
||||
|
||||
<br/>
|
||||
|
||||
|
||||
@@ -450,6 +450,7 @@ const deleteUserController = async (req, res, next) => {
|
||||
}
|
||||
|
||||
// 1. Find all the monitors associated with the user id
|
||||
|
||||
const monitors = await req.db.getMonitorsByUserId({
|
||||
params: { userId: _id },
|
||||
});
|
||||
@@ -459,6 +460,7 @@ const deleteUserController = async (req, res, next) => {
|
||||
await Promise.all(
|
||||
monitors.map(async (monitor) => {
|
||||
await req.jobQueue.deleteJob(monitor);
|
||||
|
||||
await req.db.deleteChecks(monitor._id);
|
||||
await req.db.deleteAlertByMonitorId(monitor._id);
|
||||
await req.db.deletePageSpeedChecksByMonitorId(monitor._id);
|
||||
|
||||
@@ -303,7 +303,8 @@ const editMonitor = async (req, res, next) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const monitorBeforeEdit = await req.db.getMonitorById(req, res);
|
||||
const { monitorId } = req.params;
|
||||
const monitorBeforeEdit = await req.db.getMonitorById(monitorId);
|
||||
|
||||
// Get notifications from the request body
|
||||
const notifications = req.body.notifications;
|
||||
|
||||
@@ -172,6 +172,9 @@ const getMonitorStatsById = async (req) => {
|
||||
|
||||
// Get monitor
|
||||
const monitor = await Monitor.findById(monitorId);
|
||||
if (monitor === null || monitor === undefined) {
|
||||
throw new Error(errorMessages.DB_FIND_MONTIOR_BY_ID(monitorId));
|
||||
}
|
||||
|
||||
// Determine if this is a pagespeed monitor or an http/ping monitor
|
||||
let model =
|
||||
@@ -260,10 +263,9 @@ const getMonitorStatsById = async (req) => {
|
||||
monitorStats.latestResponseTime = getLatestResponseTime(checksAll);
|
||||
monitorStats.incidents = incidents;
|
||||
monitorStats.checks = dateRangeChecks;
|
||||
|
||||
return monitorStats;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -294,7 +296,7 @@ const getMonitorById = async (monitorId) => {
|
||||
*/
|
||||
const getMonitorsByUserId = async (req, res) => {
|
||||
try {
|
||||
let { limit, type, status, sortOrder, normalize } = req.query;
|
||||
let { limit, type, status, sortOrder, normalize } = req.query || {};
|
||||
const monitorQuery = { userId: req.params.userId };
|
||||
|
||||
if (type !== undefined) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const UserModel = require("../../../models/user");
|
||||
const RecoveryToken = require("../../../models/RecoveryToken");
|
||||
const crypto = require("crypto");
|
||||
const { errorMessages } = require("../../../utils/messages");
|
||||
|
||||
@@ -19,7 +19,7 @@ const JobQueue = require("./service/jobQueue");
|
||||
const NetworkService = require("./service/networkService");
|
||||
const EmailService = require("./service/emailService");
|
||||
const PageSpeedService = require("./service/pageSpeedService");
|
||||
|
||||
const SERVICE_NAME = "Server";
|
||||
let cleaningUp = false;
|
||||
|
||||
// Need to wrap server setup in a function to handle async nature of JobQueue
|
||||
|
||||
@@ -15,12 +15,12 @@ router.get(
|
||||
checkController.getChecks
|
||||
);
|
||||
|
||||
router.get("/user/:userId", checkController.getUserChecks);
|
||||
|
||||
router.delete(
|
||||
"/:monitorId",
|
||||
verifyOwnership(Monitor, "monitorId"),
|
||||
checkController.deleteChecks
|
||||
);
|
||||
|
||||
router.get("/user/:userId", checkController.getUserChecks);
|
||||
|
||||
module.exports = router;
|
||||
@@ -16,6 +16,14 @@ class NetworkService {
|
||||
async handleNotification(job, isAlive) {
|
||||
const { _id } = job.data;
|
||||
const monitor = await this.db.getMonitorById(_id);
|
||||
if (monitor === null || monitor === undefined) {
|
||||
logger.error(`Null Monitor: ${_id}`, {
|
||||
method: "handleNotification",
|
||||
service: this.SERVICE_NAME,
|
||||
jobId: job.id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If monitor status changes, update monitor status and send notification
|
||||
if (monitor.status !== isAlive) {
|
||||
|
||||
@@ -40,7 +40,7 @@ const NormalizeData = (checks, rangeMin, rangeMax) => {
|
||||
return normalizedChecks;
|
||||
} else {
|
||||
return checks.map((check) => {
|
||||
return { ...check, originalResponseTime: check.responseTime };
|
||||
return { ...check._doc, originalResponseTime: check.responseTime };
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||