Merge branch 'develop' into HEAD
@@ -39,6 +39,7 @@ function App() {
|
||||
const DetailsWithAdminProp = withAdminProp(Details);
|
||||
const PageSpeedWithAdminProp = withAdminProp(PageSpeed);
|
||||
const MaintenanceWithAdminProp = withAdminProp(Maintenance);
|
||||
const SettingsWithAdminProp = withAdminProp(Settings);
|
||||
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
|
||||
@@ -90,7 +91,7 @@ function App() {
|
||||
/>
|
||||
<Route
|
||||
path="settings"
|
||||
element={<ProtectedRoute Component={Settings} />}
|
||||
element={<ProtectedRoute Component={SettingsWithAdminProp} />}
|
||||
/>
|
||||
<Route
|
||||
path="account/profile"
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, IconButton, Stack, Typography } from "@mui/material";
|
||||
import { Box, Button, IconButton, Stack, Typography } from "@mui/material";
|
||||
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
||||
import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined";
|
||||
import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import "./index.css";
|
||||
import Button from "../Button";
|
||||
|
||||
/**
|
||||
* Icons mapping for different alert variants.
|
||||
@@ -32,7 +31,7 @@ const icons = {
|
||||
|
||||
const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => {
|
||||
const theme = useTheme();
|
||||
const { text, light, border } = theme.palette[variant];
|
||||
const { text, bg, border } = theme.palette[variant];
|
||||
const icon = icons[variant];
|
||||
|
||||
return (
|
||||
@@ -46,7 +45,7 @@ const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => {
|
||||
padding: hasIcon
|
||||
? theme.spacing(8)
|
||||
: `${theme.spacing(4)} ${theme.spacing(8)}`,
|
||||
backgroundColor: light,
|
||||
backgroundColor: bg,
|
||||
border: `solid 1px ${border}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
}}
|
||||
@@ -65,8 +64,8 @@ const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => {
|
||||
)}
|
||||
{hasIcon && isToast && (
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Dismiss"
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
fontWeight: "600",
|
||||
@@ -74,15 +73,10 @@ const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => {
|
||||
mt: theme.spacing(4),
|
||||
padding: 0,
|
||||
minWidth: 0,
|
||||
"&:hover": {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
"& .MuiTouchRipple-root": {
|
||||
pointerEvents: "none",
|
||||
display: "none",
|
||||
},
|
||||
}}
|
||||
></Button>
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
{isToast && (
|
||||
|
||||
@@ -41,8 +41,8 @@ const PulseDot = ({ color }) => {
|
||||
"&::after": {
|
||||
content: `""`,
|
||||
position: "absolute",
|
||||
width: "6px",
|
||||
height: "6px",
|
||||
width: "7px",
|
||||
height: "7px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "white",
|
||||
top: "50%",
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Box,
|
||||
Typography,
|
||||
Stack,
|
||||
Button,
|
||||
} from "@mui/material";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setRowsPerPage } from "../../Features/UI/uiSlice";
|
||||
@@ -21,7 +22,6 @@ import RightArrowDouble from "../../assets/icons/right-arrow-double.svg?react";
|
||||
import LeftArrow from "../../assets/icons/left-arrow.svg?react";
|
||||
import RightArrow from "../../assets/icons/right-arrow.svg?react";
|
||||
import SelectorVertical from "../../assets/icons/selector-vertical.svg?react";
|
||||
import Button from "../Button";
|
||||
import "./index.css";
|
||||
/**
|
||||
* Component for pagination actions (first, previous, next, last).
|
||||
@@ -54,37 +54,37 @@ const TablePaginationActions = (props) => {
|
||||
return (
|
||||
<Box sx={{ flexShrink: 0, ml: "24px" }}>
|
||||
<Button
|
||||
level="secondary"
|
||||
label=""
|
||||
img={<LeftArrowDouble />}
|
||||
variant="group"
|
||||
onClick={handleFirstPageButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="first page"
|
||||
/>
|
||||
>
|
||||
<LeftArrowDouble />
|
||||
</Button>
|
||||
<Button
|
||||
level="secondary"
|
||||
label=""
|
||||
img={<LeftArrow />}
|
||||
variant="group"
|
||||
onClick={handleBackButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="previous page"
|
||||
/>
|
||||
>
|
||||
<LeftArrow />
|
||||
</Button>
|
||||
<Button
|
||||
level="secondary"
|
||||
label=""
|
||||
img={<RightArrow />}
|
||||
variant="group"
|
||||
onClick={handleNextButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="next page"
|
||||
/>
|
||||
>
|
||||
<RightArrow />
|
||||
</Button>
|
||||
<Button
|
||||
level="secondary"
|
||||
label=""
|
||||
img={<RightArrowDouble />}
|
||||
variant="group"
|
||||
onClick={handleLastPageButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="last page"
|
||||
/>
|
||||
>
|
||||
<RightArrowDouble />
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -200,31 +200,12 @@ const BasicTable = ({ data, paginated, reversed, table }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableContainer
|
||||
component={Paper}
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.main,
|
||||
border: `solid 1px ${theme.palette.border.light}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
}}
|
||||
>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
}}
|
||||
>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{data.cols.map((col) => (
|
||||
<TableCell
|
||||
key={col.id}
|
||||
sx={{
|
||||
color: theme.palette.text.secondary,
|
||||
borderBottomColor: theme.palette.border.light,
|
||||
}}
|
||||
>
|
||||
{col.name}
|
||||
</TableCell>
|
||||
<TableCell key={col.id}>{col.name}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
@@ -242,17 +223,7 @@ const BasicTable = ({ data, paginated, reversed, table }) => {
|
||||
onClick={row.handleClick ? row.handleClick : null}
|
||||
>
|
||||
{row.data.map((cell) => {
|
||||
return (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
sx={{
|
||||
color: theme.palette.text.secondary,
|
||||
borderBottomColor: theme.palette.border.light,
|
||||
}}
|
||||
>
|
||||
{cell.data}
|
||||
</TableCell>
|
||||
);
|
||||
return <TableCell key={cell.id}>{cell.data}</TableCell>;
|
||||
})}
|
||||
</TableRow>
|
||||
);
|
||||
@@ -322,18 +293,14 @@ const BasicTable = ({ data, paginated, reversed, table }) => {
|
||||
sx={{
|
||||
mt: theme.spacing(6),
|
||||
color: theme.palette.text.secondary,
|
||||
"& button.MuiButtonBase-root, & .MuiSelect-select": {
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
"& svg path": {
|
||||
stroke: theme.palette.text.tertiary,
|
||||
strokeWidth: 1.3,
|
||||
},
|
||||
"& button:not(.Mui-disabled):hover": {
|
||||
backgroundColor: theme.palette.background.fill,
|
||||
borderColor: theme.palette.background.fill,
|
||||
"& .MuiSelect-select": {
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
.MuiButtonBase-root.button-rotate90 svg {
|
||||
transition: transform 500ms ease;
|
||||
}
|
||||
.MuiButtonBase-root.button-rotate90:hover svg {
|
||||
transform: rotate(90deg);
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.MuiButtonBase-root.button-rotate180 svg {
|
||||
transition: transform 700ms ease;
|
||||
}
|
||||
.MuiButtonBase-root.button-rotate180:hover svg {
|
||||
transform: rotate(180deg);
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.MuiButtonBase-root.button-slideLeft {
|
||||
padding-left: 45px;
|
||||
}
|
||||
.MuiButtonBase-root.button-slideLeft svg {
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
transition: left 250ms cubic-bezier(0.65, 0, 0.076, 1);
|
||||
}
|
||||
.MuiButtonBase-root.button-slideLeft:hover svg {
|
||||
left: 11px;
|
||||
}
|
||||
|
||||
.MuiButtonBase-root.button-slideLeft:hover,
|
||||
.MuiButtonBase-root.button-rotate180:hover,
|
||||
.MuiButtonBase-root.button-rotate90:hover {
|
||||
background-color: rgba(71, 84, 103, 0.08);
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import { Button as MuiButton } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
import "./index.css";
|
||||
|
||||
const levelConfig = {
|
||||
primary: {
|
||||
variant: "contained",
|
||||
color: "primary",
|
||||
},
|
||||
secondary: {
|
||||
variant: "outlined",
|
||||
color: "secondary",
|
||||
},
|
||||
tertiary: {
|
||||
variant: "text",
|
||||
color: "tertiary",
|
||||
},
|
||||
error: {
|
||||
variant: "contained",
|
||||
color: "error",
|
||||
},
|
||||
imagePrimary: {
|
||||
color: "primary",
|
||||
variant: "text",
|
||||
},
|
||||
imageSecondary: {
|
||||
color: "secondary",
|
||||
variant: "text",
|
||||
},
|
||||
imageTertiary: {
|
||||
color: "tertiary",
|
||||
variant: "text",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @component
|
||||
* @param {Object} props
|
||||
* @param {'primary' | 'secondary' | 'tertiary' | 'error' | 'imagePrimary' | 'imageSecondary' | 'imageTertiary'} props.level - The level of the button
|
||||
* @param {string} props.type - The type of the button
|
||||
* @param {string} props.label - The label of the button
|
||||
* @param {React.ReactNode} props.img - Image for button
|
||||
* @param {boolean} [props.disabled] - Whether the button is disabled
|
||||
* @param {function} props.onClick - Function to run when the button is clicked
|
||||
* @param {Object} props.sx - Styles for the button
|
||||
* @returns {JSX.Element}
|
||||
* @example
|
||||
* // Render an error button
|
||||
* <Button type="submit" level="error" label="Error" disabled sx={{marginTop: "1rem"}}/>
|
||||
*/
|
||||
|
||||
const Button = ({
|
||||
id,
|
||||
animate,
|
||||
type,
|
||||
level,
|
||||
label,
|
||||
disabled,
|
||||
img,
|
||||
onClick,
|
||||
props,
|
||||
sx,
|
||||
}) => {
|
||||
const { variant, color } = levelConfig[level];
|
||||
return (
|
||||
<MuiButton
|
||||
id={id}
|
||||
className={`button-${animate}`}
|
||||
type={type}
|
||||
variant={variant}
|
||||
color={color}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
disableRipple
|
||||
sx={{
|
||||
lineHeight: 1.5,
|
||||
fontWeight: 400,
|
||||
boxShadow: "none",
|
||||
textTransform: "none",
|
||||
borderRadius: "4px",
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"&:hover": {
|
||||
boxShadow: "none",
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{img && img}
|
||||
<span>{label}</span>
|
||||
</MuiButton>
|
||||
);
|
||||
};
|
||||
|
||||
Button.propTypes = {
|
||||
id: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
level: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
img: PropTypes.node,
|
||||
onClick: PropTypes.func,
|
||||
sx: PropTypes.object,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Button;
|
||||
@@ -1,105 +0,0 @@
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const levelConfig = {
|
||||
primary: {
|
||||
variant: "contained",
|
||||
color: "primary",
|
||||
},
|
||||
secondary: {
|
||||
variant: "outlined",
|
||||
color: "secondary",
|
||||
},
|
||||
tertiary: {
|
||||
variant: "text",
|
||||
color: "tertiary",
|
||||
},
|
||||
error: {
|
||||
variant: "contained",
|
||||
color: "error",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @component
|
||||
* @param {Object} props
|
||||
* @param {'primary' | 'secondary' | 'tertiary' | 'error'} props.level - The style level of the button.
|
||||
* @param {string} props.label - The label text displayed on the button.
|
||||
* @param {React.ReactNode} [props.img] - Icon or image element to display within the button.
|
||||
* @param {boolean} [props.disabled=false] - Determines if the button is disabled.
|
||||
* @param {string} [props.loadingText] - Text displayed when the button is in a loading state.
|
||||
* @param {'start' | 'end'} [props.position] - Specifies where the icon or loading loadingText should be positioned relative to the label.
|
||||
* @param {function} props.onClick - Callback function invoked when the button is clicked.
|
||||
* @param {boolean} props.isLoading - Indicates if the button is in a loading state.
|
||||
* @param {Object} [props.sx] - Additional styles to apply to the button.
|
||||
* @returns {JSX.Element}
|
||||
* @example
|
||||
* // Render a primary button with an icon at the end
|
||||
* <ButtonSpinner
|
||||
* level="primary"
|
||||
* label="Save"
|
||||
* img={<SaveIcon />}
|
||||
* position="end"
|
||||
* onClick={handleSaveClick}
|
||||
* isLoading={loading}
|
||||
* />
|
||||
*/
|
||||
|
||||
const ButtonSpinner = ({
|
||||
level,
|
||||
label,
|
||||
disabled,
|
||||
loadingText,
|
||||
position,
|
||||
img,
|
||||
onClick,
|
||||
isLoading,
|
||||
sx,
|
||||
}) => {
|
||||
const { variant, color } = levelConfig[level];
|
||||
//if both a loadingPosition and loadingIndicator are provided, the spinner overlaps with the text and it breaks
|
||||
if (position && loadingText) loadingText = null;
|
||||
return (
|
||||
<LoadingButton
|
||||
variant={variant}
|
||||
color={color}
|
||||
disabled={disabled}
|
||||
loadingIndicator={loadingText}
|
||||
loadingPosition={position}
|
||||
startIcon={position === "start" && img}
|
||||
endIcon={position === "end" && img}
|
||||
onClick={onClick}
|
||||
loading={isLoading}
|
||||
disableRipple
|
||||
sx={{
|
||||
boxShadow: "none",
|
||||
textTransform: "none",
|
||||
borderRadius: "4px",
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"&:hover": {
|
||||
boxShadow: "none",
|
||||
transition: "none",
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<span>{label}</span>
|
||||
</LoadingButton>
|
||||
);
|
||||
};
|
||||
|
||||
ButtonSpinner.propTypes = {
|
||||
level: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
loadingText: PropTypes.string,
|
||||
position: PropTypes.oneOf(["start", "end"]),
|
||||
img: PropTypes.node,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
sx: PropTypes.object,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default ButtonSpinner;
|
||||
@@ -1,53 +0,0 @@
|
||||
import { BarChart, Bar, Cell, ReferenceLine, Label } from "recharts";
|
||||
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
const MonitorDetails60MinChart = ({ data }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const labelStyle = {
|
||||
fontSize: "10px",
|
||||
fill: theme.palette.text.tertiary,
|
||||
};
|
||||
|
||||
const color = {
|
||||
true: theme.palette.success.main,
|
||||
false: theme.palette.error.text,
|
||||
undefined: theme.palette.unresolved.main,
|
||||
};
|
||||
return (
|
||||
<BarChart
|
||||
width={data.length * 10 + 30}
|
||||
height={35}
|
||||
data={data}
|
||||
margin={{ top: 14, left: 15, right: 15, bottom: 2 }}
|
||||
style={{ alignSelf: "baseline" }}
|
||||
>
|
||||
<Bar dataKey="value" barSize={10}>
|
||||
{data.map((check, index) => (
|
||||
<Cell key={`cell-${index}`} fill={color[check.status]} />
|
||||
))}
|
||||
</Bar>
|
||||
<ReferenceLine x={0} stroke="black" strokeDasharray="3 3">
|
||||
<Label value="60 mins" position="top" style={labelStyle} />
|
||||
</ReferenceLine>
|
||||
<ReferenceLine
|
||||
x={Math.floor(data.length * (2 / 3))}
|
||||
stroke="black"
|
||||
strokeDasharray="3 3"
|
||||
>
|
||||
<Label value="20 mins" position="top" style={labelStyle} />
|
||||
</ReferenceLine>
|
||||
<ReferenceLine x={data.length - 1} stroke="black" strokeDasharray="3 3">
|
||||
<Label value="now" position="top" style={labelStyle} />
|
||||
</ReferenceLine>
|
||||
</BarChart>
|
||||
);
|
||||
};
|
||||
|
||||
MonitorDetails60MinChart.propTypes = {
|
||||
data: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
export default MonitorDetails60MinChart;
|
||||
@@ -1,8 +1,16 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { AreaChart, Area, XAxis, Tooltip, ResponsiveContainer } from "recharts";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
Tooltip,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
} from "recharts";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import "./index.css";
|
||||
import React, { useMemo } from "react";
|
||||
|
||||
const CustomToolTip = ({ active, payload, label }) => {
|
||||
const theme = useTheme();
|
||||
@@ -16,14 +24,15 @@ const CustomToolTip = ({ active, payload, label }) => {
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.dark,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
py: theme.spacing(6),
|
||||
px: theme.spacing(8),
|
||||
py: theme.spacing(2),
|
||||
px: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
color: theme.palette.common.main,
|
||||
fontSize: 13,
|
||||
color: theme.palette.text.tertiary,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{new Date(label).toLocaleDateString("en-US", {
|
||||
@@ -38,15 +47,39 @@ const CustomToolTip = ({ active, payload, label }) => {
|
||||
hour12: true, // AM/PM format
|
||||
})}
|
||||
</Typography>
|
||||
<Typography
|
||||
mt={theme.spacing(2.5)}
|
||||
sx={{
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
Response Time (ms): {payload[0].payload.originalResponseTime}
|
||||
</Typography>{" "}
|
||||
<Box mt={theme.spacing(1)}>
|
||||
<Box
|
||||
display="inline-block"
|
||||
width={theme.spacing(4)}
|
||||
height={theme.spacing(4)}
|
||||
backgroundColor={theme.palette.primary.main}
|
||||
sx={{ borderRadius: "50%" }}
|
||||
/>
|
||||
<Stack
|
||||
display="inline-flex"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
ml={theme.spacing(3)}
|
||||
sx={{
|
||||
"& span": {
|
||||
color: theme.palette.text.tertiary,
|
||||
fontSize: 11,
|
||||
fontWeight: 500,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography component="span" sx={{ opacity: 0.8 }}>
|
||||
Response Time
|
||||
</Typography>{" "}
|
||||
<Typography component="span">
|
||||
{payload[0].payload.originalResponseTime}
|
||||
<Typography component="span" sx={{ opacity: 0.8 }}>
|
||||
{" "}
|
||||
ms
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
{/* Display original value */}
|
||||
</Box>
|
||||
);
|
||||
@@ -64,12 +97,16 @@ const MonitorDetailsAreaChart = ({ checks }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const memoizedChecks = useMemo(() => checks, []);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ResponsiveContainer width="100%" minWidth={25} height={220}>
|
||||
<AreaChart
|
||||
width={500}
|
||||
height={400}
|
||||
data={checks}
|
||||
width="100%"
|
||||
height="100%"
|
||||
data={memoizedChecks}
|
||||
margin={{
|
||||
top: 10,
|
||||
right: 0,
|
||||
@@ -77,18 +114,41 @@ const MonitorDetailsAreaChart = ({ checks }) => {
|
||||
bottom: 0,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid
|
||||
stroke={theme.palette.border.light}
|
||||
strokeWidth={1}
|
||||
strokeOpacity={1}
|
||||
fill="transparent"
|
||||
vertical={false}
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={theme.palette.primary.main}
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={theme.palette.primary.light}
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<XAxis
|
||||
stroke={theme.palette.border.dark}
|
||||
dataKey="createdAt"
|
||||
tickFormatter={formatDate}
|
||||
tick={{ fontSize: "13px" }}
|
||||
tickLine={false}
|
||||
height={18}
|
||||
/>
|
||||
<Tooltip content={<CustomToolTip />} />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="responseTime"
|
||||
stroke="#29afee"
|
||||
fill="#eaf2fd"
|
||||
stroke={theme.palette.primary.main}
|
||||
fill="url(#colorUv)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { Stack } from "@mui/material";
|
||||
import { BarChart, Bar, ResponsiveContainer, Cell } from "recharts";
|
||||
import "./index.css";
|
||||
|
||||
const ResponseTimeChart = ({ checks = [] }) => {
|
||||
return (
|
||||
<Stack
|
||||
flexDirection="row"
|
||||
justifyContent="space-around"
|
||||
alignItems="flex-end"
|
||||
height="50px"
|
||||
width="300px"
|
||||
>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart
|
||||
width={150}
|
||||
height={40}
|
||||
data={checks}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<Bar maxBarSize={10} dataKey="responseTime">
|
||||
{checks.map((check, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={
|
||||
check.status === true
|
||||
? "var(--success-color)"
|
||||
: "var(--error-color)"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
ResponseTimeChart.propTypes = {
|
||||
checks: PropTypes.array,
|
||||
};
|
||||
export default ResponseTimeChart;
|
||||
@@ -1,11 +1,12 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { Box, Button, Stack, Typography } from "@mui/material";
|
||||
import Skeleton from "../../assets/Images/create-placeholder.svg?react";
|
||||
import SkeletonDark from "../../assets/Images/create-placeholder-dark.svg?react";
|
||||
import Background from "../../assets/Images/background-grid.svg?react";
|
||||
import Button from "../Button";
|
||||
import Check from "../Check/Check";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
@@ -22,6 +23,7 @@ import "./index.css";
|
||||
const Fallback = ({ title, checks, link = "/", isAdmin }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
@@ -29,7 +31,11 @@ const Fallback = ({ title, checks, link = "/", isAdmin }) => {
|
||||
alignItems="center"
|
||||
gap={theme.spacing(20)}
|
||||
>
|
||||
<Skeleton style={{ zIndex: 1 }} />
|
||||
{mode === "light" ? (
|
||||
<Skeleton style={{ zIndex: 1 }} />
|
||||
) : (
|
||||
<SkeletonDark style={{ zIndex: 1 }} />
|
||||
)}
|
||||
<Box
|
||||
className="background-pattern-svg"
|
||||
sx={{
|
||||
@@ -44,7 +50,7 @@ const Fallback = ({ title, checks, link = "/", isAdmin }) => {
|
||||
<Typography
|
||||
component="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.text.secondary}
|
||||
color={theme.palette.text.tertiary}
|
||||
>
|
||||
A {title} is used to:
|
||||
</Typography>
|
||||
@@ -59,11 +65,13 @@ const Fallback = ({ title, checks, link = "/", isAdmin }) => {
|
||||
{/* TODO - display a different fallback if user is not an admin*/}
|
||||
{isAdmin && (
|
||||
<Button
|
||||
level="primary"
|
||||
label={`Let's create your ${title}`}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ alignSelf: "center" }}
|
||||
onClick={() => navigate(link)}
|
||||
/>
|
||||
>
|
||||
Let's create your {title}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { Autocomplete, TextField } from "@mui/material";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
/**
|
||||
* @example
|
||||
*
|
||||
* const options = [
|
||||
* { _id: "66d6119ef959cbc681e034f0", name: "Googler" },
|
||||
* { _id: "66d6119ef959cbc681e034f0", name: "CNN" },
|
||||
* { _id: "66d61a1bf959cbc681e0353f", name: "X Corp." },
|
||||
* ];
|
||||
*
|
||||
* <AutoCompleteField options={options} />
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* AutoCompleteField component.
|
||||
*
|
||||
* @component
|
||||
* @param {Object} props - The component props.
|
||||
* @param {string} props.id - The ID of the autocomplete field.
|
||||
* @param {string} props.type - The type of the input field (text or number).
|
||||
* @param {Array} props.options - The options for the autocomplete field.
|
||||
* @param {string} props.placeholder - The placeholder text for the input field.
|
||||
* @param {boolean} props.disabled - Indicates if the field is disabled.
|
||||
* @returns {JSX.Element} The AutoCompleteField component.
|
||||
*/
|
||||
|
||||
const AutoCompleteField = ({
|
||||
id,
|
||||
type,
|
||||
options,
|
||||
placeholder = "Type to search",
|
||||
disabled,
|
||||
}) => {
|
||||
const [value, setValue] = React.useState();
|
||||
const [inputValue, setInputValue] = React.useState("");
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
freeSolo
|
||||
className="auto-complete-field"
|
||||
id={id}
|
||||
value={value}
|
||||
onChange={(event, newValue) => {
|
||||
setValue(newValue);
|
||||
}}
|
||||
inputValue={inputValue}
|
||||
onInputChange={(event, newInputValue) => {
|
||||
setInputValue(newInputValue);
|
||||
}}
|
||||
options={options}
|
||||
getOptionLabel={(option) => option.name}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
sx: {
|
||||
width: 360,
|
||||
height: 34,
|
||||
fontSize: 13,
|
||||
p: 0,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
"& input": {
|
||||
p: 0,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
renderOption={(props, option) => {
|
||||
const { key, ...optionProps } = props;
|
||||
return (
|
||||
<li key={option._id} {...optionProps}>
|
||||
<div>{<span>{option.name}</span>}</div>
|
||||
</li>
|
||||
);
|
||||
}}
|
||||
slotProps={{
|
||||
popper: {
|
||||
sx: {
|
||||
"& ul": { p: 0 },
|
||||
"& li": { borderRadius: theme.shape.borderRadius },
|
||||
},
|
||||
},
|
||||
paper: {
|
||||
sx: {
|
||||
p: 2,
|
||||
fontSize: 13,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
AutoCompleteField.displayName = "AutoCompleteField";
|
||||
|
||||
AutoCompleteField.propTypes = {
|
||||
id: PropTypes.string,
|
||||
type: PropTypes.oneOf(["text", "number"]),
|
||||
options: PropTypes.array,
|
||||
placeholder: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
setWidth: PropTypes.string,
|
||||
};
|
||||
|
||||
export default AutoCompleteField;
|
||||
@@ -38,12 +38,12 @@ const ImageField = ({ id, src, loading, onChange }) => {
|
||||
border: "dashed",
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
borderColor: isDragging
|
||||
? theme.palette.common.main
|
||||
? theme.palette.primary.main
|
||||
: theme.palette.border.light,
|
||||
borderWidth: "2px",
|
||||
transition: "0.2s",
|
||||
"&:hover": {
|
||||
borderColor: theme.palette.common.main,
|
||||
borderColor: theme.palette.primary.main,
|
||||
backgroundColor: "hsl(215, 87%, 51%, 0.05)",
|
||||
},
|
||||
}}
|
||||
@@ -96,7 +96,7 @@ const ImageField = ({ id, src, loading, onChange }) => {
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
color={theme.palette.common.main}
|
||||
color={theme.palette.primary.main}
|
||||
fontWeight={500}
|
||||
>
|
||||
Click to upload
|
||||
|
||||
@@ -67,6 +67,7 @@ const Select = ({
|
||||
component="h3"
|
||||
color={theme.palette.text.secondary}
|
||||
fontWeight={500}
|
||||
fontSize={13}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
|
||||
@@ -138,6 +138,11 @@ const StatusLabel = ({ status, text, customStyles }) => {
|
||||
bgColor: theme.palette.error.bg,
|
||||
borderColor: theme.palette.error.light,
|
||||
},
|
||||
pending: {
|
||||
dotColor: theme.palette.warning.main,
|
||||
bgColor: theme.palette.warning.bg,
|
||||
borderColor: theme.palette.warning.light,
|
||||
},
|
||||
"cannot resolve": {
|
||||
dotColor: theme.palette.unresolved.main,
|
||||
bgColor: theme.palette.unresolved.bg,
|
||||
@@ -170,7 +175,7 @@ const StatusLabel = ({ status, text, customStyles }) => {
|
||||
};
|
||||
|
||||
StatusLabel.propTypes = {
|
||||
status: PropTypes.oneOf(["up", "down", "cannot resolve"]),
|
||||
status: PropTypes.oneOf(["up", "down", "pending", "cannot resolve"]),
|
||||
text: PropTypes.string,
|
||||
customStyles: PropTypes.object,
|
||||
};
|
||||
|
||||
@@ -28,11 +28,11 @@ const Link = ({ level, label, url }) => {
|
||||
sx: {
|
||||
textDecoration: "underline",
|
||||
textDecorationStyle: "dashed",
|
||||
textDecorationColor: theme.palette.common.main,
|
||||
textDecorationColor: theme.palette.primary.main,
|
||||
textUnderlineOffset: "1px",
|
||||
":hover": {
|
||||
color: theme.palette.text.tertiary,
|
||||
textDecorationColor: theme.palette.common.main,
|
||||
textDecorationColor: theme.palette.primary.main,
|
||||
backgroundColor: theme.palette.background.fill,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -75,7 +75,7 @@ aside.collapsed .MuiAvatar-root + .MuiBox-root + .MuiIconButton-root {
|
||||
}
|
||||
|
||||
aside .MuiListSubheader-root {
|
||||
transition: all 200ms ease;
|
||||
transition: padding 200ms ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
|
||||
@@ -154,7 +154,7 @@ function Sidebar() {
|
||||
color="white"
|
||||
sx={{
|
||||
position: "relative",
|
||||
backgroundColor: theme.palette.common.main,
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
userSelect: "none",
|
||||
}}
|
||||
@@ -598,8 +598,9 @@ function Sidebar() {
|
||||
MenuListProps={{
|
||||
sx: {
|
||||
p: 2,
|
||||
"& li": { m: 0 },
|
||||
"& li:has(.MuiBox-root):hover": {
|
||||
backgroundColor: theme.palette.background.main,
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
},
|
||||
}}
|
||||
@@ -620,8 +621,22 @@ function Sidebar() {
|
||||
</MenuItem>
|
||||
)}
|
||||
{collapsed && <Divider />}
|
||||
<MenuItem onClick={() => dispatch(setMode("light"))}>Light</MenuItem>
|
||||
<MenuItem onClick={() => dispatch(setMode("dark"))}>Dark</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
dispatch(setMode("light"));
|
||||
closePopup();
|
||||
}}
|
||||
>
|
||||
Light
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
dispatch(setMode("dark"));
|
||||
closePopup();
|
||||
}}
|
||||
>
|
||||
Dark
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem
|
||||
onClick={logout}
|
||||
|
||||
@@ -2,7 +2,7 @@ import TabPanel from "@mui/lab/TabPanel";
|
||||
import React, { useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import ButtonSpinner from "../../ButtonSpinner";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import Field from "../../Inputs/Field";
|
||||
import { credentials } from "../../../Validation/validation";
|
||||
import Alert from "../../Alert";
|
||||
@@ -164,19 +164,20 @@ const PasswordPanel = () => {
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<ButtonSpinner
|
||||
level="primary"
|
||||
label="Save"
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleSubmit}
|
||||
isLoading={isLoading}
|
||||
loadingText="Saving..."
|
||||
loading={isLoading}
|
||||
loadingIndicator="Saving..."
|
||||
disabled={Object.keys(errors).length !== 0 && true}
|
||||
sx={{
|
||||
paddingX: theme.spacing(12),
|
||||
width: "fit-content",
|
||||
px: theme.spacing(12),
|
||||
mt: theme.spacing(20),
|
||||
}}
|
||||
/>
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</TabPanel>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useRef, useState } from "react";
|
||||
import TabPanel from "@mui/lab/TabPanel";
|
||||
import { Box, Divider, Modal, Stack, Typography } from "@mui/material";
|
||||
import ButtonSpinner from "../../ButtonSpinner";
|
||||
import Button from "../../Button";
|
||||
import { Box, Button, Divider, Modal, Stack, Typography } from "@mui/material";
|
||||
import Avatar from "../../Avatar";
|
||||
import Field from "../../Inputs/Field";
|
||||
import ImageField from "../../Inputs/Image";
|
||||
@@ -20,6 +18,7 @@ import { formatBytes } from "../../../Utils/fileUtils";
|
||||
import { clearUptimeMonitorState } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
|
||||
/**
|
||||
* ProfilePanel component displays a form for editing user profile information
|
||||
@@ -292,19 +291,12 @@ const ProfilePanel = () => {
|
||||
}
|
||||
sx={{ mr: "8px" }}
|
||||
/>
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Delete"
|
||||
onClick={handleDeletePicture}
|
||||
/>
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Update"
|
||||
onClick={openPictureModal}
|
||||
sx={{
|
||||
color: theme.palette.common.main,
|
||||
}}
|
||||
/>
|
||||
<Button variant="text" color="info" onClick={handleDeletePicture}>
|
||||
Delete
|
||||
</Button>
|
||||
<Button variant="text" color="primary" onClick={openPictureModal}>
|
||||
Update
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Divider
|
||||
@@ -316,19 +308,19 @@ const ProfilePanel = () => {
|
||||
/>
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<Box width="fit-content">
|
||||
<ButtonSpinner
|
||||
level="primary"
|
||||
label="Save"
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleSaveProfile}
|
||||
isLoading={isLoading}
|
||||
loadingText="Saving..."
|
||||
loading={isLoading}
|
||||
loadingIndicator="Saving..."
|
||||
disabled={
|
||||
Object.keys(errors).length !== 0 && !errors?.picture && true
|
||||
}
|
||||
sx={{
|
||||
paddingX: theme.spacing(12),
|
||||
}}
|
||||
/>
|
||||
sx={{ px: theme.spacing(12) }}
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -339,13 +331,8 @@ const ProfilePanel = () => {
|
||||
borderColor: theme.palette.border.light,
|
||||
}}
|
||||
/>
|
||||
<Stack
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Box>
|
||||
<Box component="form" noValidate spellCheck="false">
|
||||
<Box mb={theme.spacing(6)}>
|
||||
<Typography component="h1">Delete account</Typography>
|
||||
<Typography component="p" sx={{ opacity: 0.6 }}>
|
||||
Note that deleting your account will remove all data from our
|
||||
@@ -353,12 +340,13 @@ const ProfilePanel = () => {
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
level="error"
|
||||
label="Delete account"
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => setIsOpen("delete")}
|
||||
sx={{ width: "fit-content", mt: theme.spacing(4) }}
|
||||
/>
|
||||
</Stack>
|
||||
>
|
||||
Delete account
|
||||
</Button>
|
||||
</Box>
|
||||
{/* TODO - Update ModalPopup Component with @mui for reusability */}
|
||||
<Modal
|
||||
aria-labelledby="modal-delete-account"
|
||||
@@ -400,17 +388,17 @@ const ProfilePanel = () => {
|
||||
mt={theme.spacing(5)}
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Cancel"
|
||||
onClick={() => setIsOpen("")}
|
||||
/>
|
||||
<ButtonSpinner
|
||||
level="error"
|
||||
label="Delete account"
|
||||
<Button variant="text" color="info" onClick={() => setIsOpen("")}>
|
||||
Cancel
|
||||
</Button>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={handleDeleteAccount}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
loading={isLoading}
|
||||
>
|
||||
Delete account
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
@@ -480,15 +468,19 @@ const ProfilePanel = () => {
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Edit"
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
disabled
|
||||
sx={{ mr: "auto" }}
|
||||
/>
|
||||
<Button level="tertiary" label="Remove" onClick={removePicture} />
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button variant="text" color="info" onClick={removePicture}>
|
||||
Remove
|
||||
</Button>
|
||||
<Button
|
||||
level="primary"
|
||||
label="Update"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleUpdatePicture}
|
||||
disabled={
|
||||
(Object.keys(errors).length !== 0 && errors?.picture) ||
|
||||
@@ -496,7 +488,9 @@ const ProfilePanel = () => {
|
||||
? true
|
||||
: false
|
||||
}
|
||||
/>
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
@@ -1,27 +1,15 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import TabPanel from "@mui/lab/TabPanel";
|
||||
import {
|
||||
Box,
|
||||
ButtonGroup,
|
||||
Divider,
|
||||
IconButton,
|
||||
Modal,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import ButtonSpinner from "../../ButtonSpinner";
|
||||
import Button from "../../Button";
|
||||
import { Button, ButtonGroup, Modal, Stack, Typography } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import EditSvg from "../../../assets/icons/edit.svg?react";
|
||||
import Field from "../../Inputs/Field";
|
||||
import { credentials } from "../../../Validation/validation";
|
||||
import { networkService } from "../../../main";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { useSelector } from "react-redux";
|
||||
import BasicTable from "../../BasicTable";
|
||||
import Remove from "../../../assets/icons/trash-bin.svg?react";
|
||||
import Select from "../../Inputs/Select";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
|
||||
/**
|
||||
* TeamPanel component manages the organization and team members,
|
||||
@@ -54,6 +42,7 @@ const TeamPanel = () => {
|
||||
const [members, setMembers] = useState([]);
|
||||
const [filter, setFilter] = useState("all");
|
||||
const [errors, setErrors] = useState({});
|
||||
const [isSendingInvite, setIsSendingInvite] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTeam = async () => {
|
||||
@@ -173,6 +162,7 @@ const TeamPanel = () => {
|
||||
};
|
||||
|
||||
const handleInviteMember = async () => {
|
||||
setIsSendingInvite(true);
|
||||
if (!toInvite.role.includes("user") || !toInvite.role.includes("admin"))
|
||||
setToInvite((prev) => ({ ...prev, role: ["user"] }));
|
||||
|
||||
@@ -185,24 +175,28 @@ const TeamPanel = () => {
|
||||
|
||||
if (error) {
|
||||
setErrors((prev) => ({ ...prev, email: error.details[0].message }));
|
||||
} else
|
||||
try {
|
||||
await networkService.requestInvitationToken(
|
||||
authToken,
|
||||
toInvite.email,
|
||||
toInvite.role
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
closeInviteModal();
|
||||
createToast({
|
||||
body: "Member invited. They will receive an email with details on how to create their account.",
|
||||
});
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body: error.message || "Unknown error.",
|
||||
});
|
||||
}
|
||||
try {
|
||||
await networkService.requestInvitationToken(
|
||||
authToken,
|
||||
toInvite.email,
|
||||
toInvite.role
|
||||
);
|
||||
closeInviteModal();
|
||||
createToast({
|
||||
body: "Member invited. They will receive an email with details on how to create their account.",
|
||||
});
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body: error.message || "Unknown error.",
|
||||
});
|
||||
} finally {
|
||||
setIsSendingInvite(false);
|
||||
}
|
||||
};
|
||||
|
||||
const closeInviteModal = () => {
|
||||
setIsOpen(false);
|
||||
setToInvite({ email: "", role: ["0"] });
|
||||
@@ -283,48 +277,38 @@ const TeamPanel = () => {
|
||||
gap={theme.spacing(6)}
|
||||
sx={{ fontSize: 14 }}
|
||||
>
|
||||
<ButtonGroup
|
||||
sx={{
|
||||
"& button, & button:hover": {
|
||||
borderColor: theme.palette.border.light,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="All"
|
||||
variant="group"
|
||||
filled={(filter === "all").toString()}
|
||||
onClick={() => setFilter("all")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "all" && theme.palette.background.fill,
|
||||
}}
|
||||
/>
|
||||
>
|
||||
All
|
||||
</Button>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Administrator"
|
||||
variant="group"
|
||||
filled={(filter === "admin").toString()}
|
||||
onClick={() => setFilter("admin")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "admin" && theme.palette.background.fill,
|
||||
}}
|
||||
/>
|
||||
>
|
||||
Administrator
|
||||
</Button>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Member"
|
||||
variant="group"
|
||||
filled={(filter === "user").toString()}
|
||||
onClick={() => setFilter("user")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "user" && theme.palette.background.fill,
|
||||
}}
|
||||
/>
|
||||
>
|
||||
Member
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
<Button
|
||||
level="primary"
|
||||
label="Invite a team member"
|
||||
sx={{ paddingX: theme.spacing(15) }}
|
||||
<LoadingButton
|
||||
loading={isSendingInvite}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => setIsOpen(true)}
|
||||
/>
|
||||
>
|
||||
Invite a team member
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
<BasicTable
|
||||
data={tableData}
|
||||
@@ -400,18 +384,23 @@ const TeamPanel = () => {
|
||||
mt={theme.spacing(8)}
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Cancel"
|
||||
<LoadingButton
|
||||
loading={isSendingInvite}
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={closeInviteModal}
|
||||
/>
|
||||
<ButtonSpinner
|
||||
level="primary"
|
||||
label="Send invite"
|
||||
>
|
||||
Cancel
|
||||
</LoadingButton>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleInviteMember}
|
||||
isLoading={false}
|
||||
loading={isSendingInvite}
|
||||
disabled={Object.keys(errors).length !== 0}
|
||||
/>
|
||||
>
|
||||
Send invite
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { jwtDecode } from "jwt-decode";
|
||||
import { networkService } from "../../main";
|
||||
const initialState = {
|
||||
isLoading: false,
|
||||
monitors: [],
|
||||
monitorsSummary: [],
|
||||
success: null,
|
||||
msg: null,
|
||||
};
|
||||
@@ -28,19 +28,35 @@ export const createPageSpeed = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
export const getPagespeedMonitorById = createAsyncThunk(
|
||||
"monitors/getMonitorById",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitorId } = data;
|
||||
const res = await networkService.getMonitorByid(authToken, monitorId);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const getPageSpeedByTeamId = createAsyncThunk(
|
||||
"pageSpeedMonitors/getPageSpeedByTeamId",
|
||||
async (token, thunkApi) => {
|
||||
const user = jwtDecode(token);
|
||||
try {
|
||||
const res = await networkService.getMonitorsByTeamId(
|
||||
const res = await networkService.getMonitorsAndSummaryByTeamId(
|
||||
token,
|
||||
user.teamId,
|
||||
25,
|
||||
["pagespeed"],
|
||||
null,
|
||||
"desc",
|
||||
false
|
||||
["pagespeed"]
|
||||
);
|
||||
|
||||
return res.data;
|
||||
@@ -109,6 +125,25 @@ export const deletePageSpeed = createAsyncThunk(
|
||||
}
|
||||
}
|
||||
);
|
||||
export const pausePageSpeed = createAsyncThunk(
|
||||
"pageSpeedMonitors/pausePageSpeed",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitorId } = data;
|
||||
const res = await networkService.pauseMonitorById(authToken, monitorId);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const pageSpeedMonitorSlice = createSlice({
|
||||
name: "pageSpeedMonitor",
|
||||
@@ -116,7 +151,7 @@ const pageSpeedMonitorSlice = createSlice({
|
||||
reducers: {
|
||||
clearMonitorState: (state) => {
|
||||
state.isLoading = false;
|
||||
state.monitors = [];
|
||||
state.monitorsSummary = [];
|
||||
state.success = null;
|
||||
state.msg = null;
|
||||
},
|
||||
@@ -124,7 +159,7 @@ const pageSpeedMonitorSlice = createSlice({
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
// *****************************************************
|
||||
// Monitors by userId
|
||||
// Monitors by teamId
|
||||
// *****************************************************
|
||||
|
||||
.addCase(getPageSpeedByTeamId.pending, (state) => {
|
||||
@@ -133,7 +168,7 @@ const pageSpeedMonitorSlice = createSlice({
|
||||
.addCase(getPageSpeedByTeamId.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.msg;
|
||||
state.monitors = action.payload.data;
|
||||
state.monitorsSummary = action.payload.data;
|
||||
})
|
||||
.addCase(getPageSpeedByTeamId.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
@@ -143,6 +178,23 @@ const pageSpeedMonitorSlice = createSlice({
|
||||
: "Getting page speed monitors failed";
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
.addCase(getPagespeedMonitorById.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(getPagespeedMonitorById.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(getPagespeedMonitorById.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to get pagespeed monitor";
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
// Create Monitor
|
||||
// *****************************************************
|
||||
@@ -163,7 +215,7 @@ const pageSpeedMonitorSlice = createSlice({
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
// Create Monitor
|
||||
// Update Monitor
|
||||
// *****************************************************
|
||||
.addCase(updatePageSpeed.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
@@ -198,6 +250,24 @@ const pageSpeedMonitorSlice = createSlice({
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to delete page speed monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Pause Monitor
|
||||
// *****************************************************
|
||||
.addCase(pausePageSpeed.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(pausePageSpeed.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(pausePageSpeed.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to pause page speed monitor";
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { createSlice } from "@reduxjs/toolkit";
|
||||
// Add more settings as needed (e.g., theme preferences, user settings)
|
||||
const initialState = {
|
||||
monitors: {
|
||||
rowsPerPage: 5,
|
||||
rowsPerPage: 10,
|
||||
},
|
||||
team: {
|
||||
rowsPerPage: 5,
|
||||
@@ -13,6 +13,7 @@ const initialState = {
|
||||
collapsed: false,
|
||||
},
|
||||
mode: "light",
|
||||
greeting: { index: 0, lastUpdate: null },
|
||||
};
|
||||
|
||||
const uiSlice = createSlice({
|
||||
@@ -31,8 +32,13 @@ const uiSlice = createSlice({
|
||||
setMode: (state, action) => {
|
||||
state.mode = action.payload;
|
||||
},
|
||||
setGreeting(state, action) {
|
||||
state.greeting.index = action.payload.index;
|
||||
state.greeting.lastUpdate = action.payload.lastUpdate;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default uiSlice.reducer;
|
||||
export const { setRowsPerPage, toggleSidebar, setMode } = uiSlice.actions;
|
||||
export const { setRowsPerPage, toggleSidebar, setMode, setGreeting } =
|
||||
uiSlice.actions;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { jwtDecode } from "jwt-decode";
|
||||
import { networkService } from "../../main";
|
||||
const initialState = {
|
||||
isLoading: false,
|
||||
monitors: [],
|
||||
monitorsSummary: [],
|
||||
success: null,
|
||||
msg: null,
|
||||
};
|
||||
@@ -49,18 +49,14 @@ export const getUptimeMonitorById = createAsyncThunk(
|
||||
);
|
||||
|
||||
export const getUptimeMonitorsByTeamId = createAsyncThunk(
|
||||
"montiors/getMonitorsByTeamId",
|
||||
"monitors/getMonitorsByTeamId",
|
||||
async (token, thunkApi) => {
|
||||
const user = jwtDecode(token);
|
||||
try {
|
||||
const res = await networkService.getMonitorsByTeamId(
|
||||
const res = await networkService.getMonitorsAndSummaryByTeamId(
|
||||
token,
|
||||
user.teamId,
|
||||
25,
|
||||
["http", "ping"],
|
||||
null,
|
||||
"desc",
|
||||
true
|
||||
["http", "ping"]
|
||||
);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
@@ -149,13 +145,33 @@ export const pauseUptimeMonitor = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteMonitorChecksByTeamId = createAsyncThunk(
|
||||
"monitors/deleteChecksByTeamId",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, teamId } = data;
|
||||
const res = await networkService.deleteChecksByTeamId(authToken, teamId);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const uptimeMonitorsSlice = createSlice({
|
||||
name: "uptimeMonitors",
|
||||
initialState,
|
||||
reducers: {
|
||||
clearUptimeMonitorState: (state) => {
|
||||
state.isLoading = false;
|
||||
state.monitors = [];
|
||||
state.monitorsSummary = [];
|
||||
state.success = null;
|
||||
state.msg = null;
|
||||
},
|
||||
@@ -163,7 +179,7 @@ const uptimeMonitorsSlice = createSlice({
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
// *****************************************************
|
||||
// Monitors by userId
|
||||
// Monitors by teamId
|
||||
// *****************************************************
|
||||
|
||||
.addCase(getUptimeMonitorsByTeamId.pending, (state) => {
|
||||
@@ -172,7 +188,7 @@ const uptimeMonitorsSlice = createSlice({
|
||||
.addCase(getUptimeMonitorsByTeamId.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.msg;
|
||||
state.monitors = action.payload.data;
|
||||
state.monitorsSummary = action.payload.data;
|
||||
})
|
||||
.addCase(getUptimeMonitorsByTeamId.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
@@ -216,7 +232,7 @@ const uptimeMonitorsSlice = createSlice({
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to pause uptime monitor";
|
||||
: "Failed to get uptime monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// update Monitor
|
||||
@@ -256,6 +272,24 @@ const uptimeMonitorsSlice = createSlice({
|
||||
: "Failed to delete uptime monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Delete Monitor checks by Team ID
|
||||
// *****************************************************
|
||||
.addCase(deleteMonitorChecksByTeamId.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(deleteMonitorChecksByTeamId.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(deleteMonitorChecksByTeamId.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to delete monitor checks";
|
||||
})
|
||||
// *****************************************************
|
||||
// Pause Monitor
|
||||
// *****************************************************
|
||||
.addCase(pauseUptimeMonitor.pending, (state) => {
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
}
|
||||
|
||||
.home-layout {
|
||||
display: flex;
|
||||
position: relative;
|
||||
gap: var(--env-var-spacing-2);
|
||||
min-height: 100vh;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Sidebar from "../../Components/Sidebar";
|
||||
import { Outlet } from "react-router";
|
||||
import { Box } from "@mui/material";
|
||||
import { Box, Stack } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
import "./index.css";
|
||||
@@ -10,10 +10,14 @@ const HomeLayout = () => {
|
||||
|
||||
return (
|
||||
<Box backgroundColor={theme.palette.background.alt}>
|
||||
<Box className="home-layout">
|
||||
<Stack
|
||||
className="home-layout"
|
||||
flexDirection="row"
|
||||
gap={theme.spacing(14)}
|
||||
>
|
||||
<Sidebar />
|
||||
<Outlet />
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { Box, Button, Stack, Typography } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useNavigate } from "react-router";
|
||||
import { createToast } from "../../Utils/toastUtils";
|
||||
import { forgotPassword } from "../../Features/Auth/authSlice";
|
||||
import Button from "../../Components/Button";
|
||||
import background from "../../assets/Images/background_pattern_decorative.png";
|
||||
import EmailIcon from "../../assets/icons/email.svg?react";
|
||||
import Logo from "../../assets/icons/bwu-icon.svg?react";
|
||||
@@ -90,9 +89,9 @@ const CheckEmail = () => {
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.common.main,
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 600,
|
||||
fontSize: 26,
|
||||
fontSize: 22,
|
||||
},
|
||||
"& p": {
|
||||
fontSize: 14,
|
||||
@@ -150,21 +149,23 @@ const CheckEmail = () => {
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
level="primary"
|
||||
label="Open email app"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={openMail}
|
||||
sx={{
|
||||
width: "100%",
|
||||
maxWidth: 400,
|
||||
}}
|
||||
/>
|
||||
>
|
||||
Open email app
|
||||
</Button>
|
||||
<Typography sx={{ alignSelf: "center", mb: theme.spacing(6) }}>
|
||||
Didn't receive the email?{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
onClick={resendToken}
|
||||
sx={{
|
||||
color: theme.palette.common.main,
|
||||
color: theme.palette.primary.main,
|
||||
userSelect: "none",
|
||||
pointerEvents: disabled ? "none" : "auto",
|
||||
cursor: disabled ? "default" : "pointer",
|
||||
@@ -181,7 +182,7 @@ const CheckEmail = () => {
|
||||
<Typography
|
||||
component="span"
|
||||
ml={theme.spacing(2)}
|
||||
color={theme.palette.common.main}
|
||||
color={theme.palette.primary.main}
|
||||
onClick={handleNavigate}
|
||||
sx={{ userSelect: "none" }}
|
||||
>
|
||||
|
||||
@@ -7,10 +7,10 @@ import { useEffect, useState } from "react";
|
||||
import { credentials } from "../../Validation/validation";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Field from "../../Components/Inputs/Field";
|
||||
import ButtonSpinner from "../../Components/ButtonSpinner";
|
||||
import Logo from "../../assets/icons/bwu-icon.svg?react";
|
||||
import Key from "../../assets/icons/key.svg?react";
|
||||
import background from "../../assets/Images/background_pattern_decorative.png";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import "./index.css";
|
||||
|
||||
const ForgotPassword = () => {
|
||||
@@ -92,7 +92,7 @@ const ForgotPassword = () => {
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.common.main,
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 600,
|
||||
fontSize: 24,
|
||||
},
|
||||
@@ -166,18 +166,19 @@ const ForgotPassword = () => {
|
||||
onChange={handleChange}
|
||||
error={errors.email}
|
||||
/>
|
||||
<ButtonSpinner
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
loading={isLoading}
|
||||
disabled={errors.email !== undefined}
|
||||
onClick={handleSubmit}
|
||||
isLoading={isLoading}
|
||||
level="primary"
|
||||
label="Send instructions"
|
||||
sx={{
|
||||
width: "100%",
|
||||
fontWeight: 400,
|
||||
mt: theme.spacing(15),
|
||||
}}
|
||||
/>
|
||||
>
|
||||
Send instructions
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -185,7 +186,7 @@ const ForgotPassword = () => {
|
||||
<Typography display="inline-block">Go back to —</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
color={theme.palette.common.main}
|
||||
color={theme.palette.primary.main}
|
||||
ml={theme.spacing(2)}
|
||||
onClick={handleNavigate}
|
||||
sx={{ userSelect: "none" }}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { Box, Button, Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { credentials } from "../../Validation/validation";
|
||||
import { login } from "../../Features/Auth/authSlice";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { createToast } from "../../Utils/toastUtils";
|
||||
import Button from "../../Components/Button";
|
||||
import { networkService } from "../../main";
|
||||
import Field from "../../Components/Inputs/Field";
|
||||
import background from "../../assets/Images/background_pattern_decorative.png";
|
||||
@@ -40,17 +39,22 @@ const LandingPage = ({ onContinue }) => {
|
||||
</Box>
|
||||
<Box width="100%">
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Continue with Email"
|
||||
img={<Mail />}
|
||||
variant="outlined"
|
||||
color="info"
|
||||
onClick={onContinue}
|
||||
sx={{
|
||||
width: "100%",
|
||||
"& svg": {
|
||||
mr: theme.spacing(4),
|
||||
"& path": {
|
||||
stroke: theme.palette.other.icon,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<Mail />
|
||||
Continue with Email
|
||||
</Button>
|
||||
</Box>
|
||||
<Box maxWidth={400}>
|
||||
<Typography className="tos-p">
|
||||
@@ -151,27 +155,29 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
</Box>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Back"
|
||||
animate="slideLeft"
|
||||
img={<ArrowBackRoundedIcon />}
|
||||
variant="outlined"
|
||||
color="info"
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
mb: theme.spacing(6),
|
||||
px: theme.spacing(8),
|
||||
px: theme.spacing(5),
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.spacing(2),
|
||||
mr: theme.spacing(3),
|
||||
},
|
||||
}}
|
||||
props={{ tabIndex: -1 }}
|
||||
/>
|
||||
>
|
||||
<ArrowBackRoundedIcon />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
level="primary"
|
||||
label="Continue"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={onSubmit}
|
||||
disabled={errors.email && true}
|
||||
sx={{ width: "30%" }}
|
||||
/>
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
@@ -219,6 +225,7 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
<>
|
||||
<Stack
|
||||
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
|
||||
position="relative"
|
||||
textAlign="center"
|
||||
>
|
||||
<Box>
|
||||
@@ -243,39 +250,49 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
</Box>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Back"
|
||||
animate="slideLeft"
|
||||
img={<ArrowBackRoundedIcon />}
|
||||
variant="outlined"
|
||||
color="info"
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
mb: theme.spacing(6),
|
||||
px: theme.spacing(8),
|
||||
px: theme.spacing(5),
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.spacing(2),
|
||||
mr: theme.spacing(3),
|
||||
},
|
||||
}}
|
||||
props={{ tabIndex: -1 }}
|
||||
/>
|
||||
>
|
||||
<ArrowBackRoundedIcon />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
level="primary"
|
||||
label="Continue"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={onSubmit}
|
||||
disabled={errors.password && true}
|
||||
sx={{ width: "30%" }}
|
||||
/>
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
<Box textAlign="center">
|
||||
<Box
|
||||
textAlign="center"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "103%",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)"
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
className="forgot-p"
|
||||
display="inline-block"
|
||||
color={theme.palette.common.main}
|
||||
color={theme.palette.primary.main}
|
||||
>
|
||||
Forgot password?
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
color={theme.palette.common.main}
|
||||
color={theme.palette.primary.main}
|
||||
ml={theme.spacing(2)}
|
||||
sx={{ userSelect: "none" }}
|
||||
onClick={handleNavigate}
|
||||
@@ -419,7 +436,7 @@ const Login = () => {
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.common.main,
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 600,
|
||||
fontSize: 30,
|
||||
},
|
||||
@@ -491,7 +508,7 @@ const Login = () => {
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
color={theme.palette.common.main}
|
||||
color={theme.palette.primary.main}
|
||||
ml={theme.spacing(2)}
|
||||
onClick={() => {
|
||||
navigate("/register");
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { Box, Button, Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { clearAuthState } from "../../Features/Auth/authSlice";
|
||||
import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import Button from "../../Components/Button";
|
||||
import background from "../../assets/Images/background_pattern_decorative.png";
|
||||
import ConfirmIcon from "../../assets/icons/confirm-icon.svg?react";
|
||||
import Logo from "../../assets/icons/bwu-icon.svg?react";
|
||||
@@ -27,7 +26,7 @@ const NewPasswordConfirmed = () => {
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.common.main,
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 600,
|
||||
fontSize: 22,
|
||||
},
|
||||
@@ -85,21 +84,23 @@ const NewPasswordConfirmed = () => {
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
level="primary"
|
||||
label="Continue"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => navigate("/monitors")}
|
||||
sx={{
|
||||
width: "100%",
|
||||
maxWidth: 400,
|
||||
}}
|
||||
/>
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Box textAlign="center" p={theme.spacing(12)}>
|
||||
<Typography display="inline-block">Go back to —</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
color={theme.palette.common.main}
|
||||
color={theme.palette.primary.main}
|
||||
ml={theme.spacing(2)}
|
||||
onClick={handleNavigate}
|
||||
sx={{ userSelect: "none" }}
|
||||
|
||||
@@ -2,7 +2,7 @@ import PropTypes from "prop-types";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { Box, Button, Stack, Typography } from "@mui/material";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { credentials } from "../../../Validation/validation";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
@@ -13,11 +13,9 @@ import Logo from "../../../assets/icons/bwu-icon.svg?react";
|
||||
import Mail from "../../../assets/icons/mail.svg?react";
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
import Check from "../../../Components/Check/Check";
|
||||
import Button from "../../../Components/Button";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import { networkService } from "../../../main";
|
||||
import "../index.css";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
|
||||
/**
|
||||
* Displays the initial landing page.
|
||||
@@ -46,17 +44,22 @@ const LandingPage = ({ isSuperAdmin, onSignup }) => {
|
||||
</Box>
|
||||
<Box width="100%">
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Sign up with Email"
|
||||
img={<Mail />}
|
||||
variant="outlined"
|
||||
color="info"
|
||||
onClick={onSignup}
|
||||
sx={{
|
||||
width: "100%",
|
||||
"& svg": {
|
||||
mr: theme.spacing(4),
|
||||
"& path": {
|
||||
stroke: theme.palette.other.icon,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<Mail />
|
||||
Sign up with Email
|
||||
</Button>
|
||||
</Box>
|
||||
<Box maxWidth={400}>
|
||||
<Typography className="tos-p">
|
||||
@@ -180,26 +183,29 @@ const StepOne = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
</Box>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Back"
|
||||
animate="slideLeft"
|
||||
img={<ArrowBackRoundedIcon />}
|
||||
variant="outlined"
|
||||
color="info"
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
px: theme.spacing(8),
|
||||
px: theme.spacing(5),
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.spacing(2),
|
||||
mr: theme.spacing(3),
|
||||
},
|
||||
}}
|
||||
props={{ tabIndex: -1 }}
|
||||
/>
|
||||
>
|
||||
<ArrowBackRoundedIcon />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
level="primary"
|
||||
label="Continue"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={onSubmit}
|
||||
disabled={(errors.firstName || errors.lastName) && true}
|
||||
sx={{ width: "30%" }}
|
||||
/>
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
@@ -270,26 +276,29 @@ const StepTwo = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
</Box>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Back"
|
||||
animate="slideLeft"
|
||||
img={<ArrowBackRoundedIcon />}
|
||||
variant="outlined"
|
||||
color="info"
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
px: theme.spacing(8),
|
||||
px: theme.spacing(5),
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.spacing(2),
|
||||
mr: theme.spacing(3),
|
||||
},
|
||||
}}
|
||||
props={{ tabIndex: -1 }}
|
||||
/>
|
||||
>
|
||||
<ArrowBackRoundedIcon />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
level="primary"
|
||||
label="Continue"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={onSubmit}
|
||||
disabled={errors.email && true}
|
||||
sx={{ width: "30%" }}
|
||||
/>
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
@@ -443,26 +452,29 @@ const StepThree = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
</Box>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Back"
|
||||
animate="slideLeft"
|
||||
img={<ArrowBackRoundedIcon />}
|
||||
variant="outlined"
|
||||
color="info"
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
px: theme.spacing(8),
|
||||
px: theme.spacing(5),
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.spacing(2),
|
||||
mr: theme.spacing(3),
|
||||
},
|
||||
}}
|
||||
props={{ tabIndex: -1 }}
|
||||
/>
|
||||
>
|
||||
<ArrowBackRoundedIcon />
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
level="primary"
|
||||
label="Continue"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={onSubmit}
|
||||
disabled={errors.email && true}
|
||||
sx={{ width: "30%" }}
|
||||
/>
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
@@ -644,7 +656,7 @@ const Register = ({ isSuperAdmin }) => {
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.common.main,
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 600,
|
||||
fontSize: 30,
|
||||
},
|
||||
@@ -731,7 +743,7 @@ const Register = ({ isSuperAdmin }) => {
|
||||
onClick={() => {
|
||||
navigate("/login");
|
||||
}}
|
||||
sx={{ userSelect: "none", color: theme.palette.common.main }}
|
||||
sx={{ userSelect: "none", color: theme.palette.primary.main }}
|
||||
>
|
||||
Log In
|
||||
</Typography>
|
||||
|
||||
@@ -8,11 +8,11 @@ import { useState } from "react";
|
||||
import { credentials } from "../../Validation/validation";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Check from "../../Components/Check/Check";
|
||||
import ButtonSpinner from "../../Components/ButtonSpinner";
|
||||
import Field from "../../Components/Inputs/Field";
|
||||
import LockIcon from "../../assets/icons/lock-button-icon.svg?react";
|
||||
import background from "../../assets/Images/background_pattern_decorative.png";
|
||||
import Logo from "../../assets/icons/bwu-icon.svg?react";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import "./index.css";
|
||||
|
||||
const SetNewPassword = () => {
|
||||
@@ -109,7 +109,7 @@ const SetNewPassword = () => {
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.common.main,
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 600,
|
||||
fontSize: 24,
|
||||
},
|
||||
@@ -266,21 +266,23 @@ const SetNewPassword = () => {
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
<ButtonSpinner
|
||||
disabled={Object.keys(errors).length !== 0}
|
||||
isLoading={isLoading}
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
loading={isLoading}
|
||||
onClick={handleSubmit}
|
||||
level="primary"
|
||||
label="Reset password"
|
||||
disabled={Object.keys(errors).length !== 0}
|
||||
sx={{ width: "100%", maxWidth: 400 }}
|
||||
/>
|
||||
>
|
||||
Reset password
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Box textAlign="center" p={theme.spacing(12)}>
|
||||
<Typography display="inline-block">Go back to —</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
color={theme.palette.common.main}
|
||||
color={theme.palette.primary.main}
|
||||
ml={theme.spacing(2)}
|
||||
onClick={() => navigate("/login")}
|
||||
sx={{ userSelect: "none" }}
|
||||
|
||||
@@ -15,14 +15,12 @@ import {
|
||||
|
||||
import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
|
||||
import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { networkService } from "../../../main";
|
||||
import { StatusLabel } from "../../../Components/Label";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
const theme = useTheme();
|
||||
const { authToken, user } = useSelector((state) => state.auth);
|
||||
|
||||
@@ -1,38 +1,12 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { ButtonGroup, Stack, Skeleton, Typography } from "@mui/material";
|
||||
import Button from "../../Components/Button";
|
||||
import { ButtonGroup, Stack, Typography, Button } from "@mui/material";
|
||||
import { networkService } from "../../main";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import Select from "../../Components/Inputs/Select";
|
||||
import IncidentTable from "./IncidentTable";
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
* Renders a skeleton layout.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="row" alignItems="center" gap={theme.spacing(6)}>
|
||||
<Skeleton variant="rounded" width={150} height={34} />
|
||||
<Skeleton variant="rounded" width="15%" height={34} />
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="20%"
|
||||
height={34}
|
||||
sx={{ ml: "auto" }}
|
||||
/>
|
||||
</Stack>
|
||||
<Skeleton variant="rounded" width="100%" height={300} />
|
||||
<Skeleton variant="rounded" width="100%" height={100} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
import SkeletonLayout from "./skeleton";
|
||||
|
||||
const Incidents = () => {
|
||||
const theme = useTheme();
|
||||
@@ -55,11 +29,13 @@ const Incidents = () => {
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
// Reduce to a lookup object for 0(1) lookup
|
||||
if (res.data && res.data.data.length > 0) {
|
||||
const monitorLookup = res.data.data.reduce((acc, monitor) => {
|
||||
if (res?.data?.data?.monitors?.length > 0) {
|
||||
const monitorLookup = res.data.data.monitors.reduce((acc, monitor) => {
|
||||
acc[monitor._id] = monitor;
|
||||
return acc;
|
||||
}, {});
|
||||
@@ -78,11 +54,7 @@ const Incidents = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className="incidents"
|
||||
pt={theme.spacing(21)}
|
||||
gap={theme.spacing(12)}
|
||||
>
|
||||
<Stack className="incidents" pt={theme.spacing(20)} gap={theme.spacing(12)}>
|
||||
{loading ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
@@ -114,38 +86,26 @@ const Incidents = () => {
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="All"
|
||||
variant="group"
|
||||
filled={(filter === "all").toString()}
|
||||
onClick={() => setFilter("all")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "all"
|
||||
? theme.palette.background.fill
|
||||
: theme.palette.background.main,
|
||||
}}
|
||||
/>
|
||||
>
|
||||
All
|
||||
</Button>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Down"
|
||||
variant="group"
|
||||
filled={(filter === "down").toString()}
|
||||
onClick={() => setFilter("down")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "down"
|
||||
? theme.palette.background.fill
|
||||
: theme.palette.background.main,
|
||||
}}
|
||||
/>
|
||||
>
|
||||
Down
|
||||
</Button>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Cannot Resolve"
|
||||
variant="group"
|
||||
filled={(filter === "resolve").toString()}
|
||||
onClick={() => setFilter("resolve")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "resolve"
|
||||
? theme.palette.background.fill
|
||||
: theme.palette.background.main,
|
||||
}}
|
||||
/>
|
||||
>
|
||||
Cannot Resolve
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
<IncidentTable
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Stack, Skeleton } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
/**
|
||||
* Renders a skeleton layout.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="row" alignItems="center" gap={theme.spacing(6)}>
|
||||
<Skeleton variant="rounded" width={150} height={34} />
|
||||
<Skeleton variant="rounded" width="15%" height={34} />
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="20%"
|
||||
height={34}
|
||||
sx={{ ml: "auto" }}
|
||||
/>
|
||||
</Stack>
|
||||
<Skeleton variant="rounded" width="100%" height={300} />
|
||||
<Skeleton variant="rounded" width="100%" height={100} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -1,6 +1,5 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { Stack, Typography, Grid } from "@mui/material";
|
||||
import Button from "../../Components/Button";
|
||||
import { Stack, Typography, Grid, Button } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import Discord from "../../assets/icons/discord-icon.svg?react";
|
||||
import Slack from "../../assets/icons/slack-icon.svg?react";
|
||||
@@ -47,12 +46,14 @@ const IntegrationsComponent = ({ icon, header, info, onClick }) => {
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Button
|
||||
label="Add"
|
||||
level="primary"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={onClick}
|
||||
sx={{ alignSelf: "center" }}
|
||||
disabled={true}
|
||||
/>
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</Stack>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { Box, Button, Stack, Typography } from "@mui/material";
|
||||
import "./index.css";
|
||||
import { useState } from "react";
|
||||
import Button from "../../../Components/Button";
|
||||
import Back from "../../../assets/icons/left-arrow-long.svg?react";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||
@@ -215,7 +214,7 @@ const CreateNewMaintenanceWindow = () => {
|
||||
sx={{
|
||||
width: "fit-content",
|
||||
fontSize: "var(--env-var-font-size-small)",
|
||||
borderBottom: `1px dashed ${theme.palette.common.main}`,
|
||||
borderBottom: `1px dashed ${theme.palette.primary.main}`,
|
||||
paddingBottom: "4px",
|
||||
}}
|
||||
>
|
||||
@@ -230,18 +229,17 @@ const CreateNewMaintenanceWindow = () => {
|
||||
<div className="create-maintenance-window">
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Button
|
||||
id="btn-back"
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
sx={{
|
||||
width: "100px",
|
||||
height: "30px",
|
||||
gap: "10px",
|
||||
backgroundColor: theme.palette.background.fill,
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
label="Back"
|
||||
level="tertiary"
|
||||
img={<Back />}
|
||||
/>
|
||||
>
|
||||
<Back />
|
||||
Back
|
||||
</Button>
|
||||
<Box>
|
||||
<Typography
|
||||
sx={{
|
||||
@@ -278,26 +276,19 @@ const CreateNewMaintenanceWindow = () => {
|
||||
</Stack>
|
||||
<Stack justifyContent="end" direction="row" marginTop={3}>
|
||||
<Button
|
||||
variant="text"
|
||||
color="info"
|
||||
sx={{
|
||||
"&:hover": {
|
||||
backgroundColor: "transparent",
|
||||
boxShadow: "none",
|
||||
},
|
||||
}}
|
||||
level="tertiary"
|
||||
label="Cancel"
|
||||
/>
|
||||
<Button
|
||||
sx={{
|
||||
"&:hover": {
|
||||
backgroundColor: "#1570EF",
|
||||
boxShadow: "none",
|
||||
},
|
||||
}}
|
||||
level="primary"
|
||||
label="Create"
|
||||
onClick={handleSubmit}
|
||||
/>
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="contained" color="primary" onClick={handleSubmit}>
|
||||
Create
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useNavigate, useParams } from "react-router";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Box, Modal, Stack, Typography } from "@mui/material";
|
||||
import { Box, Button, Modal, Stack, Typography } from "@mui/material";
|
||||
import { monitorValidation } from "../../../Validation/validation";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
@@ -14,16 +14,17 @@ import {
|
||||
getUptimeMonitorsByTeamId,
|
||||
deleteUptimeMonitor,
|
||||
} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import Button from "../../../Components/Button";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
|
||||
import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import Checkbox from "../../../Components/Inputs/Checkbox";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import PulseDot from "../../../Components/Animated/PulseDot";
|
||||
import "./index.css";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
import ButtonSpinner from "../../../Components/ButtonSpinner";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
* Parses a URL string and returns a URL object.
|
||||
*
|
||||
@@ -52,7 +53,6 @@ const Configure = () => {
|
||||
const [monitor, setMonitor] = useState({});
|
||||
const [errors, setErrors] = useState({});
|
||||
const { monitorId } = useParams();
|
||||
|
||||
const idMap = {
|
||||
"monitor-url": "url",
|
||||
"monitor-name": "name",
|
||||
@@ -189,6 +189,18 @@ const Configure = () => {
|
||||
const parsedUrl = parseUrl(monitor?.url);
|
||||
const protocol = parsedUrl?.protocol?.replace(":", "") || "";
|
||||
|
||||
const statusColor = {
|
||||
true: theme.palette.success.main,
|
||||
false: theme.palette.error.main,
|
||||
undefined: theme.palette.warning.main,
|
||||
};
|
||||
|
||||
const statusMsg = {
|
||||
true: "Your site is up.",
|
||||
false: "Your site is down.",
|
||||
undefined: "Pending...",
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack className="configure-monitor" gap={theme.spacing(12)}>
|
||||
{Object.keys(monitor).length === 0 ? (
|
||||
@@ -210,13 +222,7 @@ const Configure = () => {
|
||||
flex={1}
|
||||
>
|
||||
<Stack direction="row" gap={theme.spacing(2)}>
|
||||
<PulseDot
|
||||
color={
|
||||
monitor?.status
|
||||
? theme.palette.success.main
|
||||
: theme.palette.error.main
|
||||
}
|
||||
/>
|
||||
<PulseDot color={statusColor[monitor?.status ?? undefined]} />
|
||||
<Box>
|
||||
{parsedUrl?.host ? (
|
||||
<Typography
|
||||
@@ -233,13 +239,9 @@ const Configure = () => {
|
||||
<Typography
|
||||
component="span"
|
||||
lineHeight={theme.spacing(12)}
|
||||
sx={{
|
||||
color: monitor?.status
|
||||
? theme.palette.success.main
|
||||
: theme.palette.error.text,
|
||||
}}
|
||||
sx={{ color: statusColor[monitor?.status ?? undefined] }}
|
||||
>
|
||||
Your site is {monitor?.status ? "up" : "down"}.
|
||||
{statusMsg[monitor?.status ?? undefined]}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
@@ -248,33 +250,42 @@ const Configure = () => {
|
||||
ml: "auto",
|
||||
}}
|
||||
>
|
||||
<ButtonSpinner
|
||||
isLoading={isLoading}
|
||||
level="tertiary"
|
||||
label={monitor?.isActive ? "Pause" : "Resume"}
|
||||
animate="rotate180"
|
||||
img={<PauseCircleOutlineIcon />}
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
loading={isLoading}
|
||||
sx={{
|
||||
border: "none",
|
||||
backgroundColor: theme.palette.background.main,
|
||||
pl: theme.spacing(4),
|
||||
pr: theme.spacing(6),
|
||||
px: theme.spacing(5),
|
||||
mr: theme.spacing(6),
|
||||
"& svg": {
|
||||
mr: theme.spacing(2),
|
||||
},
|
||||
}}
|
||||
onClick={handlePause}
|
||||
/>
|
||||
<ButtonSpinner
|
||||
isLoading={isLoading}
|
||||
level="error"
|
||||
label="Remove"
|
||||
sx={{
|
||||
boxShadow: "none",
|
||||
px: theme.spacing(8),
|
||||
}}
|
||||
>
|
||||
{monitor?.isActive ? (
|
||||
<>
|
||||
<PauseCircleOutlineIcon />
|
||||
Pause
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PlayCircleOutlineRoundedIcon />
|
||||
Resume
|
||||
</>
|
||||
)}
|
||||
</LoadingButton>
|
||||
<LoadingButton
|
||||
loading={isLoading}
|
||||
variant="contained"
|
||||
color="error"
|
||||
sx={{ px: theme.spacing(8) }}
|
||||
onClick={() => setIsOpen(true)}
|
||||
/>
|
||||
>
|
||||
Remove
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
<ConfigBox>
|
||||
@@ -381,13 +392,15 @@ const Configure = () => {
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<Stack direction="row" justifyContent="flex-end" mt="auto">
|
||||
<ButtonSpinner
|
||||
isLoading={isLoading}
|
||||
level="primary"
|
||||
label="Save"
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
loading={isLoading}
|
||||
sx={{ px: theme.spacing(12) }}
|
||||
onClick={handleSubmit}
|
||||
/>
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
@@ -440,11 +453,20 @@ const Configure = () => {
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Cancel"
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
<Button level="error" label="Delete" onClick={handleRemove} />
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="error"
|
||||
loading={isLoading}
|
||||
onClick={handleRemove}
|
||||
>
|
||||
Delete
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Box, ButtonGroup, Stack, Typography } from "@mui/material";
|
||||
import { Box, Button, ButtonGroup, Stack, Typography } from "@mui/material";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { monitorValidation } from "../../../Validation/validation";
|
||||
import { createUptimeMonitor } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
@@ -9,7 +9,6 @@ import { createToast } from "../../../Utils/toastUtils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { ConfigBox } from "../styled";
|
||||
import Radio from "../../../Components/Inputs/Radio";
|
||||
import Button from "../../../Components/Button";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import Checkbox from "../../../Components/Inputs/Checkbox";
|
||||
@@ -44,16 +43,16 @@ const CreateMonitor = () => {
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
if(monitorId){
|
||||
if (monitorId) {
|
||||
const data = monitors.find((monitor) => monitor._id === monitorId);
|
||||
if (!data) {
|
||||
console.error("Error fetching monitor of id: " + monitorId);
|
||||
navigate("/not-found", {replace: true});
|
||||
navigate("/not-found", { replace: true });
|
||||
}
|
||||
const {name, ...rest} = data; //data.name is read-only
|
||||
if(rest.type === 'http'){
|
||||
const url = new URL(rest.url)
|
||||
rest.url = url.host
|
||||
const { name, ...rest } = data; //data.name is read-only
|
||||
if (rest.type === "http") {
|
||||
const url = new URL(rest.url);
|
||||
rest.url = url.host;
|
||||
}
|
||||
rest.name = `${name} (Clone)`;
|
||||
rest.interval /= MS_PER_MINUTE;
|
||||
@@ -254,29 +253,19 @@ const CreateMonitor = () => {
|
||||
{monitor.type === "http" ? (
|
||||
<ButtonGroup sx={{ ml: "32px" }}>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="HTTPS"
|
||||
variant="group"
|
||||
filled={https.toString()}
|
||||
onClick={() => setHttps(true)}
|
||||
sx={{
|
||||
backgroundColor: https && theme.palette.background.fill,
|
||||
borderColor: theme.palette.border.dark,
|
||||
"&:hover": {
|
||||
borderColor: theme.palette.border.dark,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
>
|
||||
HTTPS
|
||||
</Button>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="HTTP"
|
||||
variant="group"
|
||||
filled={(!https).toString()}
|
||||
onClick={() => setHttps(false)}
|
||||
sx={{
|
||||
backgroundColor: !https && theme.palette.background.fill,
|
||||
borderColor: theme.palette.border.dark,
|
||||
"&:hover": {
|
||||
borderColor: theme.palette.border.dark,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
>
|
||||
HTTP
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
) : (
|
||||
""
|
||||
@@ -376,12 +365,13 @@ const CreateMonitor = () => {
|
||||
</ConfigBox>
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<Button
|
||||
id="create-monitor-btn"
|
||||
level="primary"
|
||||
label="Create monitor"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleCreateMonitor}
|
||||
disabled={Object.keys(errors).length !== 0 && true}
|
||||
/>
|
||||
>
|
||||
Create monitor
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
ResponsiveContainer,
|
||||
Cell,
|
||||
RadialBarChart,
|
||||
RadialBar,
|
||||
} from "recharts";
|
||||
import { formatDate } from "../../../../Utils/timeUtils";
|
||||
import { memo, useMemo, useState } from "react";
|
||||
|
||||
const CustomLabels = ({
|
||||
x,
|
||||
width,
|
||||
height,
|
||||
firstDataPoint,
|
||||
lastDataPoint,
|
||||
type,
|
||||
}) => {
|
||||
let options = {
|
||||
month: "short",
|
||||
year: undefined,
|
||||
hour: undefined,
|
||||
minute: undefined,
|
||||
};
|
||||
if (type === "day") delete options.hour;
|
||||
|
||||
return (
|
||||
<>
|
||||
<text x={x} y={height} dy={-3} textAnchor="start" fontSize={11}>
|
||||
{formatDate(new Date(firstDataPoint.time), options)}
|
||||
</text>
|
||||
<text x={width} y={height} dy={-3} textAnchor="end" fontSize={11}>
|
||||
{formatDate(new Date(lastDataPoint.time), options)}
|
||||
</text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const UpBarChart = memo(({ data, type, onBarHover }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [chartHovered, setChartHovered] = useState(false);
|
||||
const [hoveredBarIndex, setHoveredBarIndex] = useState(null);
|
||||
|
||||
const getColorRange = (uptime) => {
|
||||
return uptime > 80
|
||||
? { main: theme.palette.success.main, light: theme.palette.success.light }
|
||||
: uptime > 50
|
||||
? { main: theme.palette.warning.main, light: theme.palette.warning.light }
|
||||
: { main: theme.palette.error.text, light: theme.palette.error.light };
|
||||
};
|
||||
|
||||
// TODO - REMOVE THIS LATER
|
||||
const reversedData = useMemo(() => [...data].reverse(), [data]);
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" minWidth={210} height={155}>
|
||||
<BarChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
data={reversedData}
|
||||
onMouseEnter={() => {
|
||||
setChartHovered(true);
|
||||
onBarHover({ time: null, totalChecks: 0, uptimePercentage: 0 });
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setChartHovered(false);
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover(null);
|
||||
}}
|
||||
>
|
||||
<XAxis
|
||||
stroke={theme.palette.border.dark}
|
||||
height={15}
|
||||
tick={false}
|
||||
label={
|
||||
<CustomLabels
|
||||
x={0}
|
||||
y={0}
|
||||
width="100%"
|
||||
height="100%"
|
||||
firstDataPoint={reversedData[0]}
|
||||
lastDataPoint={reversedData[reversedData.length - 1]}
|
||||
type={type}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="totalChecks"
|
||||
maxBarSize={7}
|
||||
background={{ fill: "transparent" }}
|
||||
>
|
||||
{reversedData.map((entry, index) => {
|
||||
let { main, light } = getColorRange(entry.uptimePercentage);
|
||||
return (
|
||||
<Cell
|
||||
key={`cell-${entry.time}`}
|
||||
fill={
|
||||
hoveredBarIndex === index ? main : chartHovered ? light : main
|
||||
}
|
||||
onMouseEnter={() => {
|
||||
setHoveredBarIndex(index);
|
||||
onBarHover(entry);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover({
|
||||
time: null,
|
||||
totalChecks: 0,
|
||||
uptimePercentage: 0,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
});
|
||||
|
||||
export const DownBarChart = memo(({ data, type, onBarHover }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [chartHovered, setChartHovered] = useState(false);
|
||||
const [hoveredBarIndex, setHoveredBarIndex] = useState(null);
|
||||
|
||||
// TODO - REMOVE THIS LATER
|
||||
const reversedData = useMemo(() => [...data].reverse(), [data]);
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" minWidth={250} height={155}>
|
||||
<BarChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
data={reversedData}
|
||||
onMouseEnter={() => {
|
||||
setChartHovered(true);
|
||||
onBarHover({ time: null, totalIncidents: 0 });
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setChartHovered(false);
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover(null);
|
||||
}}
|
||||
>
|
||||
<XAxis
|
||||
stroke={theme.palette.border.dark}
|
||||
height={15}
|
||||
tick={false}
|
||||
label={
|
||||
<CustomLabels
|
||||
x={0}
|
||||
y={0}
|
||||
width="100%"
|
||||
height="100%"
|
||||
firstDataPoint={reversedData[0]}
|
||||
lastDataPoint={reversedData[reversedData.length - 1]}
|
||||
type={type}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="totalIncidents"
|
||||
maxBarSize={7}
|
||||
background={{ fill: "transparent" }}
|
||||
>
|
||||
{reversedData.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${entry.time}`}
|
||||
fill={
|
||||
hoveredBarIndex === index
|
||||
? theme.palette.error.text
|
||||
: chartHovered
|
||||
? theme.palette.error.light
|
||||
: theme.palette.error.text
|
||||
}
|
||||
onMouseEnter={() => {
|
||||
setHoveredBarIndex(index);
|
||||
onBarHover(entry);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHoveredBarIndex(null);
|
||||
onBarHover({ time: null, totalIncidents: 0 });
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
});
|
||||
|
||||
export const ResponseGaugeChart = ({ data }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
let max = 1000; // max ms
|
||||
|
||||
const memoizedData = useMemo(
|
||||
() => [{ response: max, fill: "transparent", background: false }, ...data],
|
||||
[data[0].response]
|
||||
);
|
||||
|
||||
let responseTime = Math.floor(memoizedData[1].response);
|
||||
let responseProps =
|
||||
responseTime <= 200
|
||||
? {
|
||||
category: "Excellent",
|
||||
main: theme.palette.success.main,
|
||||
bg: theme.palette.success.bg,
|
||||
}
|
||||
: responseTime <= 500
|
||||
? {
|
||||
category: "Fair",
|
||||
main: theme.palette.success.main,
|
||||
bg: theme.palette.success.bg,
|
||||
}
|
||||
: responseTime <= 600
|
||||
? {
|
||||
category: "Acceptable",
|
||||
main: theme.palette.warning.main,
|
||||
bg: theme.palette.warning.bg,
|
||||
}
|
||||
: {
|
||||
category: "Poor",
|
||||
main: theme.palette.error.text,
|
||||
bg: theme.palette.error.bg,
|
||||
};
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" minWidth={210} height={155}>
|
||||
<RadialBarChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
cy="89%"
|
||||
data={memoizedData}
|
||||
startAngle={180}
|
||||
endAngle={0}
|
||||
innerRadius={100}
|
||||
outerRadius={150}
|
||||
>
|
||||
<text x={0} y="100%" dx="5%" dy={-2} textAnchor="start" fontSize={11}>
|
||||
low
|
||||
</text>
|
||||
<text x="100%" y="100%" dx="-3%" dy={-2} textAnchor="end" fontSize={11}>
|
||||
high
|
||||
</text>
|
||||
<text
|
||||
x="50%"
|
||||
y="45%"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
fontSize={18}
|
||||
fontWeight={400}
|
||||
>
|
||||
{responseProps.category}
|
||||
</text>
|
||||
<text
|
||||
x="50%"
|
||||
y="55%"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="hanging"
|
||||
fontSize={25}
|
||||
>
|
||||
<tspan fontWeight={600}>{responseTime}</tspan>{" "}
|
||||
<tspan opacity={0.8}>ms</tspan>
|
||||
</text>
|
||||
<RadialBar
|
||||
background={{ fill: responseProps.bg }}
|
||||
clockWise
|
||||
dataKey="response"
|
||||
stroke="none"
|
||||
>
|
||||
<Cell fill="transparent" background={false} barSize={0} />
|
||||
<Cell fill={responseProps.main} />
|
||||
</RadialBar>
|
||||
</RadialBarChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
@@ -1,18 +1 @@
|
||||
.monitor-details h1.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large-plus);
|
||||
font-weight: 600;
|
||||
}
|
||||
.monitor-details h2.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
}
|
||||
.monitor-details h2.MuiTypography-root {
|
||||
font-weight: 600;
|
||||
}
|
||||
.monitor-details button.MuiButtonBase-root {
|
||||
height: var(--env-var-height-2);
|
||||
line-height: 1;
|
||||
}
|
||||
.monitor-details p.MuiTypography-root,
|
||||
.monitor-details p.MuiTypography-root span.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,140 +1,41 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { Box, Skeleton, Stack, Typography, useTheme } from "@mui/material";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Popover,
|
||||
Stack,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { networkService } from "../../../main";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import {
|
||||
formatDate,
|
||||
formatDuration,
|
||||
formatDurationRounded,
|
||||
formatDurationSplit,
|
||||
} from "../../../Utils/timeUtils";
|
||||
import MonitorDetailsAreaChart from "../../../Components/Charts/MonitorDetailsAreaChart";
|
||||
import ButtonGroup from "@mui/material/ButtonGroup";
|
||||
import Button from "../../../Components/Button";
|
||||
import SettingsIcon from "../../../assets/icons/settings-bold.svg?react";
|
||||
import CertificateIcon from "../../../assets/icons/certificate.svg?react";
|
||||
import UptimeIcon from "../../../assets/icons/uptime-icon.svg?react";
|
||||
import ResponseTimeIcon from "../../../assets/icons/response-time-icon.svg?react";
|
||||
import AverageResponseIcon from "../../../assets/icons/average-response-icon.svg?react";
|
||||
import IncidentsIcon from "../../../assets/icons/incidents.svg?react";
|
||||
import HistoryIcon from "../../../assets/icons/history-icon.svg?react";
|
||||
import PaginationTable from "./PaginationTable";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import PulseDot from "../../../Components/Animated/PulseDot";
|
||||
import { StatBox, ChartBox, IconBox } from "./styled";
|
||||
import { DownBarChart, ResponseGaugeChart, UpBarChart } from "./Charts";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
import "./index.css";
|
||||
|
||||
const StatBox = ({ title, value }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box
|
||||
className="stat-box"
|
||||
flex="20%"
|
||||
minWidth="100px"
|
||||
px={theme.spacing(8)}
|
||||
py={theme.spacing(4)}
|
||||
border={1}
|
||||
borderColor={theme.palette.border.light}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
backgroundColor={theme.palette.background.main}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
mb={theme.spacing(2)}
|
||||
fontSize={14}
|
||||
fontWeight={500}
|
||||
color={theme.palette.common.main}
|
||||
sx={{
|
||||
"& span": {
|
||||
color: theme.palette.text.accent,
|
||||
fontSize: 13,
|
||||
fontStyle: "italic",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h4"
|
||||
fontWeight={500}
|
||||
fontSize={13}
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
{value}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
StatBox.propTypes = {
|
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a skeleton layout.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Skeleton variant="rounded" width="20%" height={34} />
|
||||
<Stack gap={theme.spacing(20)} mt={theme.spacing(6)}>
|
||||
<Stack direction="row" gap={theme.spacing(4)} mt={theme.spacing(4)}>
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
style={{ minWidth: 24, minHeight: 24 }}
|
||||
/>
|
||||
<Box width="80%">
|
||||
<Skeleton variant="rounded" width="50%" height={24} />
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="50%"
|
||||
height={18}
|
||||
sx={{ mt: theme.spacing(4) }}
|
||||
/>
|
||||
</Box>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="20%"
|
||||
height={34}
|
||||
sx={{ alignSelf: "flex-end" }}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.spacing(12)}
|
||||
>
|
||||
<Skeleton variant="rounded" width="100%" height={80} />
|
||||
<Skeleton variant="rounded" width="100%" height={80} />
|
||||
<Skeleton variant="rounded" width="100%" height={80} />
|
||||
</Stack>
|
||||
<Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
mb={theme.spacing(8)}
|
||||
>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="20%"
|
||||
height={24}
|
||||
sx={{ alignSelf: "flex-end" }}
|
||||
/>
|
||||
<Skeleton variant="rounded" width="20%" height={34} />
|
||||
</Stack>
|
||||
<Box sx={{ height: "200px" }}>
|
||||
<Skeleton variant="rounded" width="100%" height="100%" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
<Skeleton variant="rounded" width="20%" height={24} />
|
||||
<Skeleton variant="rounded" width="100%" height={200} />
|
||||
<Skeleton variant="rounded" width="100%" height={50} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Details page component displaying monitor details and related information.
|
||||
* @component
|
||||
@@ -148,6 +49,14 @@ const DetailsPage = ({ isAdmin }) => {
|
||||
const [certificateExpiry, setCertificateExpiry] = useState("N/A");
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const openCertificate = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
const closeCertificate = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const fetchMonitor = useCallback(async () => {
|
||||
try {
|
||||
const res = await networkService.getStatsByMonitorId(
|
||||
@@ -180,7 +89,16 @@ const DetailsPage = ({ isAdmin }) => {
|
||||
authToken,
|
||||
monitorId
|
||||
);
|
||||
setCertificateExpiry(res?.data?.data?.certificateDate ?? "N/A");
|
||||
|
||||
let [month, day, year] = res?.data?.data?.certificateDate.split("/");
|
||||
const date = new Date(year, month - 1, day);
|
||||
|
||||
setCertificateExpiry(
|
||||
formatDate(date, {
|
||||
hour: undefined,
|
||||
minute: undefined,
|
||||
}) ?? "N/A"
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
@@ -188,7 +106,33 @@ const DetailsPage = ({ isAdmin }) => {
|
||||
fetchCertificate();
|
||||
}, [authToken, monitorId, monitor]);
|
||||
|
||||
const splitDuration = (duration) => {
|
||||
const { time, format } = formatDurationSplit(duration);
|
||||
return (
|
||||
<>
|
||||
{time}
|
||||
<Typography component="span">{format}</Typography>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
let loading = Object.keys(monitor).length === 0;
|
||||
|
||||
const [hoveredUptimeData, setHoveredUptimeData] = useState(null);
|
||||
const [hoveredIncidentsData, setHoveredIncidentsData] = useState(null);
|
||||
|
||||
const statusColor = {
|
||||
true: theme.palette.success.main,
|
||||
false: theme.palette.error.main,
|
||||
undefined: theme.palette.warning.main,
|
||||
};
|
||||
|
||||
const statusMsg = {
|
||||
true: "Your site is up.",
|
||||
false: "Your site is down.",
|
||||
undefined: "Pending...",
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="monitor-details">
|
||||
{loading ? (
|
||||
@@ -201,187 +145,378 @@ const DetailsPage = ({ isAdmin }) => {
|
||||
{ name: "details", path: `/monitors/${monitorId}` },
|
||||
]}
|
||||
/>
|
||||
<Stack gap={theme.spacing(12)} mt={theme.spacing(12)}>
|
||||
<Stack gap={theme.spacing(10)} mt={theme.spacing(10)}>
|
||||
<Stack direction="row" gap={theme.spacing(2)}>
|
||||
<PulseDot
|
||||
color={
|
||||
monitor?.status
|
||||
? theme.palette.success.main
|
||||
: theme.palette.error.main
|
||||
}
|
||||
/>
|
||||
<Box>
|
||||
<Typography
|
||||
component="h1"
|
||||
fontSize={22}
|
||||
fontWeight={500}
|
||||
color={theme.palette.text.primary}
|
||||
lineHeight={1}
|
||||
>
|
||||
{monitor.url?.replace(/^https?:\/\//, "") || "..."}
|
||||
{monitor.name}
|
||||
</Typography>
|
||||
<Typography
|
||||
mt={theme.spacing(4)}
|
||||
color={theme.palette.text.tertiary}
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="flex-end"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
color: monitor?.status
|
||||
? theme.palette.success.main
|
||||
: theme.palette.success.text,
|
||||
<Tooltip
|
||||
title={statusMsg[monitor?.status ?? undefined]}
|
||||
disableInteractive
|
||||
slotProps={{
|
||||
popper: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, -8],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
>
|
||||
Your site is {monitor?.status ? "up" : "down"}.
|
||||
</Typography>{" "}
|
||||
Checking every {formatDurationRounded(monitor?.interval)}.
|
||||
</Typography>
|
||||
<Box>
|
||||
<PulseDot
|
||||
color={statusColor[monitor?.status ?? undefined]}
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Typography
|
||||
component="h2"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
{monitor.url?.replace(/^https?:\/\//, "") || "..."}
|
||||
</Typography>
|
||||
<Typography
|
||||
ml={theme.spacing(6)}
|
||||
lineHeight="20px"
|
||||
fontSize={12}
|
||||
position="relative"
|
||||
color={theme.palette.text.tertiary}
|
||||
sx={{
|
||||
"&:before": {
|
||||
position: "absolute",
|
||||
content: `""`,
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: theme.palette.text.tertiary,
|
||||
opacity: 0.8,
|
||||
left: -9,
|
||||
top: "42%",
|
||||
},
|
||||
}}
|
||||
>
|
||||
Checking every {formatDurationRounded(monitor?.interval)}.
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
{isAdmin && (
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Configure"
|
||||
animate="rotate90"
|
||||
img={
|
||||
<SettingsIcon
|
||||
style={{
|
||||
minWidth: theme.spacing(10),
|
||||
minHeight: theme.spacing(10),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onClick={() => navigate(`/monitors/configure/${monitorId}`)}
|
||||
<Stack
|
||||
direction="row"
|
||||
height={34}
|
||||
sx={{
|
||||
ml: "auto",
|
||||
alignSelf: "flex-end",
|
||||
}}
|
||||
>
|
||||
<IconBox
|
||||
mr={theme.spacing(4)}
|
||||
onClick={openCertificate}
|
||||
sx={{
|
||||
ml: "auto",
|
||||
alignSelf: "flex-end",
|
||||
backgroundColor: theme.palette.background.fill,
|
||||
px: theme.spacing(6),
|
||||
"& svg": {
|
||||
mr: theme.spacing(3),
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<CertificateIcon />
|
||||
</IconBox>
|
||||
<Popover
|
||||
id="certificate-dropdown"
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={closeCertificate}
|
||||
disableScrollLock
|
||||
marginThreshold={null}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "center",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "center",
|
||||
}}
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: {
|
||||
mt: theme.spacing(4),
|
||||
py: theme.spacing(2),
|
||||
px: theme.spacing(4),
|
||||
width: 140,
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<Typography fontSize={12} color={theme.palette.text.tertiary}>
|
||||
Certificate Expiry
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize={13}
|
||||
color={theme.palette.text.primary}
|
||||
>
|
||||
{certificateExpiry}
|
||||
</Typography>
|
||||
</Popover>
|
||||
{isAdmin && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={() => navigate(`/monitors/configure/${monitorId}`)}
|
||||
sx={{
|
||||
px: theme.spacing(5),
|
||||
"& svg": {
|
||||
mr: theme.spacing(3),
|
||||
"& path": {
|
||||
stroke: theme.palette.text.tertiary,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SettingsIcon /> Configure
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.spacing(12)}
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<Stack direction="row" gap={theme.spacing(8)}>
|
||||
<StatBox
|
||||
title="Currently up for"
|
||||
value={formatDuration(monitor?.uptimeDuration)}
|
||||
/>
|
||||
<StatBox
|
||||
title="Last check"
|
||||
value={`${formatDurationRounded(monitor?.lastChecked)} ago`}
|
||||
/>
|
||||
<StatBox title="Incidents" value={monitor?.incidents} />
|
||||
<StatBox title="Certificate Expiry" value={certificateExpiry} />
|
||||
<StatBox
|
||||
title="Latest response time"
|
||||
value={monitor?.latestResponseTime}
|
||||
/>
|
||||
<StatBox
|
||||
title={
|
||||
<>
|
||||
Avg. Response Time{" "}
|
||||
<Typography component="span">(24-hr)</Typography>
|
||||
</>
|
||||
sx={
|
||||
monitor?.status === undefined
|
||||
? {
|
||||
backgroundColor: theme.palette.warning.light,
|
||||
borderColor: theme.palette.warning.border,
|
||||
"& h2": { color: theme.palette.warning.main },
|
||||
}
|
||||
: monitor?.status
|
||||
? {
|
||||
backgroundColor: theme.palette.success.bg,
|
||||
borderColor: theme.palette.success.light,
|
||||
"& h2": { color: theme.palette.success.main },
|
||||
}
|
||||
: {
|
||||
backgroundColor: theme.palette.error.bg,
|
||||
borderColor: theme.palette.error.light,
|
||||
"& h2": { color: theme.palette.error.main },
|
||||
}
|
||||
}
|
||||
value={parseFloat(monitor?.avgResponseTime24hours)
|
||||
.toFixed(2)
|
||||
.replace(/\.?0+$/, "")}
|
||||
/>
|
||||
<StatBox
|
||||
title={
|
||||
<>
|
||||
Uptime <Typography component="span">(24-hr)</Typography>
|
||||
</>
|
||||
}
|
||||
value={`${parseFloat(monitor?.uptime24Hours)
|
||||
.toFixed(2)
|
||||
.replace(/\.?0+$/, "")}%`}
|
||||
/>
|
||||
<StatBox
|
||||
title={
|
||||
<>
|
||||
Uptime <Typography component="span">(30-day)</Typography>
|
||||
</>
|
||||
}
|
||||
value={`${parseFloat(monitor?.uptime30Days)
|
||||
.toFixed(2)
|
||||
.replace(/\.?0+$/, "")}%`}
|
||||
/>
|
||||
>
|
||||
<Typography component="h2">active for</Typography>
|
||||
<Typography>
|
||||
{splitDuration(monitor?.uptimeDuration)}
|
||||
</Typography>
|
||||
</StatBox>
|
||||
<StatBox>
|
||||
<Typography component="h2">last check</Typography>
|
||||
<Typography>
|
||||
{splitDuration(monitor?.lastChecked)}
|
||||
<Typography component="span">ago</Typography>
|
||||
</Typography>
|
||||
</StatBox>
|
||||
<StatBox>
|
||||
<Typography component="h2">last response time</Typography>
|
||||
<Typography>
|
||||
{monitor?.latestResponseTime}
|
||||
<Typography component="span">ms</Typography>
|
||||
</Typography>
|
||||
</StatBox>
|
||||
</Stack>
|
||||
<Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="flex-end"
|
||||
gap={theme.spacing(4)}
|
||||
mb={theme.spacing(8)}
|
||||
>
|
||||
<Typography
|
||||
component="h2"
|
||||
alignSelf="flex-end"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
Response Times
|
||||
<Typography fontSize={12} color={theme.palette.text.tertiary}>
|
||||
Showing statistics for past{" "}
|
||||
{dateRange === "day"
|
||||
? "24 hours"
|
||||
: dateRange === "week"
|
||||
? "7 days"
|
||||
: "30 days"}
|
||||
.
|
||||
</Typography>
|
||||
<ButtonGroup
|
||||
<ButtonGroup sx={{ height: 32 }}>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(dateRange === "day").toString()}
|
||||
onClick={() => setDateRange("day")}
|
||||
>
|
||||
Day
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(dateRange === "week").toString()}
|
||||
onClick={() => setDateRange("week")}
|
||||
>
|
||||
Week
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(dateRange === "month").toString()}
|
||||
onClick={() => setDateRange("month")}
|
||||
>
|
||||
Month
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
<Stack direction="row" flexWrap="wrap" gap={theme.spacing(8)}>
|
||||
<ChartBox>
|
||||
<Stack>
|
||||
<IconBox>
|
||||
<UptimeIcon />
|
||||
</IconBox>
|
||||
<Typography component="h2">Uptime</Typography>
|
||||
</Stack>
|
||||
<Stack justifyContent="space-between">
|
||||
<Box position="relative">
|
||||
<Typography>Total Checks</Typography>
|
||||
<Typography component="span">
|
||||
{hoveredUptimeData !== null
|
||||
? hoveredUptimeData.totalChecks
|
||||
: monitor?.periodTotalChecks}
|
||||
</Typography>
|
||||
{hoveredUptimeData !== null &&
|
||||
hoveredUptimeData.time !== null && (
|
||||
<Typography
|
||||
component="h5"
|
||||
position="absolute"
|
||||
top="100%"
|
||||
fontSize={11}
|
||||
color={theme.palette.text.tertiary}
|
||||
>
|
||||
{formatDate(new Date(hoveredUptimeData.time), {
|
||||
month: "short",
|
||||
year: undefined,
|
||||
minute: undefined,
|
||||
hour: dateRange === "day" ? "numeric" : undefined,
|
||||
})}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography>Uptime Percentage</Typography>
|
||||
<Typography component="span">
|
||||
{hoveredUptimeData !== null
|
||||
? Math.floor(
|
||||
hoveredUptimeData.uptimePercentage * 10
|
||||
) / 10
|
||||
: Math.floor(monitor?.periodUptime * 10) / 10}
|
||||
<Typography component="span">%</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
<UpBarChart
|
||||
data={monitor?.aggregateData}
|
||||
type={dateRange}
|
||||
onBarHover={setHoveredUptimeData}
|
||||
/>
|
||||
</ChartBox>
|
||||
<ChartBox>
|
||||
<Stack>
|
||||
<IconBox>
|
||||
<IncidentsIcon />
|
||||
</IconBox>
|
||||
<Typography component="h2">Incidents</Typography>
|
||||
</Stack>
|
||||
<Box position="relative">
|
||||
<Typography>Total Incidents</Typography>
|
||||
<Typography component="span">
|
||||
{hoveredIncidentsData !== null
|
||||
? hoveredIncidentsData.totalIncidents
|
||||
: monitor?.periodIncidents}
|
||||
</Typography>
|
||||
{hoveredIncidentsData !== null &&
|
||||
hoveredIncidentsData.time !== null && (
|
||||
<Typography
|
||||
component="h5"
|
||||
position="absolute"
|
||||
top="100%"
|
||||
fontSize={11}
|
||||
color={theme.palette.text.tertiary}
|
||||
>
|
||||
{formatDate(new Date(hoveredIncidentsData.time), {
|
||||
month: "short",
|
||||
year: undefined,
|
||||
minute: undefined,
|
||||
hour: dateRange === "day" ? "numeric" : undefined,
|
||||
})}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<DownBarChart
|
||||
data={monitor?.aggregateData}
|
||||
type={dateRange}
|
||||
onBarHover={setHoveredIncidentsData}
|
||||
/>
|
||||
</ChartBox>
|
||||
<ChartBox justifyContent="space-between">
|
||||
<Stack>
|
||||
<IconBox>
|
||||
<AverageResponseIcon />
|
||||
</IconBox>
|
||||
<Typography component="h2">
|
||||
Average Response Time
|
||||
</Typography>
|
||||
</Stack>
|
||||
<ResponseGaugeChart
|
||||
data={[{ response: monitor?.periodAvgResponseTime }]}
|
||||
/>
|
||||
</ChartBox>
|
||||
<ChartBox
|
||||
sx={{
|
||||
"& .MuiButtonBase-root": {
|
||||
borderColor: theme.palette.border.light,
|
||||
"& tspan": {
|
||||
fontSize: 11,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Day"
|
||||
onClick={() => setDateRange("day")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
dateRange === "day" && theme.palette.background.fill,
|
||||
}}
|
||||
<Stack>
|
||||
<IconBox>
|
||||
<ResponseTimeIcon />
|
||||
</IconBox>
|
||||
<Typography component="h2">Response Times</Typography>
|
||||
</Stack>
|
||||
<MonitorDetailsAreaChart
|
||||
checks={[...monitor.checks].reverse()}
|
||||
/>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Week"
|
||||
onClick={() => setDateRange("week")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
dateRange === "week" && theme.palette.background.fill,
|
||||
}}
|
||||
</ChartBox>
|
||||
<ChartBox
|
||||
gap={theme.spacing(8)}
|
||||
sx={{
|
||||
flex: "100%",
|
||||
height: "fit-content",
|
||||
"& nav": { mt: theme.spacing(12) },
|
||||
}}
|
||||
>
|
||||
<Stack mb={theme.spacing(8)}>
|
||||
<IconBox>
|
||||
<HistoryIcon />
|
||||
</IconBox>
|
||||
<Typography
|
||||
component="h2"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
History
|
||||
</Typography>
|
||||
</Stack>
|
||||
<PaginationTable
|
||||
monitorId={monitorId}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Month"
|
||||
onClick={() => setDateRange("month")}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
dateRange === "month" && theme.palette.background.fill,
|
||||
}}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</ChartBox>
|
||||
</Stack>
|
||||
<Box
|
||||
p={theme.spacing(10)}
|
||||
pb={theme.spacing(2)}
|
||||
backgroundColor={theme.palette.background.main}
|
||||
border={1}
|
||||
borderColor={theme.palette.border.light}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
sx={{ height: "250px" }}
|
||||
>
|
||||
<MonitorDetailsAreaChart
|
||||
checks={[...monitor.checks].reverse()}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
<Typography component="h2" color={theme.palette.text.secondary}>
|
||||
History
|
||||
</Typography>
|
||||
<PaginationTable monitorId={monitorId} dateRange={dateRange} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Box, Skeleton, Stack, useTheme } from "@mui/material";
|
||||
|
||||
/**
|
||||
* Renders a skeleton layout.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Skeleton variant="rounded" width="20%" height={34} />
|
||||
<Stack gap={theme.spacing(20)} mt={theme.spacing(6)}>
|
||||
<Stack direction="row" gap={theme.spacing(4)} mt={theme.spacing(4)}>
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
style={{ minWidth: 24, minHeight: 24 }}
|
||||
/>
|
||||
<Box width="80%">
|
||||
<Skeleton variant="rounded" width="50%" height={24} />
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="50%"
|
||||
height={18}
|
||||
sx={{ mt: theme.spacing(4) }}
|
||||
/>
|
||||
</Box>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="20%"
|
||||
height={34}
|
||||
sx={{ alignSelf: "flex-end" }}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
gap={theme.spacing(12)}
|
||||
>
|
||||
<Skeleton variant="rounded" width="100%" height={80} />
|
||||
<Skeleton variant="rounded" width="100%" height={80} />
|
||||
<Skeleton variant="rounded" width="100%" height={80} />
|
||||
</Stack>
|
||||
<Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
mb={theme.spacing(8)}
|
||||
>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="20%"
|
||||
height={24}
|
||||
sx={{ alignSelf: "flex-end" }}
|
||||
/>
|
||||
<Skeleton variant="rounded" width="20%" height={34} />
|
||||
</Stack>
|
||||
<Box sx={{ height: "200px" }}>
|
||||
<Skeleton variant="rounded" width="100%" height="100%" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
<Skeleton variant="rounded" width="20%" height={24} />
|
||||
<Skeleton variant="rounded" width="100%" height={200} />
|
||||
<Skeleton variant="rounded" width="100%" height={50} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Box, Stack, styled } from "@mui/material";
|
||||
|
||||
export const ChartBox = styled(Stack)(({ theme }) => ({
|
||||
flex: "1 30%",
|
||||
gap: theme.spacing(8),
|
||||
height: 300,
|
||||
minWidth: 250,
|
||||
padding: theme.spacing(8),
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
"& h2": {
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: 15,
|
||||
fontWeight: 500,
|
||||
},
|
||||
"& .MuiBox-root:not(.area-tooltip) p": {
|
||||
color: theme.palette.text.tertiary,
|
||||
fontSize: 13,
|
||||
},
|
||||
"& .MuiBox-root > span": {
|
||||
color: theme.palette.text.primary,
|
||||
fontSize: 20,
|
||||
"& span": {
|
||||
opacity: 0.8,
|
||||
marginLeft: 2,
|
||||
fontSize: 15,
|
||||
},
|
||||
},
|
||||
"& .MuiStack-root": {
|
||||
flexDirection: "row",
|
||||
gap: theme.spacing(6),
|
||||
},
|
||||
"& .MuiStack-root:first-of-type": {
|
||||
alignItems: "center",
|
||||
},
|
||||
"& tspan, & text": {
|
||||
fill: theme.palette.text.tertiary,
|
||||
},
|
||||
"& path": {
|
||||
transition: "fill 300ms ease",
|
||||
},
|
||||
}));
|
||||
|
||||
export const IconBox = styled(Box)(({ theme }) => ({
|
||||
height: 34,
|
||||
minWidth: 34,
|
||||
width: 34,
|
||||
position: "relative",
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.dark,
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
"& svg": {
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 20,
|
||||
height: 20,
|
||||
"& path": {
|
||||
stroke: theme.palette.text.tertiary,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
export const StatBox = styled(Box)(({ theme }) => ({
|
||||
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
|
||||
minWidth: 200,
|
||||
width: 225,
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
"& h2": {
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
color: theme.palette.text.secondary,
|
||||
textTransform: "uppercase",
|
||||
},
|
||||
"& p": {
|
||||
fontSize: 18,
|
||||
color: theme.palette.text.primary,
|
||||
marginTop: theme.spacing(2),
|
||||
"& span": {
|
||||
color: theme.palette.text.tertiary,
|
||||
marginLeft: theme.spacing(2),
|
||||
fontSize: 15,
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,352 @@
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
TableContainer,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
Paper,
|
||||
Box,
|
||||
TablePagination,
|
||||
Stack,
|
||||
Typography,
|
||||
Button,
|
||||
} from "@mui/material";
|
||||
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
|
||||
|
||||
import { setRowsPerPage } from "../../../../Features/UI/uiSlice";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { logger } from "../../../../Utils/Logger";
|
||||
import Host from "../host";
|
||||
import { StatusLabel } from "../../../../Components/Label";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { networkService } from "../../../../main";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import BarChart from "../../../../Components/Charts/BarChart";
|
||||
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 ActionsMenu from "../actionsMenu";
|
||||
|
||||
/**
|
||||
* 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
|
||||
variant="group"
|
||||
onClick={handleFirstPageButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="first page"
|
||||
>
|
||||
<LeftArrowDouble />
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
onClick={handleBackButtonClick}
|
||||
disabled={page === 0}
|
||||
aria-label="previous page"
|
||||
>
|
||||
<LeftArrow />
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
onClick={handleNextButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="next page"
|
||||
>
|
||||
<RightArrow />
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
onClick={handleLastPageButtonClick}
|
||||
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
|
||||
aria-label="last page"
|
||||
>
|
||||
<RightArrowDouble />
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
TablePaginationActions.propTypes = {
|
||||
count: PropTypes.number.isRequired,
|
||||
page: PropTypes.number.isRequired,
|
||||
rowsPerPage: PropTypes.number.isRequired,
|
||||
onPageChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const MonitorTable = ({ isAdmin }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { rowsPerPage } = useSelector((state) => state.ui.monitors);
|
||||
const [page, setPage] = useState(0);
|
||||
const [monitors, setMonitors] = useState([]);
|
||||
const [monitorCount, setMonitorCount] = useState(0);
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const [updateTrigger, setUpdateTrigger] = useState(false);
|
||||
|
||||
const handleActionMenuDelete = () => {
|
||||
setUpdateTrigger((prev) => !prev);
|
||||
};
|
||||
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
dispatch(
|
||||
setRowsPerPage({
|
||||
value: parseInt(event.target.value, 10),
|
||||
table: "monitors",
|
||||
})
|
||||
);
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPage = async () => {
|
||||
try {
|
||||
const { authToken } = authState;
|
||||
const user = jwtDecode(authToken);
|
||||
const res = await networkService.getMonitorsByTeamId(
|
||||
authToken,
|
||||
user.teamId,
|
||||
25,
|
||||
["http", "ping"],
|
||||
null,
|
||||
"desc",
|
||||
true,
|
||||
page,
|
||||
rowsPerPage
|
||||
);
|
||||
setMonitors(res?.data?.data?.monitors ?? []);
|
||||
setMonitorCount(res?.data?.data?.monitorCount ?? 0);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
};
|
||||
fetchPage();
|
||||
}, [updateTrigger, authState, page, rowsPerPage]);
|
||||
|
||||
/**
|
||||
* 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, monitorCount);
|
||||
return `${start} - ${end}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Host</TableCell>
|
||||
<TableCell>
|
||||
{" "}
|
||||
<Box width="max-content">
|
||||
Status
|
||||
<span>
|
||||
<ArrowDownwardRoundedIcon />
|
||||
</span>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>Response Time</TableCell>
|
||||
<TableCell>Type</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{monitors.map((monitor) => {
|
||||
let uptimePercentage = "";
|
||||
let percentageColor = theme.palette.percentage.uptimeExcellent;
|
||||
|
||||
// Determine uptime percentage and color based on the monitor's uptimePercentage value
|
||||
if (monitor.uptimePercentage !== undefined) {
|
||||
uptimePercentage =
|
||||
monitor.uptimePercentage === 0
|
||||
? "0"
|
||||
: (monitor.uptimePercentage * 100).toFixed(2);
|
||||
|
||||
percentageColor =
|
||||
monitor.uptimePercentage < 0.25
|
||||
? theme.palette.percentage.uptimePoor
|
||||
: monitor.uptimePercentage < 0.5
|
||||
? theme.palette.percentage.uptimeFair
|
||||
: monitor.uptimePercentage < 0.75
|
||||
? theme.palette.percentage.uptimeGood
|
||||
: theme.palette.percentage.uptimeExcellent;
|
||||
}
|
||||
|
||||
const params = {
|
||||
url: monitor.url,
|
||||
title: monitor.name,
|
||||
percentage: uptimePercentage,
|
||||
percentageColor,
|
||||
status:
|
||||
monitor.status === undefined
|
||||
? "pending"
|
||||
: monitor.status === true
|
||||
? "up"
|
||||
: "down",
|
||||
};
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={monitor._id}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
navigate(`/monitors/${monitor._id}`);
|
||||
}}
|
||||
>
|
||||
<TableCell>
|
||||
<Host key={monitor._id} params={params} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StatusLabel
|
||||
status={params.status}
|
||||
text={params.status}
|
||||
customStyles={{ textTransform: "capitalize" }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<BarChart checks={monitor.checks.slice().reverse()} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span style={{ textTransform: "uppercase" }}>
|
||||
{monitor.type}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ActionsMenu
|
||||
monitor={monitor}
|
||||
isAdmin={isAdmin}
|
||||
updateCallback={handleActionMenuDelete}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
px={theme.spacing(4)}
|
||||
sx={{
|
||||
"& p": {
|
||||
color: theme.palette.text.tertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography px={theme.spacing(2)} fontSize={12} sx={{ opacity: 0.7 }}>
|
||||
Showing {getRange()} of {monitorCount} monitor(s)
|
||||
</Typography>
|
||||
<TablePagination
|
||||
component="div"
|
||||
count={monitorCount}
|
||||
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",
|
||||
sx: {
|
||||
mt: 0,
|
||||
mb: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
transformOrigin: { vertical: "bottom", horizontal: "left" },
|
||||
anchorOrigin: { vertical: "top", horizontal: "left" },
|
||||
sx: { mt: theme.spacing(-2) },
|
||||
},
|
||||
inputProps: { id: "pagination-dropdown" },
|
||||
IconComponent: SelectorVertical,
|
||||
sx: {
|
||||
ml: theme.spacing(4),
|
||||
mr: theme.spacing(12),
|
||||
minWidth: theme.spacing(20),
|
||||
textAlign: "left",
|
||||
"&.Mui-focused > div": {
|
||||
backgroundColor: theme.palette.background.main,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
mt: theme.spacing(6),
|
||||
color: theme.palette.text.secondary,
|
||||
"& svg path": {
|
||||
stroke: theme.palette.text.tertiary,
|
||||
strokeWidth: 1.3,
|
||||
},
|
||||
"& .MuiSelect-select": {
|
||||
border: 1,
|
||||
borderColor: theme.palette.border.light,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
MonitorTable.propTypes = {
|
||||
isAdmin: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default MonitorTable;
|
||||
@@ -2,7 +2,7 @@ import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import Arrow from "../../../assets/icons/top-right-arrow.svg?react";
|
||||
import background from "../../../assets/Images/background-grid.svg";
|
||||
import Background from "../../../assets/Images/background-grid.svg?react";
|
||||
import ClockSnooze from "../../../assets/icons/clock-snooze.svg?react";
|
||||
|
||||
const StatusBox = ({ title, value }) => {
|
||||
@@ -48,26 +48,27 @@ const StatusBox = ({ title, value }) => {
|
||||
borderColor={theme.palette.border.light}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
backgroundColor={theme.palette.background.main}
|
||||
px={theme.spacing(12)}
|
||||
py={theme.spacing(8)}
|
||||
p={theme.spacing(8)}
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
"&:after": {
|
||||
position: "absolute",
|
||||
content: `""`,
|
||||
backgroundImage: `url(${background})`,
|
||||
width: "400px",
|
||||
height: "200px",
|
||||
top: "-10%",
|
||||
left: "5%",
|
||||
zIndex: 10000,
|
||||
pointerEvents: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "-10%",
|
||||
left: "5%",
|
||||
pointerEvents: "none",
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.other.grid,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background />
|
||||
</Box>
|
||||
<Box
|
||||
textTransform="uppercase"
|
||||
fontSize={15}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTheme } from "@emotion/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
@@ -16,10 +17,9 @@ import {
|
||||
getUptimeMonitorsByTeamId,
|
||||
} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import Settings from "../../../assets/icons/settings-bold.svg?react";
|
||||
import Button from "../../../Components/Button";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const ActionsMenu = ({ monitor, isAdmin }) => {
|
||||
const ActionsMenu = ({ monitor, isAdmin, updateCallback }) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [actions, setActions] = useState({});
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -37,6 +37,7 @@ const ActionsMenu = ({ monitor, isAdmin }) => {
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
setIsOpen(false); // close modal
|
||||
dispatch(getUptimeMonitorsByTeamId(authState.authToken));
|
||||
updateCallback();
|
||||
createToast({ body: "Monitor deleted successfully." });
|
||||
} else {
|
||||
createToast({ body: "Failed to delete monitor." });
|
||||
@@ -93,17 +94,8 @@ const ActionsMenu = ({ monitor, isAdmin }) => {
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: {
|
||||
"& ul": {
|
||||
p: theme.spacing(2.5),
|
||||
minWidth: "100px",
|
||||
},
|
||||
"& li": {
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: 13,
|
||||
px: theme.spacing(4),
|
||||
py: theme.spacing(2.5),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
"& ul": { p: theme.spacing(2.5) },
|
||||
"& li": { m: 0 },
|
||||
"& li:last-of-type": {
|
||||
color: theme.palette.error.text,
|
||||
},
|
||||
@@ -206,21 +198,25 @@ const ActionsMenu = ({ monitor, isAdmin }) => {
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Cancel"
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
level="error"
|
||||
label="Delete"
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(e);
|
||||
handleRemove(e);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
@@ -235,6 +231,7 @@ ActionsMenu.propTypes = {
|
||||
type: PropTypes.string,
|
||||
}).isRequired,
|
||||
isAdmin: PropTypes.bool,
|
||||
updateCallback: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ActionsMenu;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { Button, Stack, Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import Button from "../../../Components/Button";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
@@ -25,13 +24,15 @@ const Fallback = ({ isAdmin }) => {
|
||||
</Typography>
|
||||
{isAdmin && (
|
||||
<Button
|
||||
level="primary"
|
||||
label="Create your first monitor"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
navigate("/monitors/create");
|
||||
}}
|
||||
sx={{ mt: theme.spacing(12) }}
|
||||
/>
|
||||
>
|
||||
Create your first monitor
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -53,7 +53,8 @@ Host.propTypes = {
|
||||
params: PropTypes.shape({
|
||||
title: PropTypes.string,
|
||||
percentageColor: PropTypes.string,
|
||||
percentage: PropTypes.number,
|
||||
percentage: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -3,16 +3,15 @@ import { useEffect } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { getUptimeMonitorsByTeamId } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Button from "../../../Components/Button";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import BasicTable from "../../../Components/BasicTable";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { Box, Button, Stack, Typography } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
import Fallback from "./fallback";
|
||||
import StatusBox from "./StatusBox";
|
||||
import { buildData } from "./monitorData";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import Greeting from "../../../Utils/greeting";
|
||||
import MonitorTable from "./MonitorTable";
|
||||
|
||||
const Monitors = ({ isAdmin }) => {
|
||||
const theme = useTheme();
|
||||
@@ -24,43 +23,11 @@ const Monitors = ({ isAdmin }) => {
|
||||
useEffect(() => {
|
||||
dispatch(getUptimeMonitorsByTeamId(authState.authToken));
|
||||
}, [authState.authToken, dispatch]);
|
||||
|
||||
const monitorStats = monitorState.monitors.reduce(
|
||||
(acc, monitor) => {
|
||||
if (monitor.isActive === false) {
|
||||
acc["paused"] += 1;
|
||||
} else if (monitor.status === true) {
|
||||
acc["up"] += 1;
|
||||
} else {
|
||||
acc["down"] += 1;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{ paused: 0, up: 0, down: 0 }
|
||||
);
|
||||
|
||||
const data = buildData(monitorState.monitors, isAdmin, navigate);
|
||||
|
||||
let loading = monitorState.isLoading && monitorState.monitors.length === 0;
|
||||
|
||||
const now = new Date();
|
||||
const hour = now.getHours();
|
||||
|
||||
let greeting = "";
|
||||
let emoji = "";
|
||||
if (hour < 12) {
|
||||
greeting = "morning";
|
||||
emoji = "🌅";
|
||||
} else if (hour < 18) {
|
||||
greeting = "afternoon";
|
||||
emoji = "🌞";
|
||||
} else {
|
||||
greeting = "evening";
|
||||
emoji = "🌙";
|
||||
}
|
||||
|
||||
let loading =
|
||||
monitorState?.isLoading &&
|
||||
monitorState?.monitorsSummary?.monitors?.length === 0;
|
||||
return (
|
||||
<Stack className="monitors" gap={theme.spacing(12)}>
|
||||
<Stack className="monitors" gap={theme.spacing(8)}>
|
||||
{loading ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
@@ -73,67 +40,52 @@ const Monitors = ({ isAdmin }) => {
|
||||
alignItems="center"
|
||||
mt={theme.spacing(5)}
|
||||
>
|
||||
<Box>
|
||||
<Typography
|
||||
component="h1"
|
||||
lineHeight={1}
|
||||
color={theme.palette.text.primary}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
Good {greeting},{" "}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
fontWeight="inherit"
|
||||
>
|
||||
{authState.user.firstName} {emoji}
|
||||
</Typography>
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{ opacity: 0.8 }}
|
||||
lineHeight={1}
|
||||
fontWeight={300}
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
Here’s an overview of your uptime monitors.
|
||||
</Typography>
|
||||
</Box>
|
||||
{monitorState.monitors?.length !== 0 && (
|
||||
<Greeting type="uptime" />
|
||||
{monitorState?.monitorsSummary?.monitors?.length !== 0 && (
|
||||
<Button
|
||||
level="primary"
|
||||
label="Create monitor"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
navigate("/monitors/create");
|
||||
}}
|
||||
sx={{ fontWeight: 500 }}
|
||||
/>
|
||||
>
|
||||
Create monitor
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
{isAdmin && monitorState.monitors?.length === 0 && (
|
||||
{isAdmin && monitorState?.monitorsSummary?.monitors?.length === 0 && (
|
||||
<Fallback isAdmin={isAdmin} />
|
||||
)}
|
||||
|
||||
{monitorState.monitors?.length !== 0 && (
|
||||
{monitorState?.monitorsSummary?.monitors?.length !== 0 && (
|
||||
<>
|
||||
<Stack
|
||||
gap={theme.spacing(12)}
|
||||
gap={theme.spacing(8)}
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<StatusBox title="up" value={monitorStats.up} />
|
||||
<StatusBox title="down" value={monitorStats.down} />
|
||||
<StatusBox title="paused" value={monitorStats.paused} />
|
||||
<StatusBox
|
||||
title="up"
|
||||
value={monitorState?.monitorsSummary?.monitorCounts?.up ?? 0}
|
||||
/>
|
||||
<StatusBox
|
||||
title="down"
|
||||
value={
|
||||
monitorState?.monitorsSummary?.monitorCounts?.down ?? 0
|
||||
}
|
||||
/>
|
||||
<StatusBox
|
||||
title="paused"
|
||||
value={
|
||||
monitorState?.monitorsSummary?.monitorCounts?.paused ?? 0
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<Box
|
||||
flex={1}
|
||||
px={theme.spacing(16)}
|
||||
py={theme.spacing(12)}
|
||||
p={theme.spacing(10)}
|
||||
border={1}
|
||||
borderColor={theme.palette.border.light}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
@@ -142,7 +94,7 @@ const Monitors = ({ isAdmin }) => {
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
mb={theme.spacing(12)}
|
||||
mb={theme.spacing(8)}
|
||||
>
|
||||
<Typography
|
||||
component="h2"
|
||||
@@ -158,11 +110,12 @@ const Monitors = ({ isAdmin }) => {
|
||||
borderColor={theme.palette.border.light}
|
||||
backgroundColor={theme.palette.background.accent}
|
||||
>
|
||||
{monitorState.monitors.length}
|
||||
{monitorState?.monitorsSummary?.monitors?.monitors
|
||||
?.length || 0}
|
||||
</Box>
|
||||
{/* TODO - add search bar */}
|
||||
</Stack>
|
||||
<BasicTable data={data} paginated={true} table={"monitors"} />
|
||||
<MonitorTable isAdmin={isAdmin} />
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -27,22 +27,52 @@ const data = {
|
||||
rows: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds table data for a list of monitors.
|
||||
*
|
||||
* @param {Array} monitors - An array of monitor objects containing information about each monitor.
|
||||
* @param {boolean} isAdmin - Flag indicating if the current user is an admin.
|
||||
* @param {Function} navigate - A function to navigate to the monitor detail page.
|
||||
* @returns {Object} The data structure containing columns and rows for the table.
|
||||
*/
|
||||
export const buildData = (monitors, isAdmin, navigate) => {
|
||||
const theme = useTheme();
|
||||
|
||||
data.rows = monitors.map((monitor, idx) => {
|
||||
let uptimePercentage = "";
|
||||
let percentageColor = theme.palette.percentage.uptimeExcellent;
|
||||
|
||||
// Determine uptime percentage and color based on the monitor's uptimePercentage value
|
||||
if (monitor.uptimePercentage !== undefined) {
|
||||
uptimePercentage =
|
||||
monitor.uptimePercentage === 0
|
||||
? "0"
|
||||
: (monitor.uptimePercentage * 100).toFixed(2);
|
||||
|
||||
percentageColor =
|
||||
monitor.uptimePercentage < 0.25
|
||||
? theme.palette.percentage.uptimePoor
|
||||
: monitor.uptimePercentage < 0.5
|
||||
? theme.palette.percentage.uptimeFair
|
||||
: monitor.uptimePercentage < 0.75
|
||||
? theme.palette.percentage.uptimeGood
|
||||
: theme.palette.percentage.uptimeExcellent;
|
||||
}
|
||||
|
||||
const params = {
|
||||
url: monitor.url,
|
||||
title: monitor.name,
|
||||
percentage: 100,
|
||||
percentageColor:
|
||||
monitor.status === true
|
||||
? theme.palette.success.main
|
||||
: theme.palette.error.text,
|
||||
status: monitor.status === true ? "up" : "down",
|
||||
percentage: uptimePercentage,
|
||||
percentageColor,
|
||||
status:
|
||||
monitor.status === undefined
|
||||
? "pending"
|
||||
: monitor.status === true
|
||||
? "up"
|
||||
: "down",
|
||||
};
|
||||
|
||||
// Reverse checks so latest check is on the right
|
||||
// Reverse checks so the latest check is on the right
|
||||
const reversedChecks = monitor.checks.slice().reverse();
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import Button from "../../Components/Button";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import NotFoundSvg from "../../../src/assets/Images/sushi_404.svg";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { Button, Stack, Typography } from "@mui/material";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
@@ -48,11 +47,13 @@ const NotFound = ({ title = DefaultValue.title, desc = DefaultValue.desc }) => {
|
||||
</Typography>
|
||||
<Typography fontSize={13}>{desc}</Typography>
|
||||
<Button
|
||||
label="Go to the main dashboard"
|
||||
level="primary"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ mt: theme.spacing(10) }}
|
||||
onClick={() => navigate("/")}
|
||||
/>
|
||||
>
|
||||
Go to the main dashboard
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -1,80 +1,36 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Modal, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { Box, Button, Modal, Stack, Typography } from "@mui/material";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import {
|
||||
deletePageSpeed,
|
||||
getPagespeedMonitorById,
|
||||
getPageSpeedByTeamId,
|
||||
updatePageSpeed,
|
||||
pausePageSpeed,
|
||||
} from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
|
||||
import { monitorValidation } from "../../../Validation/validation";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import Button from "../../../Components/Button";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import Checkbox from "../../../Components/Inputs/Checkbox";
|
||||
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import PulseDot from "../../../Components/Animated/PulseDot";
|
||||
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
* Renders a skeleton layout.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Skeleton variant="rounded" width="15%" height={34} />
|
||||
<Stack gap={theme.spacing(20)} mt={theme.spacing(6)} maxWidth="1000px">
|
||||
<Stack direction="row" gap={theme.spacing(4)} mt={theme.spacing(4)}>
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
style={{ minWidth: 24, minHeight: 24 }}
|
||||
/>
|
||||
<Box width="80%">
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="50%"
|
||||
height={24}
|
||||
sx={{ mb: theme.spacing(4) }}
|
||||
/>
|
||||
<Skeleton variant="rounded" width="50%" height={18} />
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(6)}
|
||||
sx={{
|
||||
ml: "auto",
|
||||
alignSelf: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Skeleton variant="rounded" width={100} height={34} />
|
||||
<Skeleton variant="rounded" width={100} height={34} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Skeleton variant="rounded" width="100%" height={500} />
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<Skeleton variant="rounded" width="15%" height={34} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const PageSpeedConfigure = () => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const MS_PER_MINUTE = 60000;
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const { monitors } = useSelector((state) => state.pageSpeedMonitors);
|
||||
const { isLoading } = useSelector((state) => state.pageSpeedMonitors);
|
||||
const { monitorId } = useParams();
|
||||
const [monitor, setMonitor] = useState({});
|
||||
const [errors, setErrors] = useState({});
|
||||
@@ -90,15 +46,25 @@ const PageSpeedConfigure = () => {
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const data = monitors.find((monitor) => monitor._id === monitorId);
|
||||
if (!data) {
|
||||
logger.error("Error fetching pagespeed monitor of id: " + monitorId);
|
||||
navigate("/not-found", { replace: true });
|
||||
}
|
||||
setMonitor({
|
||||
...data,
|
||||
});
|
||||
}, [monitorId, monitors, navigate]);
|
||||
const fetchMonitor = async () => {
|
||||
try {
|
||||
const action = await dispatch(
|
||||
getPagespeedMonitorById({ authToken, monitorId })
|
||||
);
|
||||
|
||||
if (getPagespeedMonitorById.fulfilled.match(action)) {
|
||||
const monitor = action.payload.data;
|
||||
setMonitor(monitor);
|
||||
} else if (getPagespeedMonitorById.rejected.match(action)) {
|
||||
throw new Error(action.error.message);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error fetching monitor of id: " + monitorId);
|
||||
navigate("/not-found", { replace: true });
|
||||
}
|
||||
};
|
||||
fetchMonitor();
|
||||
}, [dispatch, authToken, monitorId, navigate]);
|
||||
|
||||
const handleChange = (event, id) => {
|
||||
let { value } = event.target;
|
||||
@@ -120,6 +86,21 @@ const PageSpeedConfigure = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handlePause = async () => {
|
||||
try {
|
||||
const action = await dispatch(pausePageSpeed({ authToken, monitorId }));
|
||||
if (pausePageSpeed.fulfilled.match(action)) {
|
||||
const monitor = action.payload.data;
|
||||
setMonitor(monitor);
|
||||
} else if (pausePageSpeed.rejected.match(action)) {
|
||||
throw new Error(action.error.message);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error pausing monitor: " + monitorId);
|
||||
createToast({ body: "Failed to pause monitor" });
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async (event) => {
|
||||
event.preventDefault();
|
||||
const action = await dispatch(
|
||||
@@ -144,11 +125,9 @@ const PageSpeedConfigure = () => {
|
||||
}
|
||||
};
|
||||
|
||||
let loading = Object.keys(monitor).length === 0;
|
||||
|
||||
return (
|
||||
<Stack className="configure-pagespeed" gap={theme.spacing(12)}>
|
||||
{loading ? (
|
||||
{Object.keys(monitor).length === 0 ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
<>
|
||||
@@ -196,30 +175,46 @@ const PageSpeedConfigure = () => {
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box alignSelf="flex-end" ml="auto">
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Pause"
|
||||
animate="rotate180"
|
||||
img={<PauseCircleOutlineIcon />}
|
||||
<LoadingButton
|
||||
onClick={handlePause}
|
||||
loading={isLoading}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.fill,
|
||||
pl: theme.spacing(4),
|
||||
pr: theme.spacing(6),
|
||||
"& svg": {
|
||||
mr: theme.spacing(2),
|
||||
"& path": {
|
||||
stroke: theme.palette.other.icon,
|
||||
strokeWidth: 0.1,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level="error"
|
||||
label="Remove"
|
||||
>
|
||||
{monitor?.isActive ? (
|
||||
<>
|
||||
<PauseCircleOutlineIcon />
|
||||
Pause
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PlayCircleOutlineRoundedIcon />
|
||||
Resume
|
||||
</>
|
||||
)}
|
||||
</LoadingButton>
|
||||
<LoadingButton
|
||||
loading={isLoading}
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => setIsOpen(true)}
|
||||
sx={{
|
||||
boxShadow: "none",
|
||||
px: theme.spacing(8),
|
||||
ml: theme.spacing(6),
|
||||
}}
|
||||
onClick={() => setIsOpen(true)}
|
||||
/>
|
||||
>
|
||||
Remove
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack
|
||||
@@ -319,13 +314,16 @@ const PageSpeedConfigure = () => {
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack direction="row" justifyContent="flex-end" mt="auto">
|
||||
<Button
|
||||
<LoadingButton
|
||||
loading={isLoading}
|
||||
type="submit"
|
||||
level="primary"
|
||||
label="Save"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleSave}
|
||||
sx={{ px: theme.spacing(12), mt: theme.spacing(12) }}
|
||||
/>
|
||||
sx={{ px: theme.spacing(12) }}
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
@@ -376,11 +374,15 @@ const PageSpeedConfigure = () => {
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Cancel"
|
||||
variant="text"
|
||||
color="info"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
<Button level="error" label="Delete" onClick={handleRemove} />
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="contained" color="error" onClick={handleRemove}>
|
||||
Delete
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Box, Skeleton, Stack } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
/**
|
||||
* Renders a skeleton layout.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Skeleton variant="rounded" width="15%" height={34} />
|
||||
<Stack gap={theme.spacing(20)} mt={theme.spacing(6)} maxWidth="1000px">
|
||||
<Stack direction="row" gap={theme.spacing(4)} mt={theme.spacing(4)}>
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
style={{ minWidth: 24, minHeight: 24 }}
|
||||
/>
|
||||
<Box width="80%">
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="50%"
|
||||
height={24}
|
||||
sx={{ mb: theme.spacing(4) }}
|
||||
/>
|
||||
<Skeleton variant="rounded" width="50%" height={18} />
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(6)}
|
||||
sx={{
|
||||
ml: "auto",
|
||||
alignSelf: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Skeleton variant="rounded" width={100} height={34} />
|
||||
<Skeleton variant="rounded" width={100} height={34} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Skeleton variant="rounded" width="100%" height={500} />
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<Skeleton variant="rounded" width="15%" height={34} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -22,6 +22,7 @@
|
||||
.create-page-speed .section-disabled {
|
||||
opacity: 0.4;
|
||||
padding: 10px;
|
||||
margin-right: -5px;
|
||||
border-radius: 8px;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
|
||||
@@ -1,27 +1,136 @@
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import { Box, Button, ButtonGroup, Stack, Typography } from "@mui/material";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { monitorValidation } from "../../../Validation/validation";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useNavigate } from "react-router";
|
||||
import { createPageSpeed } from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { ConfigBox } from "../../Monitors/styled";
|
||||
import Radio from "../../../Components/Inputs/Radio";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import Button from "../../../Components/Button";
|
||||
import Checkbox from "../../../Components/Inputs/Checkbox";
|
||||
import { monitorValidation } from "../../../Validation/validation";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { createPageSpeed } from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import "./index.css";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
|
||||
const CreatePageSpeed = () => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const MS_PER_MINUTE = 60000;
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
|
||||
const idMap = {
|
||||
"monitor-url": "url",
|
||||
"monitor-name": "name",
|
||||
"monitor-checks-http": "type",
|
||||
"monitor-checks-ping": "type",
|
||||
"notify-email-default": "notification-email",
|
||||
};
|
||||
|
||||
const [monitor, setMonitor] = useState({
|
||||
url: "",
|
||||
name: "",
|
||||
type: "pagespeed",
|
||||
notifications: [],
|
||||
interval: 3,
|
||||
});
|
||||
const [https, setHttps] = useState(true);
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const handleChange = (event, name) => {
|
||||
const { value, id } = event.target;
|
||||
if (!name) name = idMap[id];
|
||||
|
||||
if (name.includes("notification-")) {
|
||||
name = name.replace("notification-", "");
|
||||
let hasNotif = monitor.notifications.some(
|
||||
(notification) => notification.type === name
|
||||
);
|
||||
setMonitor((prev) => {
|
||||
const notifs = [...prev.notifications];
|
||||
if (hasNotif) {
|
||||
return {
|
||||
...prev,
|
||||
notifications: notifs.filter((notif) => notif.type !== name),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...prev,
|
||||
notifications: [
|
||||
...notifs,
|
||||
name === "email"
|
||||
? { type: name, address: value }
|
||||
: // TODO - phone number
|
||||
{ type: name, phone: value },
|
||||
],
|
||||
};
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setMonitor((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
|
||||
const { error } = monitorValidation.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
if (error) updatedErrors[name] = error.details[0].message;
|
||||
else delete updatedErrors[name];
|
||||
return updatedErrors;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateMonitor = async (event) => {
|
||||
event.preventDefault();
|
||||
//obj to submit
|
||||
let form = {
|
||||
url: `http${https ? "s" : ""}://` + monitor.url,
|
||||
name: monitor.name === "" ? monitor.url : monitor.name,
|
||||
type: monitor.type,
|
||||
interval: monitor.interval * MS_PER_MINUTE,
|
||||
};
|
||||
|
||||
const { error } = monitorValidation.validate(form, {
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
createToast({ body: "Error validation data." });
|
||||
} else {
|
||||
form = {
|
||||
...form,
|
||||
description: form.name,
|
||||
teamId: user.teamId,
|
||||
userId: user._id,
|
||||
notifications: monitor.notifications,
|
||||
};
|
||||
const action = await dispatch(
|
||||
createPageSpeed({ authToken, monitor: form })
|
||||
);
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
createToast({ body: "Monitor created successfully!" });
|
||||
navigate("/pagespeed");
|
||||
} else {
|
||||
createToast({ body: "Failed to create monitor." });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//select values
|
||||
const frequencies = [
|
||||
{ _id: 3, name: "3 minutes" },
|
||||
{ _id: 5, name: "5 minutes" },
|
||||
@@ -31,75 +140,15 @@ const CreatePageSpeed = () => {
|
||||
{ _id: 1440, name: "1 day" },
|
||||
{ _id: 10080, name: "1 week" },
|
||||
];
|
||||
|
||||
const [form, setForm] = useState({
|
||||
name: "",
|
||||
url: "",
|
||||
interval: 3,
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const handleChange = (event, id) => {
|
||||
const { value } = event.target;
|
||||
setForm((prev) => ({ ...prev, [id]: value }));
|
||||
|
||||
const { error } = monitorValidation.validate(
|
||||
{ [id]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const newErrors = { ...prev };
|
||||
if (error) newErrors[id] = error.details[0].message;
|
||||
else delete newErrors[id];
|
||||
return newErrors;
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreate = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
let monitor = {
|
||||
url: "http://" + form.url,
|
||||
name: form.name === "" ? form.url : form.name,
|
||||
};
|
||||
|
||||
const { error } = monitorValidation.validate(form, { abortEarly: false });
|
||||
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
createToast({ body: "Error validating data." });
|
||||
} else {
|
||||
monitor = {
|
||||
...monitor,
|
||||
description: monitor.name,
|
||||
userId: user._id,
|
||||
teamId: user.teamId,
|
||||
interval: form.interval * MS_PER_MINUTE,
|
||||
type: "pagespeed",
|
||||
};
|
||||
try {
|
||||
const action = await dispatch(createPageSpeed({ authToken, monitor }));
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
navigate("/pagespeed");
|
||||
}
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body:
|
||||
error.details && error.details.length > 0
|
||||
? error.details[0].message
|
||||
: "Unknown error.",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack className="create-page-speed" gap={theme.spacing(6)}>
|
||||
<Box
|
||||
className="create-monitor"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Breadcrumbs
|
||||
list={[
|
||||
{ name: "pagespeed", path: "/pagespeed" },
|
||||
@@ -108,18 +157,14 @@ const CreatePageSpeed = () => {
|
||||
/>
|
||||
<Stack
|
||||
component="form"
|
||||
className="create-monitor-form"
|
||||
onSubmit={handleCreateMonitor}
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
onSubmit={handleCreate}
|
||||
gap={theme.spacing(12)}
|
||||
flex={1}
|
||||
mt={theme.spacing(6)}
|
||||
>
|
||||
<Typography
|
||||
component="h1"
|
||||
lineHeight={1}
|
||||
fontSize={21}
|
||||
color={theme.palette.text.primary}
|
||||
>
|
||||
<Typography component="h1">
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
@@ -128,115 +173,170 @@ const CreatePageSpeed = () => {
|
||||
Create your{" "}
|
||||
</Typography>
|
||||
<Typography component="span" fontSize="inherit" fontWeight="inherit">
|
||||
pagespeed{" "}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
monitor
|
||||
Pagespeed monitor
|
||||
</Typography>
|
||||
</Typography>
|
||||
<Stack
|
||||
border={1}
|
||||
borderColor={theme.palette.border.light}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
backgroundColor={theme.palette.background.main}
|
||||
p={theme.spacing(20)}
|
||||
pl={theme.spacing(15)}
|
||||
gap={theme.spacing(20)}
|
||||
sx={{
|
||||
"& h3, & p": {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack direction="row">
|
||||
<Typography component="h3">Monitor display name</Typography>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">General settings</Typography>
|
||||
<Typography component="p">
|
||||
Here you can select the URL of the host, together with the type of
|
||||
monitor.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(15)}>
|
||||
<Field
|
||||
type={"url"}
|
||||
id="monitor-url"
|
||||
label="URL to monitor"
|
||||
https={https}
|
||||
placeholder="google.com"
|
||||
value={monitor.url}
|
||||
onChange={handleChange}
|
||||
error={errors["url"]}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="monitor-name"
|
||||
placeholder="Example monitor"
|
||||
value={form.name}
|
||||
onChange={(event) => handleChange(event, "name")}
|
||||
error={errors.name}
|
||||
label="Display name"
|
||||
isOptional={true}
|
||||
placeholder="Google"
|
||||
value={monitor.name}
|
||||
onChange={handleChange}
|
||||
error={errors["name"]}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Typography component="h3">URL</Typography>
|
||||
<Field
|
||||
type="url"
|
||||
id="monitor-url"
|
||||
placeholder="random.website.com"
|
||||
value={form.url}
|
||||
onChange={(event) => handleChange(event, "url")}
|
||||
error={errors.url}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Typography component="h3">Check frequency</Typography>
|
||||
<Select
|
||||
id="monitor-frequency"
|
||||
items={frequencies}
|
||||
value={form.interval}
|
||||
onChange={(event) => handleChange(event, "interval")}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row">
|
||||
<Typography component="h3">
|
||||
Incidents notifications{" "}
|
||||
<Typography component="span">(coming soon)</Typography>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">Checks to perform</Typography>
|
||||
<Typography component="p">
|
||||
You can always add or remove checks after adding your site.
|
||||
</Typography>
|
||||
<Stack
|
||||
className="section-disabled"
|
||||
backgroundColor={theme.palette.background.fill}
|
||||
>
|
||||
<Typography mb={theme.spacing(4)}>
|
||||
When there is a new incident,
|
||||
</Typography>
|
||||
<Checkbox
|
||||
id="notify-sms"
|
||||
label="Notify via SMS (coming soon)"
|
||||
isChecked={false}
|
||||
isDisabled={true}
|
||||
/>
|
||||
<Checkbox
|
||||
id="notify-email"
|
||||
label="Notify via email (to gorkem.cetin@bluewavelabs.ca)"
|
||||
isChecked={false}
|
||||
/>
|
||||
<Checkbox
|
||||
id="notify-emails"
|
||||
label="Notify via email to following emails"
|
||||
isChecked={false}
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(12)}>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Radio
|
||||
id="monitor-checks-http"
|
||||
title="Website monitoring"
|
||||
desc="Use HTTP(s) to monitor your website or API endpoint."
|
||||
size="small"
|
||||
value="http"
|
||||
checked={monitor.type === "pagespeed"}
|
||||
onChange={(event) => handleChange(event)}
|
||||
/>
|
||||
<ButtonGroup sx={{ ml: "32px" }}>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={https.toString()}
|
||||
onClick={() => setHttps(true)}
|
||||
>
|
||||
HTTPS
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(!https).toString()}
|
||||
onClick={() => setHttps(false)}
|
||||
>
|
||||
HTTP
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
{errors["type"] ? (
|
||||
<Box className="error-container">
|
||||
<Typography
|
||||
component="p"
|
||||
className="input-error"
|
||||
color={theme.palette.error.text}
|
||||
>
|
||||
{errors["type"]}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">Incident notifications</Typography>
|
||||
<Typography component="p">
|
||||
When there is an incident, notify users.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography component="p">When there is a new incident,</Typography>
|
||||
<Checkbox
|
||||
id="notify-sms"
|
||||
label="Notify via SMS (coming soon)"
|
||||
isChecked={false}
|
||||
value=""
|
||||
onChange={() => logger.warn("disabled")}
|
||||
isDisabled={true}
|
||||
/>
|
||||
<Checkbox
|
||||
id="notify-email-default"
|
||||
label={`Notify via email (to ${user.email})`}
|
||||
isChecked={monitor.notifications.some(
|
||||
(notification) => notification.type === "email"
|
||||
)}
|
||||
value={user?.email}
|
||||
onChange={(event) => handleChange(event)}
|
||||
/>
|
||||
<Checkbox
|
||||
id="notify-email"
|
||||
label="Also notify via email to multiple addresses (coming soon)"
|
||||
isChecked={false}
|
||||
value=""
|
||||
onChange={() => logger.warn("disabled")}
|
||||
isDisabled={true}
|
||||
/>
|
||||
{monitor.notifications.some(
|
||||
(notification) => notification.type === "emails"
|
||||
) ? (
|
||||
<Box mx={theme.spacing(16)}>
|
||||
<Field
|
||||
id="notify-emails-list"
|
||||
placeholder="notifications@gmail.com"
|
||||
id="notify-email-list"
|
||||
type="text"
|
||||
placeholder="name@gmail.com"
|
||||
value=""
|
||||
onChange={() => logger.warn("disabled")}
|
||||
error=""
|
||||
/>
|
||||
<Typography mt={theme.spacing(4)}>
|
||||
You can separate multiple emails with a comma
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack direction="row" justifyContent="flex-end" mt="auto">
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">Advanced settings</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(12)}>
|
||||
<Select
|
||||
id="monitor-interval"
|
||||
label="Check frequency"
|
||||
value={monitor.interval || 3}
|
||||
onChange={(event) => handleChange(event, "interval")}
|
||||
items={frequencies}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<Button
|
||||
type="submit"
|
||||
level="primary"
|
||||
label="Create"
|
||||
onClick={handleCreate}
|
||||
sx={{ px: theme.spacing(12), mt: theme.spacing(12) }}
|
||||
/>
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleCreateMonitor}
|
||||
disabled={Object.keys(errors).length !== 0 && true}
|
||||
>
|
||||
Create monitor
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { Box, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { Box, Button, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { PieChart } from "@mui/x-charts/PieChart";
|
||||
import { useDrawingArea } from "@mui/x-charts";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
} from "../../../Utils/timeUtils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { networkService } from "../../../main";
|
||||
import Button from "../../../Components/Button";
|
||||
import SettingsIcon from "../../../assets/icons/settings-bold.svg?react";
|
||||
import LastCheckedIcon from "../../../assets/icons/calendar-check.svg?react";
|
||||
import ClockIcon from "../../../assets/icons/maintenance.svg?react";
|
||||
@@ -43,7 +42,7 @@ const StatBox = ({ icon, title, value }) => {
|
||||
<Box>
|
||||
<Typography
|
||||
variant="h6"
|
||||
color={theme.palette.common.main}
|
||||
color={theme.palette.primary.main}
|
||||
mb={theme.spacing(6)}
|
||||
>
|
||||
{title}
|
||||
@@ -214,7 +213,7 @@ const PageSpeedDetails = () => {
|
||||
monitorId,
|
||||
"desc",
|
||||
50,
|
||||
null,
|
||||
"day",
|
||||
null,
|
||||
null
|
||||
);
|
||||
@@ -398,28 +397,24 @@ const PageSpeedDetails = () => {
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Configure"
|
||||
animate="rotate90"
|
||||
img={
|
||||
<SettingsIcon
|
||||
style={{
|
||||
width: theme.spacing(10),
|
||||
height: theme.spacing(10),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={() => navigate(`/pagespeed/configure/${monitorId}`)}
|
||||
sx={{
|
||||
ml: "auto",
|
||||
alignSelf: "flex-end",
|
||||
backgroundColor: theme.palette.background.fill,
|
||||
px: theme.spacing(6),
|
||||
px: theme.spacing(5),
|
||||
"& svg": {
|
||||
mr: "6px",
|
||||
mr: theme.spacing(3),
|
||||
"& path": {
|
||||
stroke: theme.palette.other.icon,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<SettingsIcon />
|
||||
Configure
|
||||
</Button>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
@@ -592,7 +587,7 @@ const PageSpeedDetails = () => {
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
color: theme.palette.common.main,
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 500,
|
||||
textDecoration: "underline",
|
||||
cursor: "pointer",
|
||||
@@ -627,7 +622,7 @@ const PageSpeedDetails = () => {
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
color: theme.palette.common.main,
|
||||
color: theme.palette.primary.main,
|
||||
fontWeight: 500,
|
||||
textDecoration: "underline",
|
||||
cursor: "pointer",
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { getLastChecked } from "../../Utils/monitorUtils";
|
||||
import { formatDate, formatDurationRounded } from "../../Utils/timeUtils";
|
||||
import PageSpeedIcon from "../../assets/icons/page-speed.svg?react";
|
||||
import { StatusLabel } from "../../Components/Label";
|
||||
import { Box, Grid, Stack, Typography } from "@mui/material";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const Card = ({ data }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Grid
|
||||
item
|
||||
lg={6}
|
||||
flexGrow={1}
|
||||
sx={{
|
||||
"&:hover > .MuiStack-root": {
|
||||
backgroundColor: theme.palette.background.accent,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(6)}
|
||||
p={theme.spacing(8)}
|
||||
onClick={() => navigate(`/pagespeed/${data._id}`)}
|
||||
border={1}
|
||||
borderColor={theme.palette.border.light}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
backgroundColor={theme.palette.background.main}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
"& svg path": { stroke: theme.palette.other.icon, strokeWidth: 0.8 },
|
||||
}}
|
||||
>
|
||||
<PageSpeedIcon
|
||||
style={{ width: theme.spacing(8), height: theme.spacing(8) }}
|
||||
/>
|
||||
<Box flex={1}>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography
|
||||
component="h2"
|
||||
mb={theme.spacing(2)}
|
||||
color={theme.palette.primary.main}
|
||||
>
|
||||
{data.name}
|
||||
</Typography>
|
||||
<StatusLabel
|
||||
status={data.status ? "up" : "cannot resolve"}
|
||||
text={data.status ? "Live (collecting data)" : "Inactive"}
|
||||
/>
|
||||
</Stack>
|
||||
<Typography fontSize={13}>
|
||||
{data.url.replace(/^https?:\/\//, "")}
|
||||
</Typography>
|
||||
<Typography mt={theme.spacing(12)}>
|
||||
<Typography component="span" fontWeight={600}>
|
||||
Last checked:{" "}
|
||||
</Typography>
|
||||
{formatDate(getLastChecked(data.checks, false))}{" "}
|
||||
<Typography component="span" fontStyle="italic">
|
||||
({formatDurationRounded(getLastChecked(data.checks))} ago)
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
Card.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default Card;
|
||||
@@ -1,14 +1,13 @@
|
||||
.page-speed h2.MuiTypography-root {
|
||||
line-height: 1.1;
|
||||
}
|
||||
.page-speed h1.MuiTypography-root,
|
||||
.page-speed:not(:has([class*="fallback__"])) h1.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large-plus);
|
||||
}
|
||||
.page-speed h2.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-large);
|
||||
font-weight: 600;
|
||||
}
|
||||
.page-speed p.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
.page-speed p:has(> span.MuiTypography-root),
|
||||
.page-speed p.MuiTypography-root > span.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
@@ -20,5 +19,4 @@
|
||||
}
|
||||
.page-speed:not(:has([class*="fallback__"])) button {
|
||||
height: 34px;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
@@ -1,141 +1,16 @@
|
||||
import { Box, Grid, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { Box, Button, Grid, Stack } from "@mui/material";
|
||||
import { useEffect } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { formatDate, formatDurationRounded } from "../../Utils/timeUtils";
|
||||
import { StatusLabel } from "../../Components/Label";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { getPageSpeedByTeamId } from "../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
|
||||
import PageSpeedIcon from "../../assets/icons/page-speed.svg?react";
|
||||
import Fallback from "../../Components/Fallback";
|
||||
import "./index.css";
|
||||
import Button from "../../Components/Button";
|
||||
import { useNavigate } from "react-router";
|
||||
import { getLastChecked } from "../../Utils/monitorUtils";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const Card = ({ data }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Grid
|
||||
item
|
||||
lg={6}
|
||||
flexGrow={1}
|
||||
sx={{
|
||||
"&:hover > .MuiStack-root": {
|
||||
backgroundColor: "var(--primary-bg-accent)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(6)}
|
||||
p={theme.spacing(8)}
|
||||
onClick={() => navigate(`/pagespeed/${data._id}`)}
|
||||
border={1}
|
||||
borderColor={theme.palette.border.light}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
backgroundColor={theme.palette.background.main}
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
<PageSpeedIcon
|
||||
style={{ width: theme.spacing(8), height: theme.spacing(8) }}
|
||||
/>
|
||||
<Box flex={1}>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography
|
||||
component="h2"
|
||||
mb={theme.spacing(2)}
|
||||
color={theme.palette.common.main}
|
||||
>
|
||||
{data.name}
|
||||
</Typography>
|
||||
<StatusLabel
|
||||
status={data.status ? "up" : "cannot resolve"}
|
||||
text={data.status ? "Live (collecting data)" : "Inactive"}
|
||||
/>
|
||||
</Stack>
|
||||
<Typography>{data.url.replace(/^https?:\/\//, "")}</Typography>
|
||||
<Typography mt={theme.spacing(12)}>
|
||||
<Typography component="span" fontWeight={600}>
|
||||
Last checked:{" "}
|
||||
</Typography>
|
||||
{formatDate(getLastChecked(data.checks, false))}{" "}
|
||||
<Typography component="span" fontStyle="italic">
|
||||
({formatDurationRounded(getLastChecked(data.checks))} ago)
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
Card.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a skeleton layout.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(2)}>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
mb={theme.spacing(12)}
|
||||
>
|
||||
<Box width="80%">
|
||||
<Skeleton variant="rounded" width="25%" height={24} />
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="50%"
|
||||
height={19.5}
|
||||
sx={{ mt: theme.spacing(2) }}
|
||||
/>
|
||||
</Box>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="20%"
|
||||
height={34}
|
||||
sx={{ alignSelf: "flex-end" }}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row" flexWrap="wrap" gap={theme.spacing(12)}>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={120}
|
||||
sx={{ flex: "35%" }}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={120}
|
||||
sx={{ flex: "35%" }}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={120}
|
||||
sx={{ flex: "35%" }}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={120}
|
||||
sx={{ flex: "35%" }}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
import Breadcrumbs from "../../Components/Breadcrumbs";
|
||||
import Greeting from "../../Utils/greeting";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
import Card from "./card";
|
||||
|
||||
const PageSpeed = ({ isAdmin }) => {
|
||||
const theme = useTheme();
|
||||
@@ -143,7 +18,7 @@ const PageSpeed = ({ isAdmin }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const { monitors, isLoading } = useSelector(
|
||||
const { monitorsSummary, isLoading } = useSelector(
|
||||
(state) => state.pageSpeedMonitors
|
||||
);
|
||||
useEffect(() => {
|
||||
@@ -152,12 +27,11 @@ const PageSpeed = ({ isAdmin }) => {
|
||||
|
||||
// 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
|
||||
let isActuallyLoading = isLoading && monitors.length === 0;
|
||||
let isActuallyLoading = isLoading && monitorsSummary?.monitors?.length === 0;
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="page-speed"
|
||||
pt={theme.spacing(20)}
|
||||
sx={{
|
||||
':has(> [class*="fallback__"])': {
|
||||
position: "relative",
|
||||
@@ -172,38 +46,38 @@ const PageSpeed = ({ isAdmin }) => {
|
||||
>
|
||||
{isActuallyLoading ? (
|
||||
<SkeletonLayout />
|
||||
) : monitors?.length !== 0 ? (
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
) : monitorsSummary?.monitors?.length !== 0 ? (
|
||||
<Box
|
||||
sx={{
|
||||
"& h1, & span.MuiTypography-root, & p": {
|
||||
"& p": {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
mb={theme.spacing(12)}
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h1">All page speed monitors</Typography>
|
||||
<Typography mt={theme.spacing(2)}>
|
||||
Click on one of the monitors to get more site speed information.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
level="primary"
|
||||
label="Create new"
|
||||
onClick={() => navigate("/pagespeed/create")}
|
||||
/>
|
||||
</Stack>
|
||||
<Box mb={theme.spacing(12)}>
|
||||
<Breadcrumbs list={[{ name: `pagespeed`, path: "/pagespeed" }]} />
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
mt={theme.spacing(5)}
|
||||
>
|
||||
<Greeting type="pagespeed" />
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => navigate("/pagespeed/create")}
|
||||
>
|
||||
Create new
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Grid container spacing={theme.spacing(12)}>
|
||||
{monitors?.map((monitor) => (
|
||||
<Card data={monitor} key={`monitor-${monitor._id}`} />
|
||||
{monitorsSummary?.monitors?.map((monitor) => (
|
||||
<Card data={monitor} key={monitor._id} />
|
||||
))}
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Box>
|
||||
) : (
|
||||
<Fallback
|
||||
title="pagespeed monitor"
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Box, Skeleton, Stack } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
/**
|
||||
* Renders a skeleton layout.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SkeletonLayout = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(2)}>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
mb={theme.spacing(12)}
|
||||
>
|
||||
<Box width="80%">
|
||||
<Skeleton variant="rounded" width="25%" height={24} />
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="50%"
|
||||
height={19.5}
|
||||
sx={{ mt: theme.spacing(2) }}
|
||||
/>
|
||||
</Box>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="20%"
|
||||
height={34}
|
||||
sx={{ alignSelf: "flex-end" }}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction="row" flexWrap="wrap" gap={theme.spacing(12)}>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={120}
|
||||
sx={{ flex: "35%" }}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={120}
|
||||
sx={{ flex: "35%" }}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={120}
|
||||
sx={{ flex: "35%" }}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
width="100%"
|
||||
height={120}
|
||||
sx={{ flex: "35%" }}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -1,14 +1,40 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, styled, Typography } from "@mui/material";
|
||||
import Button from "../../Components/Button";
|
||||
import { Box, Button, Stack, styled, Typography } from "@mui/material";
|
||||
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 = () => {
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { createToast } from "../../Utils/toastUtils";
|
||||
import { deleteMonitorChecksByTeamId } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import PropTypes from "prop-types";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
const Settings = ({ isAdmin }) => {
|
||||
const theme = useTheme();
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
const { isLoading } = useSelector((state) => state.uptimeMonitors);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// TODO Handle saving
|
||||
|
||||
const handleClearStats = async () => {
|
||||
try {
|
||||
const action = await dispatch(
|
||||
deleteMonitorChecksByTeamId({ teamId: user.teamId, authToken })
|
||||
);
|
||||
|
||||
if (deleteMonitorChecksByTeamId.fulfilled.match(action)) {
|
||||
createToast({ body: "Stats cleared successfully" });
|
||||
} else {
|
||||
createToast({ body: "Failed to clear stats" });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
createToast({ body: "Failed to clear stats" });
|
||||
}
|
||||
};
|
||||
|
||||
const ConfigBox = styled("div")({
|
||||
display: "flex",
|
||||
@@ -79,35 +105,41 @@ const Settings = () => {
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">History and monitoring</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Define here for how long you want to keep the data. You can also
|
||||
remove all past data.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
type="text"
|
||||
id="history-monitoring"
|
||||
label="The days you want to keep monitoring history."
|
||||
isOptional={true}
|
||||
optionalLabel="0 for infinite"
|
||||
placeholder="90"
|
||||
value=""
|
||||
onChange={() => logger.warn("Disabled")}
|
||||
/>
|
||||
{isAdmin && (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography>Clear all stats. This is irreversible.</Typography>
|
||||
<Button
|
||||
level="error"
|
||||
label="Clear all stats"
|
||||
sx={{ mt: theme.spacing(4) }}
|
||||
/>
|
||||
<Typography component="h1">History and monitoring</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
Define here for how long you want to keep the data. You can also
|
||||
remove all past data.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
type="text"
|
||||
id="history-monitoring"
|
||||
label="The days you want to keep monitoring history."
|
||||
isOptional={true}
|
||||
optionalLabel="0 for infinite"
|
||||
placeholder="90"
|
||||
value=""
|
||||
onChange={() => logger.warn("Disabled")}
|
||||
/>
|
||||
<Box>
|
||||
<Typography>Clear all stats. This is irreversible.</Typography>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="error"
|
||||
loading={isLoading}
|
||||
onClick={handleClearStats}
|
||||
sx={{ mt: theme.spacing(4) }}
|
||||
>
|
||||
Clear all stats
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
)}
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">About</Typography>
|
||||
@@ -127,15 +159,21 @@ const Settings = () => {
|
||||
</Box>
|
||||
</ConfigBox>
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<Button
|
||||
level="primary"
|
||||
label="Save"
|
||||
<LoadingButton
|
||||
loading={false}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
|
||||
/>
|
||||
>
|
||||
Save
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Settings.propTypes = {
|
||||
isAdmin: PropTypes.bool,
|
||||
};
|
||||
export default Settings;
|
||||
|
||||
@@ -58,9 +58,28 @@ class NetworkService {
|
||||
});
|
||||
}
|
||||
|
||||
async getMonitorsAndSummaryByTeamId(authToken, teamId, types) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (types) {
|
||||
types.forEach((type) => {
|
||||
params.append("type", type);
|
||||
});
|
||||
}
|
||||
return this.axiosInstance.get(
|
||||
`/monitors/team/summary/${teamId}?${params.toString()}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Get all uptime monitors for a user
|
||||
* Get all uptime monitors for a Team
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
@@ -80,7 +99,9 @@ class NetworkService {
|
||||
types,
|
||||
status,
|
||||
sortOrder,
|
||||
normalize
|
||||
normalize,
|
||||
page,
|
||||
rowsPerPage
|
||||
) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
@@ -93,6 +114,8 @@ class NetworkService {
|
||||
if (status) params.append("status", status);
|
||||
if (sortOrder) params.append("sortOrder", sortOrder);
|
||||
if (normalize) params.append("normalize", normalize);
|
||||
if (page) params.append("page", page);
|
||||
if (rowsPerPage) params.append("rowsPerPage", rowsPerPage);
|
||||
|
||||
return this.axiosInstance.get(
|
||||
`/monitors/team/${teamId}?${params.toString()}`,
|
||||
@@ -116,7 +139,7 @@ class NetworkService {
|
||||
* @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 {number} [numToDisplay] - The number of checks to display.
|
||||
* @param {boolean} [normalize] - Whether to normalize the retrieved statistics.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios GET request.
|
||||
*/
|
||||
@@ -146,6 +169,31 @@ class NetworkService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Gets aggregate stats by monitor ID
|
||||
* ************************************
|
||||
*
|
||||
* @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 getAggregateStatsById(authToken, monitorId, dateRange) {
|
||||
const params = new URLSearchParams();
|
||||
if (dateRange) params.append("dateRange", dateRange);
|
||||
|
||||
return this.axiosInstance.get(
|
||||
`/monitors/aggregate/${monitorId}?${params.toString()}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Updates a single monitor
|
||||
@@ -184,6 +232,25 @@ class NetworkService {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ************************************
|
||||
* Deletes all checks for all monitor by teamID
|
||||
* ************************************
|
||||
*
|
||||
* @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 deleteChecksByTeamId(authToken, teamId) {
|
||||
return this.axiosInstance.delete(`/checks/team/${teamId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* ************************************
|
||||
* Pauses a single monitor by its ID
|
||||
|
||||
@@ -4,16 +4,16 @@ const text = {
|
||||
primary: "#fafafa",
|
||||
secondary: "#e6e6e6",
|
||||
tertiary: "#a1a1aa",
|
||||
accent: "#e6e6e6",
|
||||
accent: "#8e8e8f",
|
||||
disabled: "rgba(172, 172, 172, 0.3)",
|
||||
};
|
||||
const background = {
|
||||
main: "#151518",
|
||||
alt: "#09090b",
|
||||
fill: "#2e2e2e",
|
||||
fill: "#2D2D33",
|
||||
accent: "#18181a",
|
||||
};
|
||||
const border = { light: "#27272a", dark: "#2c2c2c" };
|
||||
const border = { light: "#27272a", dark: "#36363e" };
|
||||
|
||||
const fontFamilyDefault =
|
||||
'"Inter","system-ui", "Avenir", "Helvetica", "Arial", sans-serif';
|
||||
@@ -24,13 +24,14 @@ const darkTheme = createTheme({
|
||||
typography: { fontFamily: fontFamilyDefault, fontSize: 13 },
|
||||
palette: {
|
||||
mode: "dark",
|
||||
common: { main: "#1570ef" },
|
||||
primary: { main: "#1570ef" },
|
||||
secondary: { main: "#2D2D33" },
|
||||
text: text,
|
||||
background: background,
|
||||
border: border,
|
||||
info: {
|
||||
text: text.primary,
|
||||
main: text.primary,
|
||||
main: text.secondary,
|
||||
bg: background.main,
|
||||
light: background.main,
|
||||
border: border.light,
|
||||
@@ -38,22 +39,29 @@ const darkTheme = createTheme({
|
||||
success: {
|
||||
text: "#079455",
|
||||
main: "#45bb7a",
|
||||
light: "#93d5aa",
|
||||
bg: "#27272a",
|
||||
light: "#1c4428",
|
||||
bg: "#12261e",
|
||||
},
|
||||
error: {
|
||||
text: "#f04438",
|
||||
main: "#d32f2f",
|
||||
light: "#f04438",
|
||||
bg: "#27272a",
|
||||
light: "#542426",
|
||||
bg: "#301a1f",
|
||||
dark: "#932020",
|
||||
border: "#f04438",
|
||||
},
|
||||
warning: {
|
||||
text: "#DC6803",
|
||||
main: "#e88c30",
|
||||
light: "#fffcf5",
|
||||
bg: "#ffecbc",
|
||||
border: "#fec84b",
|
||||
text: "#e88c30",
|
||||
main: "#FF9F00",
|
||||
light: "#272115",
|
||||
bg: "#624711",
|
||||
border: "#e88c30",
|
||||
},
|
||||
percentage: {
|
||||
uptimePoor: "#d32f2f",
|
||||
uptimeFair: "#e88c30",
|
||||
uptimeGood: "#ffd600",
|
||||
uptimeExcellent: "#079455",
|
||||
},
|
||||
unresolved: { main: "#4e5ba6", light: "#e2eaf7", bg: "#f2f4f7" },
|
||||
divider: border.light,
|
||||
@@ -61,30 +69,63 @@ const darkTheme = createTheme({
|
||||
icon: "#e6e6e6",
|
||||
line: "#27272a",
|
||||
fill: "#18181a",
|
||||
grid: "#454546",
|
||||
},
|
||||
// TO BE REMOVED //
|
||||
primary: {
|
||||
main: "#1570ef",
|
||||
},
|
||||
secondary: {
|
||||
main: "#e6e6e6",
|
||||
},
|
||||
tertiary: {
|
||||
main: "#e6e6e6",
|
||||
},
|
||||
// ----------------- //
|
||||
},
|
||||
spacing: 2,
|
||||
components: {
|
||||
MuiButtonBase: {
|
||||
MuiButton: {
|
||||
defaultProps: {
|
||||
disableRipple: true,
|
||||
},
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
variants: [
|
||||
{
|
||||
props: (props) => props.variant === "group",
|
||||
style: {
|
||||
color: theme.palette.secondary.contrastText,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.light,
|
||||
},
|
||||
},
|
||||
{
|
||||
props: (props) =>
|
||||
props.variant === "group" && props.filled === "true",
|
||||
style: {
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
},
|
||||
},
|
||||
{
|
||||
props: (props) =>
|
||||
props.variant === "contained" && props.color === "secondary",
|
||||
style: {
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.dark,
|
||||
},
|
||||
},
|
||||
],
|
||||
fontWeight: 400,
|
||||
borderRadius: 4,
|
||||
boxShadow: "none",
|
||||
textTransform: "none",
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"&:hover": {
|
||||
boxShadow: "none",
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiIconButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
padding: 4,
|
||||
transition: "none",
|
||||
"&:hover": {
|
||||
backgroundColor: background.fill,
|
||||
},
|
||||
@@ -102,6 +143,7 @@ const darkTheme = createTheme({
|
||||
borderRadius: 4,
|
||||
boxShadow: shadow,
|
||||
backgroundColor: background.main,
|
||||
backgroundImage: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -112,6 +154,13 @@ const darkTheme = createTheme({
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiListItemButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
transition: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiMenuItem: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
@@ -171,6 +220,13 @@ const darkTheme = createTheme({
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiSkeleton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: "#151518",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 2,
|
||||
|
||||
@@ -22,16 +22,17 @@ const shadow =
|
||||
const lightTheme = createTheme({
|
||||
typography: { fontFamily: fontFamilyDefault, fontSize: 13 },
|
||||
palette: {
|
||||
common: { main: "#1570ef" },
|
||||
primary: { main: "#1570EF" },
|
||||
secondary: { main: "#F4F4F4", dark: "#e3e3e3", contrastText: "#475467" },
|
||||
text: text,
|
||||
background: background,
|
||||
border: border,
|
||||
info: {
|
||||
text: "#475467",
|
||||
main: "#475467",
|
||||
bg: "#ffffff",
|
||||
light: "#ffffff",
|
||||
border: "#D0D5DD",
|
||||
text: text.primary,
|
||||
main: text.tertiary,
|
||||
bg: background.main,
|
||||
light: background.main,
|
||||
border: border.dark,
|
||||
},
|
||||
success: {
|
||||
text: "#079455",
|
||||
@@ -53,36 +54,75 @@ const lightTheme = createTheme({
|
||||
bg: "#ffecbc",
|
||||
border: "#fec84b",
|
||||
},
|
||||
percentage: {
|
||||
uptimePoor: "#d32f2f",
|
||||
uptimeFair: "#ec8013",
|
||||
uptimeGood: "#ffb800",
|
||||
uptimeExcellent: "#079455",
|
||||
},
|
||||
unresolved: { main: "#4e5ba6", light: "#e2eaf7", bg: "#f2f4f7" },
|
||||
divider: border.light,
|
||||
other: {
|
||||
icon: "#667085",
|
||||
line: "#d6d9dd",
|
||||
fill: "#e3e3e3",
|
||||
grid: "#a2a3a3",
|
||||
},
|
||||
// TO BE REMOVED //
|
||||
primary: {
|
||||
main: "#1570EF",
|
||||
},
|
||||
secondary: {
|
||||
main: "#475467",
|
||||
},
|
||||
tertiary: {
|
||||
main: "#475467",
|
||||
},
|
||||
// ----------------- //
|
||||
},
|
||||
spacing: 2,
|
||||
components: {
|
||||
MuiButtonBase: {
|
||||
MuiButton: {
|
||||
defaultProps: {
|
||||
disableRipple: true,
|
||||
},
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
variants: [
|
||||
{
|
||||
props: (props) => props.variant === "group",
|
||||
style: {
|
||||
color: theme.palette.secondary.contrastText,
|
||||
backgroundColor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.light,
|
||||
},
|
||||
},
|
||||
{
|
||||
props: (props) =>
|
||||
props.variant === "group" && props.filled === "true",
|
||||
style: {
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
},
|
||||
},
|
||||
{
|
||||
props: (props) =>
|
||||
props.variant === "contained" && props.color === "secondary",
|
||||
style: {
|
||||
border: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.palette.border.light,
|
||||
},
|
||||
},
|
||||
],
|
||||
fontWeight: 400,
|
||||
borderRadius: 4,
|
||||
boxShadow: "none",
|
||||
textTransform: "none",
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"&:hover": {
|
||||
boxShadow: "none",
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
MuiIconButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
padding: 4,
|
||||
transition: "none",
|
||||
"&:hover": {
|
||||
backgroundColor: background.fill,
|
||||
},
|
||||
@@ -109,6 +149,13 @@ const lightTheme = createTheme({
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiListItemButton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
transition: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiMenuItem: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
@@ -169,6 +216,13 @@ const lightTheme = createTheme({
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiSkeleton: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: "#f2f4f7",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 2,
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useEffect } from "react";
|
||||
import { setGreeting } from "../Features/UI/uiSlice";
|
||||
|
||||
const early = [
|
||||
{
|
||||
prepend: "Rise and shine",
|
||||
append: "If you’re up this early, you might as well be a legend!",
|
||||
emoji: "☕",
|
||||
},
|
||||
{
|
||||
prepend: "Good morning",
|
||||
append: "The world’s still asleep, but you’re already awesome!",
|
||||
emoji: "🦉",
|
||||
},
|
||||
{
|
||||
prepend: "Good morning",
|
||||
append: "Are you a wizard? Only magical people are up at this hour!",
|
||||
emoji: "🌄",
|
||||
},
|
||||
{
|
||||
prepend: "Up before the roosters",
|
||||
append: "Ready to tackle the day before it even starts?",
|
||||
emoji: "🐓",
|
||||
},
|
||||
{
|
||||
prepend: "Early bird special",
|
||||
append: "Let’s get things done while everyone else is snoozing!",
|
||||
emoji: "🌟",
|
||||
},
|
||||
];
|
||||
|
||||
const morning = [
|
||||
{
|
||||
prepend: "Good morning",
|
||||
append: "Is it coffee o’clock yet, or should we start with high fives?",
|
||||
emoji: "☕",
|
||||
},
|
||||
{
|
||||
prepend: "Morning",
|
||||
append: "The sun is up, and so are you—time to be amazing!",
|
||||
emoji: "🌞",
|
||||
},
|
||||
{
|
||||
prepend: "Good morning",
|
||||
append: "Time to make today the best thing since sliced bread!",
|
||||
emoji: "🥐",
|
||||
},
|
||||
{
|
||||
prepend: "Morning",
|
||||
append: "Let’s kick off the day with more energy than a double espresso!",
|
||||
emoji: "🚀",
|
||||
},
|
||||
{
|
||||
prepend: "Rise and shine",
|
||||
append: "You’re about to make today so great, even Monday will be jealous!",
|
||||
emoji: "🌟",
|
||||
},
|
||||
];
|
||||
|
||||
const afternoon = [
|
||||
{
|
||||
prepend: "Good afternoon",
|
||||
append: "How about a break to celebrate how awesome you’re doing?",
|
||||
emoji: "🥪",
|
||||
},
|
||||
{
|
||||
prepend: "Afternoon",
|
||||
append: "If you’re still going strong, you’re officially a rockstar!",
|
||||
emoji: "🌞",
|
||||
},
|
||||
{
|
||||
prepend: "Hey there",
|
||||
append: "The afternoon is your playground—let’s make it epic!",
|
||||
emoji: "🍕",
|
||||
},
|
||||
{
|
||||
prepend: "Good afternoon",
|
||||
append: "Time to crush the rest of the day like a pro!",
|
||||
emoji: "🏆",
|
||||
},
|
||||
{
|
||||
prepend: "Afternoon",
|
||||
append: "Time to turn those afternoon slumps into afternoon triumphs!",
|
||||
emoji: "🎉",
|
||||
},
|
||||
];
|
||||
|
||||
const evening = [
|
||||
{
|
||||
prepend: "Good evening",
|
||||
append: "Time to wind down and think about how you crushed today!",
|
||||
emoji: "🌇",
|
||||
},
|
||||
{
|
||||
prepend: "Evening",
|
||||
append: "You’ve earned a break—let’s make the most of these evening vibes!",
|
||||
emoji: "🍹",
|
||||
},
|
||||
{
|
||||
prepend: "Hey there",
|
||||
append: "Time to relax and bask in the glow of your day’s awesomeness!",
|
||||
emoji: "🌙",
|
||||
},
|
||||
{
|
||||
prepend: "Good evening",
|
||||
append: "Ready to trade productivity for chill mode?",
|
||||
emoji: "🛋️ ",
|
||||
},
|
||||
{
|
||||
prepend: "Evening",
|
||||
append: "Let’s call it a day and toast to your success!",
|
||||
emoji: "🕶️",
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Greeting component that displays a personalized greeting message
|
||||
* based on the time of day and the user's first name.
|
||||
*
|
||||
* @component
|
||||
* @example
|
||||
* return <Greeting type={"pagespeed"} />;
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {string} props.type - The type of monitor to be displayed in the message
|
||||
* @returns {JSX.Element} The rendered Greeting component
|
||||
*/
|
||||
|
||||
const Greeting = ({ type = "" }) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const { firstName } = useSelector((state) => state.auth.user);
|
||||
const index = useSelector((state) => state.ui.greeting.index);
|
||||
const lastUpdate = useSelector((state) => state.ui.greeting.lastUpdate);
|
||||
|
||||
const now = new Date();
|
||||
const hour = now.getHours();
|
||||
|
||||
useEffect(() => {
|
||||
const hourDiff = lastUpdate ? hour - lastUpdate : null;
|
||||
|
||||
if (!lastUpdate || hourDiff >= 1) {
|
||||
let random = Math.floor(Math.random() * 5);
|
||||
dispatch(setGreeting({ index: random, lastUpdate: hour }));
|
||||
}
|
||||
}, [dispatch, hour]);
|
||||
|
||||
let greetingArray =
|
||||
hour < 6 ? early : hour < 12 ? morning : hour < 18 ? afternoon : evening;
|
||||
const { prepend, append, emoji } = greetingArray[index];
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography
|
||||
component="h1"
|
||||
lineHeight={1}
|
||||
fontWeight={500}
|
||||
color={theme.palette.text.primary}
|
||||
mb={theme.spacing(1)}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
{prepend},{" "}
|
||||
</Typography>
|
||||
<Typography component="span" fontSize="inherit" fontWeight="inherit">
|
||||
{firstName} {emoji}
|
||||
</Typography>
|
||||
</Typography>
|
||||
<Typography
|
||||
lineHeight={1}
|
||||
fontSize={14}
|
||||
fontWeight={400}
|
||||
color={theme.palette.text.accent}
|
||||
>
|
||||
{append} — Here’s an overview of your {type} monitors.
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Greeting.propTypes = {
|
||||
type: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Greeting;
|
||||
@@ -43,6 +43,23 @@ export const formatDurationRounded = (ms) => {
|
||||
return time;
|
||||
};
|
||||
|
||||
export const formatDurationSplit = (ms) => {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
return days > 0
|
||||
? { time: days, format: days === 1 ? "day" : "days" }
|
||||
: hours > 0
|
||||
? { time: hours, format: hours === 1 ? "hour" : "hours" }
|
||||
: minutes > 0
|
||||
? { time: minutes, format: minutes === 1 ? "minute" : "minutes" }
|
||||
: seconds > 0
|
||||
? { time: seconds, format: seconds === 1 ? "second" : "seconds" }
|
||||
: { time: 0, format: "seconds" };
|
||||
};
|
||||
|
||||
export const formatDate = (date, customOptions) => {
|
||||
const options = {
|
||||
year: "numeric",
|
||||
@@ -51,9 +68,11 @@ export const formatDate = (date, customOptions) => {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: true,
|
||||
...customOptions
|
||||
...customOptions,
|
||||
};
|
||||
|
||||
// Return the date using the specified options
|
||||
return date.toLocaleString("en-US", options);
|
||||
return date
|
||||
.toLocaleString("en-US", options)
|
||||
.replace(/\b(AM|PM)\b/g, (match) => match.toLowerCase());
|
||||
};
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<svg width="256" height="170" viewBox="0 0 256 170" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_24_2)">
|
||||
<path d="M248.196 0.390244H7.80489C3.7099 0.390244 0.390259 3.70989 0.390259 7.80487V161.561C0.390259 165.656 3.7099 168.975 7.80489 168.975H248.196C252.291 168.975 255.61 165.656 255.61 161.561V7.80487C255.61 3.70989 252.291 0.390244 248.196 0.390244Z" fill="#151518"/>
|
||||
<path d="M248.196 0.390244H7.80489C3.7099 0.390244 0.390259 3.70989 0.390259 7.80487V161.561C0.390259 165.656 3.7099 168.975 7.80489 168.975H248.196C252.291 168.975 255.61 165.656 255.61 161.561V7.80487C255.61 3.70989 252.291 0.390244 248.196 0.390244Z" stroke="#27272A" stroke-width="0.780488"/>
|
||||
<path d="M234.976 18H21.122C19.3977 18 18 19.3977 18 21.122V69.5122C18 71.2364 19.3977 72.6341 21.122 72.6341H234.976C236.7 72.6341 238.098 71.2364 238.098 69.5122V21.122C238.098 19.3977 236.7 18 234.976 18Z" fill="#27272A" fill-opacity="0.8"/>
|
||||
<path d="M99.9025 78.8293H21.0732C19.3489 78.8293 17.9512 80.227 17.9512 81.9512V85.0732C17.9512 86.7974 19.3489 88.1952 21.0732 88.1952H99.9025C101.627 88.1952 103.024 86.7974 103.024 85.0732V81.9512C103.024 80.227 101.627 78.8293 99.9025 78.8293Z" fill="#27272A" fill-opacity="0.6"/>
|
||||
<path d="M234.927 93.6584H21.0732C19.3489 93.6584 17.9512 95.0561 17.9512 96.7803V99.9023C17.9512 101.627 19.3489 103.024 21.0732 103.024H234.927C236.651 103.024 238.049 101.627 238.049 99.9023V96.7803C238.049 95.0561 236.651 93.6584 234.927 93.6584Z" fill="#27272A" fill-opacity="0.3"/>
|
||||
<path d="M234.927 108.488H21.0732C19.3489 108.488 17.9512 109.886 17.9512 111.61V114.732C17.9512 116.456 19.3489 117.854 21.0732 117.854H234.927C236.651 117.854 238.049 116.456 238.049 114.732V111.61C238.049 109.886 236.651 108.488 234.927 108.488Z" fill="#27272A" fill-opacity="0.3"/>
|
||||
<g filter="url(#filter0_d_24_2)">
|
||||
<mask id="mask0_24_2" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="174" y="129" width="65" height="21">
|
||||
<path d="M234.927 129.561H177.171C175.447 129.561 174.049 130.959 174.049 132.683V146.732C174.049 148.456 175.447 149.854 177.171 149.854H234.927C236.651 149.854 238.049 148.456 238.049 146.732V132.683C238.049 130.959 236.651 129.561 234.927 129.561Z" fill="white"/>
|
||||
<path d="M177.171 130.061H234.927C236.375 130.061 237.549 131.235 237.549 132.683V146.732C237.549 148.18 236.375 149.354 234.927 149.354H177.171C175.723 149.354 174.549 148.18 174.549 146.732V132.683C174.549 131.235 175.723 130.061 177.171 130.061Z" stroke="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_24_2)">
|
||||
<path d="M234.927 129.561H177.171C175.447 129.561 174.049 130.959 174.049 132.683V146.732C174.049 148.456 175.447 149.854 177.171 149.854H234.927C236.651 149.854 238.049 148.456 238.049 146.732V132.683C238.049 130.959 236.651 129.561 234.927 129.561Z" fill="#27272A" fill-opacity="0.2"/>
|
||||
<path d="M177.171 130.061H234.927C236.375 130.061 237.549 131.235 237.549 132.683V146.732C237.549 148.18 236.375 149.354 234.927 149.354H177.171C175.723 149.354 174.549 148.18 174.549 146.732V132.683C174.549 131.235 175.723 130.061 177.171 130.061Z" stroke="#27272A" stroke-opacity="0.8"/>
|
||||
</g>
|
||||
<path d="M234.927 129.951H177.171C175.662 129.951 174.439 131.174 174.439 132.683V146.731C174.439 148.24 175.662 149.463 177.171 149.463H234.927C236.435 149.463 237.658 148.24 237.658 146.731V132.683C237.658 131.174 236.435 129.951 234.927 129.951Z" stroke="#27272A" stroke-opacity="0.8" stroke-width="0.780488"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_24_2" x="172.049" y="128.561" width="68.0002" height="24.2929" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_24_2"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_24_2" result="shape"/>
|
||||
</filter>
|
||||
<clipPath id="clip0_24_2">
|
||||
<rect width="256" height="170" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.2 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="M20 20V13M12 20V10M4 20L4 16M13.4067 5.0275L18.5751 6.96567M10.7988 5.40092L5.20023 9.59983M21.0607 6.43934C21.6464 7.02513 21.6464 7.97487 21.0607 8.56066C20.4749 9.14645 19.5251 9.14645 18.9393 8.56066C18.3536 7.97487 18.3536 7.02513 18.9393 6.43934C19.5251 5.85355 20.4749 5.85355 21.0607 6.43934ZM5.06066 9.43934C5.64645 10.0251 5.64645 10.9749 5.06066 11.5607C4.47487 12.1464 3.52513 12.1464 2.93934 11.5607C2.35355 10.9749 2.35355 10.0251 2.93934 9.43934C3.52513 8.85355 4.47487 8.85355 5.06066 9.43934ZM13.0607 3.43934C13.6464 4.02513 13.6464 4.97487 13.0607 5.56066C12.4749 6.14645 11.5251 6.14645 10.9393 5.56066C10.3536 4.97487 10.3536 4.02513 10.9393 3.43934C11.5251 2.85355 12.4749 2.85355 13.0607 3.43934Z" stroke="black" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 915 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="M21 10H3M21 12.5V8.8C21 7.11984 21 6.27976 20.673 5.63803C20.3854 5.07354 19.9265 4.6146 19.362 4.32698C18.7202 4 17.8802 4 16.2 4H7.8C6.11984 4 5.27976 4 4.63803 4.32698C4.07354 4.6146 3.6146 5.07354 3.32698 5.63803C3 6.27976 3 7.11984 3 8.8V17.2C3 18.8802 3 19.7202 3.32698 20.362C3.6146 20.9265 4.07354 21.3854 4.63803 21.673C5.27976 22 6.11984 22 7.8 22H12M16 2V6M8 2V6M14.5 19L16.5 21L21 16.5" stroke="black" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 594 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="M3.07598 7.48282L7.36402 10.5457C7.58715 10.705 7.69872 10.7847 7.81548 10.8031C7.91821 10.8192 8.02343 10.8029 8.11648 10.7565C8.22223 10.7037 8.30449 10.594 8.46901 10.3747L9.37511 9.16652C9.42164 9.10448 9.4449 9.07347 9.47224 9.04671C9.49652 9.02295 9.52315 9.00173 9.55173 8.98338C9.58392 8.9627 9.61935 8.94696 9.6902 8.91546L13.5588 7.19609C13.7192 7.12482 13.7993 7.08918 13.8598 7.03352C13.9133 6.9843 13.9554 6.924 13.9832 6.85684C14.0146 6.78091 14.0204 6.69336 14.0321 6.51826L14.3154 2.2694M13.5 13.5L16.116 14.6211C16.4195 14.7512 16.5713 14.8163 16.6517 14.9243C16.7222 15.0191 16.7569 15.1358 16.7496 15.2537C16.7413 15.3881 16.6497 15.5255 16.4665 15.8002L15.2375 17.6438C15.1507 17.774 15.1072 17.8391 15.0499 17.8863C14.9991 17.928 14.9406 17.9593 14.8777 17.9784C14.8067 18 14.7284 18 14.5719 18H12.5766C12.3693 18 12.2656 18 12.1774 17.9653C12.0995 17.9347 12.0305 17.885 11.9768 17.8208C11.916 17.7481 11.8832 17.6497 11.8177 17.453L11.1048 15.3144C11.0661 15.1983 11.0468 15.1403 11.0417 15.0814C11.0372 15.0291 11.0409 14.9764 11.0528 14.9253C11.0662 14.8677 11.0935 14.813 11.1482 14.7036L11.6897 13.6206C11.7997 13.4005 11.8547 13.2905 11.9395 13.2222C12.0141 13.162 12.1046 13.1246 12.1999 13.1143C12.3081 13.1027 12.4248 13.1416 12.6582 13.2194L13.5 13.5ZM22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z" stroke="black" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 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="M22 7L14.1314 14.8686C13.7354 15.2646 13.5373 15.4627 13.309 15.5368C13.1082 15.6021 12.8918 15.6021 12.691 15.5368C12.4627 15.4627 12.2646 15.2646 11.8686 14.8686L9.13137 12.1314C8.73535 11.7354 8.53735 11.5373 8.30902 11.4632C8.10817 11.3979 7.89183 11.3979 7.69098 11.4632C7.46265 11.5373 7.26465 11.7354 6.86863 12.1314L2 17M22 7H15M22 7V14" stroke="black" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 541 B |
@@ -1,3 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 17L17 7M17 7H7M17 7V17" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7 17L17 7M17 7H7M17 7V17" stroke="#667085" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 224 B After Width: | Height: | Size: 224 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 7H4.6C4.03995 7 3.75992 7 3.54601 7.10899C3.35785 7.20487 3.20487 7.35785 3.10899 7.54601C3 7.75992 3 8.03995 3 8.6V19.4C3 19.9601 3 20.2401 3.10899 20.454C3.20487 20.6422 3.35785 20.7951 3.54601 20.891C3.75992 21 4.03995 21 4.6 21H9M9 21H15M9 21L9 4.6C9 4.03995 9 3.75992 9.10899 3.54601C9.20487 3.35785 9.35785 3.20487 9.54601 3.10899C9.75992 3 10.0399 3 10.6 3L13.4 3C13.9601 3 14.2401 3 14.454 3.10899C14.6422 3.20487 14.7951 3.35785 14.891 3.54601C15 3.75992 15 4.03995 15 4.6V21M15 11H19.4C19.9601 11 20.2401 11 20.454 11.109C20.6422 11.2049 20.7951 11.3578 20.891 11.546C21 11.7599 21 12.0399 21 12.6V19.4C21 19.9601 21 20.2401 20.891 20.454C20.7951 20.6422 20.6422 20.7951 20.454 20.891C20.2401 21 19.9601 21 19.4 21H15" stroke="black" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 927 B |
@@ -4,6 +4,10 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-weight: 400;
|
||||
@@ -86,10 +90,6 @@ body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body .MuiSkeleton-root {
|
||||
background-color: #f2f4f7;
|
||||
}
|
||||
|
||||
@keyframes ripple {
|
||||
from {
|
||||
opacity: 1;
|
||||
|
||||
@@ -11,9 +11,8 @@
|
||||
<p align="center"><strong>An open source server monitoring application</strong></p>
|
||||
|
||||
|
||||
https://github.com/user-attachments/assets/c8234cce-edf8-4d7e-8f6b-f18b06669fcc
|
||||
|
||||
|
||||

|
||||
(yes, we have a light theme as well, but this looks better on readme.md)
|
||||
|
||||
|
||||
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.
|
||||
|
||||
@@ -6,6 +6,7 @@ const {
|
||||
getTeamChecksParamValidation,
|
||||
getTeamChecksQueryValidation,
|
||||
deleteChecksParamValidation,
|
||||
deleteChecksByTeamIdParamValidation,
|
||||
} = require("../validation/joi");
|
||||
const { successMessages } = require("../utils/messages");
|
||||
const SERVICE_NAME = "check";
|
||||
@@ -112,9 +113,34 @@ const deleteChecks = async (req, res, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
const deleteChecksByTeamId = async (req, res, next) => {
|
||||
try {
|
||||
await deleteChecksByTeamIdParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "deleteChecksByTeam";
|
||||
error.status = 422;
|
||||
next(error);
|
||||
}
|
||||
|
||||
try {
|
||||
const deletedCount = await req.db.deleteChecksByTeamId(req.params.teamId);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.CHECK_DELETE,
|
||||
data: { deletedCount },
|
||||
});
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "deleteChecksByTeamId";
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createCheck,
|
||||
getChecks,
|
||||
getTeamChecks,
|
||||
deleteChecks,
|
||||
deleteChecksByTeamId,
|
||||
};
|
||||
|
||||
@@ -4,15 +4,20 @@ const {
|
||||
getMonitorsByTeamIdValidation,
|
||||
createMonitorBodyValidation,
|
||||
editMonitorBodyValidation,
|
||||
getMonitorsAndSummaryByTeamIdParamValidation,
|
||||
getMonitorsAndSummaryByTeamIdQueryValidation,
|
||||
getMonitorsByTeamIdQueryValidation,
|
||||
pauseMonitorParamValidation,
|
||||
getMonitorStatsByIdParamValidation,
|
||||
getMonitorStatsByIdQueryValidation,
|
||||
getCertificateParamValidation,
|
||||
getMonitorAggregateStatsParamValidation,
|
||||
getMonitorAggregateStatsQueryValidation,
|
||||
} = require("../validation/joi");
|
||||
|
||||
const sslChecker = require("ssl-checker");
|
||||
const SERVICE_NAME = "monitorController";
|
||||
const { errorMessages, successMessages } = require("../utils/messages");
|
||||
const { runInNewContext } = require("vm");
|
||||
|
||||
/**
|
||||
* Returns all monitors
|
||||
* @async
|
||||
@@ -35,6 +40,44 @@ const getAllMonitors = async (req, res, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns agregate stats for a monitor
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Express.Response>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
|
||||
const getMonitorAggregateStats = async (req, res, next) => {
|
||||
try {
|
||||
await getMonitorAggregateStatsParamValidation.validateAsync(req.params);
|
||||
await getMonitorAggregateStatsQueryValidation.validateAsync(req.query);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { monitorId } = req.params;
|
||||
const dateRange = req.query.dateRange;
|
||||
const aggregateStats = await req.db.getMonitorAggregateStats(
|
||||
monitorId,
|
||||
dateRange
|
||||
);
|
||||
return res.json({
|
||||
success: true,
|
||||
msg: successMessages.MONTIOR_STATS_BY_ID,
|
||||
data: aggregateStats,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns monitor stats for monitor with matching ID
|
||||
* @async
|
||||
@@ -45,7 +88,8 @@ const getAllMonitors = async (req, res, next) => {
|
||||
*/
|
||||
const getMonitorStatsById = async (req, res, next) => {
|
||||
try {
|
||||
//Validation
|
||||
await getMonitorStatsByIdParamValidation.validateAsync(req.params);
|
||||
await getMonitorStatsByIdQueryValidation.validateAsync(req.query);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.message =
|
||||
@@ -69,7 +113,7 @@ const getMonitorStatsById = async (req, res, next) => {
|
||||
|
||||
const getMonitorCertificate = async (req, res, next) => {
|
||||
try {
|
||||
//validation
|
||||
await getCertificateParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.message =
|
||||
@@ -143,7 +187,54 @@ const getMonitorById = async (req, res, next) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns all monitors belong to User with UserID
|
||||
* Returns all monitors and a sumamry for a team with TeamID
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Express.Response>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
|
||||
const getMonitorsAndSummaryByTeamId = async (req, res, next) => {
|
||||
try {
|
||||
await getMonitorsAndSummaryByTeamIdParamValidation.validateAsync(
|
||||
req.params
|
||||
);
|
||||
await getMonitorsAndSummaryByTeamIdQueryValidation.validateAsync(req.query);
|
||||
//validation
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.method === undefined &&
|
||||
error.method === "getMonitorsAndSummaryByTeamId";
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { teamId } = req.params;
|
||||
const { type } = req.query;
|
||||
const monitorsSummary = await req.db.getMonitorsAndSummaryByTeamId(
|
||||
teamId,
|
||||
type
|
||||
);
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.MONITOR_GET_BY_USER_ID(teamId),
|
||||
data: monitorsSummary,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method === undefined &&
|
||||
error.method === "getMonitorsAndSummaryByTeamId";
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns all monitors belong to team with TeamID
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
@@ -205,12 +296,14 @@ const createMonitor = async (req, res, next) => {
|
||||
const monitor = await req.db.createMonitor(req, res);
|
||||
|
||||
if (notifications && notifications.length !== 0) {
|
||||
await Promise.all(
|
||||
const setNotifications = await Promise.all(
|
||||
notifications.map(async (notification) => {
|
||||
notification.monitorId = monitor._id;
|
||||
await req.db.createNotification(notification);
|
||||
})
|
||||
);
|
||||
monitor.notifications = setNotifications;
|
||||
await monitor.save();
|
||||
}
|
||||
// Add monitor to job queue
|
||||
req.jobQueue.addJob(monitor._id, monitor);
|
||||
@@ -357,16 +450,14 @@ const pauseMonitor = async (req, res, next) => {
|
||||
await req.jobQueue.addJob(monitor._id, monitor);
|
||||
}
|
||||
monitor.isActive = !monitor.isActive;
|
||||
const updatedMonitor = await req.db.editMonitor(
|
||||
req.params.monitorId,
|
||||
monitor
|
||||
);
|
||||
monitor.status = undefined;
|
||||
monitor.save();
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: updatedMonitor.isActive
|
||||
msg: monitor.isActive
|
||||
? successMessages.MONITOR_RESUME
|
||||
: successMessages.MONITOR_PAUSE,
|
||||
data: updatedMonitor,
|
||||
data: monitor,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
@@ -377,9 +468,11 @@ const pauseMonitor = async (req, res, next) => {
|
||||
|
||||
module.exports = {
|
||||
getAllMonitors,
|
||||
getMonitorAggregateStats,
|
||||
getMonitorStatsById,
|
||||
getMonitorCertificate,
|
||||
getMonitorById,
|
||||
getMonitorsAndSummaryByTeamId,
|
||||
getMonitorsByTeamId,
|
||||
createMonitor,
|
||||
deleteMonitor,
|
||||
|
||||
@@ -66,6 +66,7 @@ const {
|
||||
getAllMonitors,
|
||||
getMonitorStatsById,
|
||||
getMonitorById,
|
||||
getMonitorsAndSummaryByTeamId,
|
||||
getMonitorsByTeamId,
|
||||
createMonitor,
|
||||
deleteMonitor,
|
||||
@@ -94,6 +95,7 @@ const {
|
||||
getChecks,
|
||||
getTeamChecks,
|
||||
deleteChecks,
|
||||
deleteChecksByTeamId,
|
||||
} = require("./modules/checkModule");
|
||||
|
||||
//****************************************
|
||||
@@ -145,6 +147,7 @@ module.exports = {
|
||||
getAllMonitors,
|
||||
getMonitorStatsById,
|
||||
getMonitorById,
|
||||
getMonitorsAndSummaryByTeamId,
|
||||
getMonitorsByTeamId,
|
||||
createMonitor,
|
||||
deleteMonitor,
|
||||
@@ -155,6 +158,7 @@ module.exports = {
|
||||
getChecks,
|
||||
getTeamChecks,
|
||||
deleteChecks,
|
||||
deleteChecksByTeamId,
|
||||
createAlert,
|
||||
getAlertsByUserId,
|
||||
getAlertsByMonitorId,
|
||||
|
||||
@@ -22,7 +22,27 @@ const dateRangeLookup = {
|
||||
|
||||
const createCheck = async (checkData) => {
|
||||
try {
|
||||
const { monitorId, status } = checkData;
|
||||
|
||||
const n = await Check.countDocuments({ monitorId }) + 1;
|
||||
|
||||
const check = await new Check({ ...checkData }).save();
|
||||
|
||||
const monitor = await Monitor.findById(monitorId);
|
||||
|
||||
if (!monitor) {
|
||||
throw new Error("Monitor not found");
|
||||
}
|
||||
|
||||
if (monitor.uptimePercentage === undefined) {
|
||||
monitor.uptimePercentage = (status === true) ? 1 : 0;
|
||||
} else {
|
||||
monitor.uptimePercentage =
|
||||
(monitor.uptimePercentage * (n - 1) + (status === true ? 1: 0)) / n;
|
||||
}
|
||||
|
||||
await monitor.save();
|
||||
|
||||
return check;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -170,7 +190,7 @@ const getTeamChecks = async (req) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete all checks for a user
|
||||
* Delete all checks for a monitor
|
||||
* @async
|
||||
* @param {string} monitorId
|
||||
* @returns {number}
|
||||
@@ -185,10 +205,40 @@ const deleteChecks = async (monitorId) => {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete all checks for a team
|
||||
* @async
|
||||
* @param {string} monitorId
|
||||
* @returns {number}
|
||||
* @throws {Error}
|
||||
*/
|
||||
|
||||
const deleteChecksByTeamId = async (teamId) => {
|
||||
try {
|
||||
const teamMonitors = await Monitor.find({ teamId: teamId });
|
||||
let totalDeletedCount = 0;
|
||||
|
||||
await Promise.all(
|
||||
teamMonitors.map(async (monitor) => {
|
||||
const result = await Check.deleteMany({ monitorId: monitor._id });
|
||||
totalDeletedCount += result.deletedCount;
|
||||
monitor.status = true;
|
||||
await monitor.save();
|
||||
})
|
||||
);
|
||||
|
||||
return totalDeletedCount;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createCheck,
|
||||
getChecksCount,
|
||||
getChecks,
|
||||
getTeamChecks,
|
||||
deleteChecks,
|
||||
deleteChecksByTeamId,
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@ const calculateUptimeDuration = (checks) => {
|
||||
const latestCheck = new Date(checks[0].createdAt);
|
||||
let latestDownCheck = 0;
|
||||
|
||||
for (let i = 0; i < checks.length; i++) {
|
||||
for (let i = checks.length; i <= 0; i--) {
|
||||
if (checks[i].status === false) {
|
||||
latestDownCheck = new Date(checks[i].createdAt);
|
||||
break;
|
||||
@@ -77,11 +77,11 @@ const getLatestResponseTime = (checks) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to get average 24h response time
|
||||
* Helper function to get average response time
|
||||
* @param {Array} checks Array of check objects.
|
||||
* @returns {number} Timestamp of the most recent check.
|
||||
*/
|
||||
const getAverageResponseTime24Hours = (checks) => {
|
||||
const getAverageResponseTime = (checks) => {
|
||||
if (!checks || checks.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
@@ -121,28 +121,7 @@ const getIncidents = (checks) => {
|
||||
return check.status === false ? (acc += 1) : acc;
|
||||
}, 0);
|
||||
};
|
||||
/**
|
||||
* Helper function to get all incidents
|
||||
* @param {Array} checks Array of check objects.
|
||||
* @returns {Array<Boolean>} Array of booleans representing up/down.
|
||||
*/
|
||||
const getStatusBarValues = (monitor, checks) => {
|
||||
const checksIn60Mins = Math.floor((60 * 60 * 1000) / monitor.interval);
|
||||
const noBlankChecks = checksIn60Mins - checks.length;
|
||||
|
||||
const statusBarValues = checks.map((check) => {
|
||||
return {
|
||||
status: check.status,
|
||||
responseTime: check.responseTime,
|
||||
value: 75,
|
||||
};
|
||||
});
|
||||
|
||||
for (let i = 0; i < noBlankChecks; i++) {
|
||||
statusBarValues.push({ status: undefined, responseTime: 0, value: 75 });
|
||||
}
|
||||
return statusBarValues.reverse();
|
||||
};
|
||||
/**
|
||||
* Get stats by monitor ID
|
||||
* @async
|
||||
@@ -152,42 +131,35 @@ const getStatusBarValues = (monitor, checks) => {
|
||||
* @throws {Error}
|
||||
*/
|
||||
const getMonitorStatsById = async (req) => {
|
||||
const filterLookup = {
|
||||
hour: new Date(new Date().getTime() - 60 * 60 * 1000),
|
||||
const startDates = {
|
||||
day: new Date(new Date().setDate(new Date().getDate() - 1)),
|
||||
week: new Date(new Date().setDate(new Date().getDate() - 7)),
|
||||
month: new Date(new Date().setMonth(new Date().getMonth() - 1)),
|
||||
};
|
||||
|
||||
const endDate = new Date();
|
||||
try {
|
||||
const { monitorId } = req.params;
|
||||
let { status, limit, sortOrder, dateRange, numToDisplay, normalize } =
|
||||
req.query;
|
||||
|
||||
// This effectively removes limit, returning all checks
|
||||
if (limit === undefined) limit = 0;
|
||||
|
||||
// Default sort order is newest -> oldest
|
||||
sortOrder = sortOrder === "asc" ? 1 : -1;
|
||||
|
||||
// Get monitor
|
||||
const { monitorId } = req.params;
|
||||
let { limit, sortOrder, dateRange, numToDisplay, normalize } = req.query;
|
||||
const monitor = await Monitor.findById(monitorId);
|
||||
if (monitor === null || monitor === undefined) {
|
||||
throw new Error(errorMessages.DB_FIND_MONTIOR_BY_ID(monitorId));
|
||||
}
|
||||
// This effectively removes limit, returning all checks
|
||||
if (limit === undefined) limit = 0;
|
||||
// Default sort order is newest -> oldest
|
||||
sortOrder = sortOrder === "asc" ? 1 : -1;
|
||||
|
||||
// Determine if this is a pagespeed monitor or an http/ping monitor
|
||||
let model =
|
||||
monitor.type === "http" || monitor.type === "ping"
|
||||
? Check
|
||||
: PageSpeedCheck;
|
||||
|
||||
// Build monitor stats object
|
||||
const monitorStats = {
|
||||
...monitor.toObject(),
|
||||
};
|
||||
|
||||
// Start building query
|
||||
// Build checks query
|
||||
const checksQuery = { monitorId: monitor._id };
|
||||
|
||||
// Get all checks
|
||||
@@ -195,76 +167,101 @@ const getMonitorStatsById = async (req) => {
|
||||
createdAt: sortOrder,
|
||||
});
|
||||
|
||||
const checksQueryForDateRange = {
|
||||
...checksQuery,
|
||||
createdAt: {
|
||||
$gte: startDates[dateRange],
|
||||
$lte: endDate,
|
||||
},
|
||||
};
|
||||
|
||||
const checksForDateRange = await model
|
||||
.find(checksQueryForDateRange)
|
||||
.sort({ createdAt: sortOrder });
|
||||
|
||||
if (monitor.type === "http" || monitor.type === "ping") {
|
||||
const checksQuery24Hours = {
|
||||
...checksQuery,
|
||||
createdAt: { $gte: filterLookup.day },
|
||||
};
|
||||
const checksQuery30Days = {
|
||||
...checksQuery,
|
||||
createdAt: { $gte: filterLookup.month },
|
||||
};
|
||||
const checksQuery60Mins = {
|
||||
...checksQuery,
|
||||
createdAt: { $gte: filterLookup.hour },
|
||||
};
|
||||
|
||||
const [checks24Hours, checks30Days, checks60Mins] = await Promise.all([
|
||||
model.find(checksQuery24Hours).sort({ createdAt: sortOrder }),
|
||||
model.find(checksQuery30Days).sort({ createdAt: sortOrder }),
|
||||
model.find(checksQuery60Mins).sort({ createdAt: sortOrder }),
|
||||
]);
|
||||
|
||||
// HTTP/PING Specific stats
|
||||
monitorStats.avgResponseTime24hours =
|
||||
getAverageResponseTime24Hours(checks24Hours);
|
||||
monitorStats.uptime24Hours = getUptimePercentage(checks24Hours);
|
||||
monitorStats.uptime30Days = getUptimePercentage(checks30Days);
|
||||
monitorStats.statusBar = getStatusBarValues(monitor, checks60Mins);
|
||||
monitorStats.periodAvgResponseTime =
|
||||
getAverageResponseTime(checksForDateRange);
|
||||
monitorStats.periodUptime = getUptimePercentage(checksForDateRange);
|
||||
|
||||
// Aggregate data
|
||||
let groupedChecks;
|
||||
// Group checks by hour if range is day
|
||||
if (dateRange === "day") {
|
||||
groupedChecks = checksForDateRange.reduce((acc, check) => {
|
||||
const time = new Date(check.createdAt);
|
||||
time.setMinutes(0, 0, 0);
|
||||
if (!acc[time]) {
|
||||
acc[time] = { time, checks: [] };
|
||||
}
|
||||
acc[time].checks.push(check);
|
||||
return acc;
|
||||
}, {});
|
||||
} else {
|
||||
groupedChecks = checksForDateRange.reduce((acc, check) => {
|
||||
const time = new Date(check.createdAt).toISOString().split("T")[0]; // Extract the date part
|
||||
if (!acc[time]) {
|
||||
acc[time] = { time, checks: [] };
|
||||
}
|
||||
acc[time].checks.push(check);
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
// Map grouped checks to stats
|
||||
const aggregateData = Object.values(groupedChecks).map((group) => {
|
||||
const totalChecks = group.checks.length;
|
||||
const uptimePercentage = getUptimePercentage(group.checks);
|
||||
const totalIncidents = group.checks.filter(
|
||||
(check) => check.status === false
|
||||
).length;
|
||||
const avgResponseTime =
|
||||
group.checks.reduce((sum, check) => sum + check.responseTime, 0) /
|
||||
totalChecks;
|
||||
|
||||
return {
|
||||
time: group.time,
|
||||
uptimePercentage,
|
||||
totalChecks,
|
||||
totalIncidents,
|
||||
avgResponseTime,
|
||||
};
|
||||
});
|
||||
monitorStats.aggregateData = aggregateData;
|
||||
}
|
||||
|
||||
//Get checks for dateRange
|
||||
if (status !== undefined) {
|
||||
checksQuery.status = status;
|
||||
}
|
||||
|
||||
// Filter checks by "day", "week", or "month"
|
||||
if (dateRange !== undefined) {
|
||||
checksQuery.createdAt = { $gte: filterLookup[dateRange] };
|
||||
}
|
||||
|
||||
let dateRangeChecks = await model
|
||||
.find(checksQuery)
|
||||
.sort({
|
||||
createdAt: sortOrder,
|
||||
})
|
||||
.limit(limit);
|
||||
|
||||
const incidents = getIncidents(dateRangeChecks);
|
||||
monitorStats.periodIncidents = getIncidents(checksForDateRange);
|
||||
monitorStats.periodTotalChecks = checksForDateRange.length;
|
||||
|
||||
// If more than numToDisplay checks, pick every nth check
|
||||
|
||||
let nthChecks = checksForDateRange;
|
||||
|
||||
if (
|
||||
numToDisplay !== undefined &&
|
||||
dateRangeChecks &&
|
||||
dateRangeChecks.length > numToDisplay
|
||||
checksForDateRange &&
|
||||
checksForDateRange.length > numToDisplay
|
||||
) {
|
||||
const n = Math.ceil(dateRangeChecks.length / numToDisplay);
|
||||
dateRangeChecks = dateRangeChecks.filter((_, index) => index % n === 0);
|
||||
const n = Math.ceil(checksForDateRange.length / numToDisplay);
|
||||
nthChecks = checksForDateRange.filter((_, index) => index % n === 0);
|
||||
}
|
||||
|
||||
// Normalize checks if requested
|
||||
if (normalize !== undefined) {
|
||||
dateRangeChecks = NormalizeData(dateRangeChecks, 1, 100);
|
||||
const normailzedChecks = NormalizeData(nthChecks, 1, 100);
|
||||
monitorStats.checks = normailzedChecks;
|
||||
} else {
|
||||
monitorStats.checks = nthChecks;
|
||||
}
|
||||
|
||||
// Add common stats and stats that depend on the dateRange
|
||||
monitorStats.uptimeDuration = calculateUptimeDuration(checksAll);
|
||||
monitorStats.lastChecked = getLastChecked(checksAll);
|
||||
monitorStats.latestResponseTime = getLatestResponseTime(checksAll);
|
||||
monitorStats.incidents = incidents;
|
||||
monitorStats.checks = dateRangeChecks;
|
||||
|
||||
return monitorStats;
|
||||
} catch (error) {
|
||||
error.methodName = "getMonitorStatsById";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -280,12 +277,58 @@ const getMonitorStatsById = async (req) => {
|
||||
const getMonitorById = async (monitorId) => {
|
||||
try {
|
||||
const monitor = await Monitor.findById(monitorId);
|
||||
return monitor;
|
||||
if (monitor === null || monitor === undefined) {
|
||||
throw new Error(errorMessages.DB_FIND_MONTIOR_BY_ID(monitorId));
|
||||
}
|
||||
// Get notifications
|
||||
const notifications = await Notification.find({
|
||||
monitorId: monitorId,
|
||||
});
|
||||
monitor.notifications = notifications;
|
||||
const monitorWithNotifications = await monitor.save();
|
||||
return monitorWithNotifications;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get monitors and Summary by TeamID
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Array<Monitor>>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
|
||||
const getMonitorsAndSummaryByTeamId = async (teamId, type) => {
|
||||
try {
|
||||
const monitors = await Monitor.find({ teamId, type });
|
||||
const monitorCounts = monitors.reduce(
|
||||
(acc, monitor) => {
|
||||
if (monitor.status === true) {
|
||||
acc.up += 1;
|
||||
}
|
||||
|
||||
if (monitor.status === false) {
|
||||
acc.down += 1;
|
||||
}
|
||||
|
||||
if (monitor.isActive === false) {
|
||||
acc.paused += 1;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{ up: 0, down: 0, paused: 0 }
|
||||
);
|
||||
monitorCounts.total = monitors.length;
|
||||
return { monitors, monitorCounts };
|
||||
} catch (error) {
|
||||
error.method = "getMonitorsAndSummaryByTeamId";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get monitors by TeamID
|
||||
* @async
|
||||
@@ -296,8 +339,19 @@ const getMonitorById = async (monitorId) => {
|
||||
*/
|
||||
const getMonitorsByTeamId = async (req, res) => {
|
||||
try {
|
||||
let { limit, type, status, sortOrder, normalize } = req.query || {};
|
||||
let { limit, type, status, sortOrder, normalize, page, rowsPerPage } =
|
||||
req.query || {};
|
||||
const monitorQuery = { teamId: req.params.teamId };
|
||||
const monitorsCount = await Monitor.countDocuments({
|
||||
teamId: req.params.teamId,
|
||||
type,
|
||||
});
|
||||
|
||||
// Pagination
|
||||
let skip = 0;
|
||||
if (page && rowsPerPage) {
|
||||
skip = page * rowsPerPage;
|
||||
}
|
||||
|
||||
if (type !== undefined) {
|
||||
const types = Array.isArray(type) ? type : [type];
|
||||
@@ -314,7 +368,9 @@ const getMonitorsByTeamId = async (req, res) => {
|
||||
// This effectively removes limit, returning all checks
|
||||
if (limit === undefined) limit = 0;
|
||||
|
||||
const monitors = await Monitor.find(monitorQuery);
|
||||
const monitors = await Monitor.find(monitorQuery)
|
||||
.skip(skip)
|
||||
.limit(rowsPerPage);
|
||||
// Map each monitor to include its associated checks
|
||||
const monitorsWithChecks = await Promise.all(
|
||||
monitors.map(async (monitor) => {
|
||||
@@ -340,15 +396,10 @@ const getMonitorsByTeamId = async (req, res) => {
|
||||
if (normalize !== undefined) {
|
||||
checks = NormalizeData(checks, 10, 100);
|
||||
}
|
||||
|
||||
// Get notifications
|
||||
const notifications = await Notification.find({
|
||||
monitorId: monitor._id,
|
||||
});
|
||||
return { ...monitor.toObject(), checks, notifications };
|
||||
return { ...monitor.toObject(), checks };
|
||||
})
|
||||
);
|
||||
return monitorsWithChecks;
|
||||
return { monitors: monitorsWithChecks, monitorCount: monitorsCount };
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -450,6 +501,7 @@ module.exports = {
|
||||
getAllMonitors,
|
||||
getMonitorStatsById,
|
||||
getMonitorById,
|
||||
getMonitorsAndSummaryByTeamId,
|
||||
getMonitorsByTeamId,
|
||||
createMonitor,
|
||||
deleteMonitor,
|
||||
|
||||
@@ -5,7 +5,10 @@ const handleErrors = (error, req, res, next) => {
|
||||
const status = error.status || 500;
|
||||
const message = error.message || errorMessages.FRIENDLY_ERROR;
|
||||
const service = error.service || errorMessages.UNKNOWN_SERVICE;
|
||||
logger.error(error.message, { service: service });
|
||||
logger.error(error.message, {
|
||||
service: service,
|
||||
methodName: error.methodName,
|
||||
});
|
||||
res.status(status).json({ success: false, msg: message });
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const mongoose = require("mongoose");
|
||||
const Notification = require("./Notification");
|
||||
|
||||
const MonitorSchema = mongoose.Schema(
|
||||
{
|
||||
@@ -23,7 +24,7 @@ const MonitorSchema = mongoose.Schema(
|
||||
},
|
||||
status: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
default: undefined,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
@@ -43,6 +44,16 @@ const MonitorSchema = mongoose.Schema(
|
||||
type: Number,
|
||||
default: 60000,
|
||||
},
|
||||
uptimePercentage: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
notifications: [
|
||||
{
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Notification",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
|
||||
@@ -0,0 +1,835 @@
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "BlueWave Uptime",
|
||||
"summary": "BlueWave Uptime OpenAPI Specifications",
|
||||
"description": "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.",
|
||||
"contact": {
|
||||
"name": "API Support",
|
||||
"url": "mailto:support@bluewavelabs.ca",
|
||||
"email": "support@bluewavelabs.ca"
|
||||
},
|
||||
"license": {
|
||||
"name": "AGPLv3",
|
||||
"url": "https://github.com/bluewave-labs/bluewave-uptime/tree/HEAD/LICENSE"
|
||||
},
|
||||
"version": "1.0"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "http://localhost:{PORT}/{API_PATH}",
|
||||
"description": "Local Development Server",
|
||||
"variables": {
|
||||
"PORT": {
|
||||
"description": "API Port",
|
||||
"enum": ["5000", "3000", "8080"],
|
||||
"default": "5000"
|
||||
},
|
||||
"API_PATH": {
|
||||
"description": "API Base Path",
|
||||
"enum": ["api/v1", "api/v1.1", "api/v2"],
|
||||
"default": "api/v1"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
{
|
||||
"name": "auth",
|
||||
"description": "Authentication"
|
||||
},
|
||||
{
|
||||
"name": "invite",
|
||||
"description": "Invite"
|
||||
},
|
||||
{
|
||||
"name": "monitors",
|
||||
"description": "Monitors"
|
||||
},
|
||||
{
|
||||
"name": "checks",
|
||||
"description": "Checks"
|
||||
},
|
||||
{
|
||||
"name": "alerts",
|
||||
"description": "Alerts"
|
||||
},
|
||||
{
|
||||
"name": "pagespeed",
|
||||
"description": "Page Speed"
|
||||
},
|
||||
{
|
||||
"name": "maintenance-window",
|
||||
"description": "Maintenance window"
|
||||
},
|
||||
{
|
||||
"name": "healthy",
|
||||
"description": "Health check"
|
||||
},
|
||||
{
|
||||
"name": "mail",
|
||||
"description": "Mail service"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/auth/register": {
|
||||
"post": {
|
||||
"tags": ["auth"],
|
||||
"description": "Register a new user",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["firstName", "lastName", "email", "password", "role", "teamId"],
|
||||
"properties": {
|
||||
"firstName": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastName": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"format": "password"
|
||||
},
|
||||
"profileImage": {
|
||||
"type": "file",
|
||||
"format": "file"
|
||||
},
|
||||
"role": {
|
||||
"type": "array",
|
||||
"enum": [["user"], ["admin"], ["superadmin"]],
|
||||
"default": ["superadmin"]
|
||||
},
|
||||
"teamId": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UserSuccessResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Content",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/login": {
|
||||
"post": {
|
||||
"tags": ["auth"],
|
||||
"description": "Login with credentials",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["email", "password"],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"format": "password"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UserSuccessResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Content",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/user/:userId": {
|
||||
"put": {
|
||||
"tags": ["auth"],
|
||||
"description": "Change user informations",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UserSuccessResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Content",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": ["auth"],
|
||||
"description": "Delete user",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UserSuccessResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Content",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/users/admin": {
|
||||
"get": {
|
||||
"tags": ["auth"],
|
||||
"description": "Checks to see if an admin account exists",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UserSuccessResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Content",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/users": {
|
||||
"get": {
|
||||
"tags": ["auth"],
|
||||
"description": "Get all users",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UserSuccessResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Content",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/invite": {
|
||||
"post": {
|
||||
"tags": ["auth"],
|
||||
"description": "Invite people to application",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UserSuccessResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Content",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/invite/verify": {
|
||||
"post": {
|
||||
"tags": ["auth"],
|
||||
"description": "Verify the invite",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UserSuccessResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Content",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/recovery/request": {
|
||||
"post": {
|
||||
"tags": ["auth"],
|
||||
"description": "Request a recovery token",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UserSuccessResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Content",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/recovery/validate": {
|
||||
"post": {
|
||||
"tags": ["auth"],
|
||||
"description": "Validate recovery token",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UserSuccessResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Content",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/auth/recovery/reset": {
|
||||
"post": {
|
||||
"tags": ["auth"],
|
||||
"description": "Password reset",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UserSuccessResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Content",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/healthy": {
|
||||
"get": {
|
||||
"tags": ["healthy"],
|
||||
"description": "Health check",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"ErrorResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SuccessResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UserSuccessResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"$ref": "#/components/schemas/UserDataType"
|
||||
}
|
||||
}
|
||||
},
|
||||
"MonitorSuccessResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"$ref": "#/components/schemas/MonitorDataType"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CheckDataSuccessResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"$ref": "#/components/schemas/CheckDataType"
|
||||
}
|
||||
}
|
||||
},
|
||||
"AlertDataSuccessResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"$ref": "#/components/schemas/AlertDataType"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UserDataType": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"firstname",
|
||||
"lastname",
|
||||
"email",
|
||||
"profilePicUrl",
|
||||
"isActive",
|
||||
"isVerified",
|
||||
"updatedAt",
|
||||
"createdAt"
|
||||
],
|
||||
"properties": {
|
||||
"firstname": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastname": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
},
|
||||
"profilePicUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"isActive": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isVerified": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"MonitorDataType": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"userId",
|
||||
"name",
|
||||
"description",
|
||||
"url",
|
||||
"isActive",
|
||||
"interval",
|
||||
"updatedAt",
|
||||
"createdAt"
|
||||
],
|
||||
"properties": {
|
||||
"userId": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"isActive": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"interval": {
|
||||
"type": "integer"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CheckDataType": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"monitorId",
|
||||
"status",
|
||||
"responseTime",
|
||||
"statusCode",
|
||||
"message",
|
||||
"updatedAt",
|
||||
"createdAt"
|
||||
],
|
||||
"properties": {
|
||||
"monitorId": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"responseTime": {
|
||||
"type": "integer"
|
||||
},
|
||||
"statusCode": {
|
||||
"type": "integer"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"AlertDataType": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"checkId",
|
||||
"monitorId",
|
||||
"userId",
|
||||
"status",
|
||||
"message",
|
||||
"notifiedStatus",
|
||||
"acknowledgedStatus",
|
||||
"updatedAt",
|
||||
"createdAt"
|
||||
],
|
||||
"properties": {
|
||||
"checkId": {
|
||||
"type": "string"
|
||||
},
|
||||
"monitorId": {
|
||||
"type": "string"
|
||||
},
|
||||
"userId": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"notifiedStatus": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"acknowledgedStatus": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
const router = require("express").Router();
|
||||
const checkController = require("../controllers/checkController");
|
||||
const { verifyOwnership } = require("../middleware/verifyOwnership");
|
||||
const { isAllowed } = require("../middleware/isAllowed");
|
||||
const Monitor = require("../models/Monitor");
|
||||
|
||||
router.post(
|
||||
@@ -19,4 +20,10 @@ router.delete(
|
||||
checkController.deleteChecks
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/team/:teamId",
|
||||
isAllowed(["admin", "superadmin"]),
|
||||
checkController.deleteChecksByTeamId
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -3,9 +3,14 @@ const monitorController = require("../controllers/monitorController");
|
||||
const { isAllowed } = require("../middleware/isAllowed");
|
||||
|
||||
router.get("/", monitorController.getAllMonitors);
|
||||
router.get("/aggregate/:monitorId", monitorController.getMonitorAggregateStats);
|
||||
router.get("/stats/:monitorId", monitorController.getMonitorStatsById);
|
||||
router.get("/certificate/:monitorId", monitorController.getMonitorCertificate);
|
||||
router.get("/:monitorId", monitorController.getMonitorById);
|
||||
router.get(
|
||||
"/team/summary/:teamId",
|
||||
monitorController.getMonitorsAndSummaryByTeamId
|
||||
);
|
||||
router.get("/team/:teamId", monitorController.getMonitorsByTeamId);
|
||||
|
||||
router.post(
|
||||
|
||||
@@ -39,7 +39,9 @@ class JobQueue {
|
||||
queue.networkService = networkService;
|
||||
const monitors = await db.getAllMonitors();
|
||||
for (const monitor of monitors) {
|
||||
await queue.addJob(monitor.id, monitor);
|
||||
if (monitor.active) {
|
||||
await queue.addJob(monitor.id, monitor);
|
||||
}
|
||||
}
|
||||
const workerStats = await queue.getWorkerStats();
|
||||
await queue.scaleWorkers(workerStats);
|
||||
@@ -238,7 +240,6 @@ class JobQueue {
|
||||
console.log("Adding job", payload.url);
|
||||
// Execute job immediately
|
||||
await this.queue.add(jobName, payload);
|
||||
|
||||
await this.queue.add(jobName, payload, {
|
||||
repeat: {
|
||||
every: payload.interval,
|
||||
|
||||
@@ -15,28 +15,15 @@ class NetworkService {
|
||||
this.NETWORK_ERROR = 5000;
|
||||
}
|
||||
|
||||
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) {
|
||||
monitor.status = !monitor.status;
|
||||
await monitor.save();
|
||||
|
||||
async handleNotification(monitor, isAlive) {
|
||||
try {
|
||||
let template =
|
||||
isAlive === true ? "serverIsUpTemplate" : "serverIsDownTemplate";
|
||||
let status = isAlive === true ? "up" : "down";
|
||||
|
||||
const notifications = await this.db.getNotificationsByMonitorId(_id);
|
||||
const notifications = await this.db.getNotificationsByMonitorId(
|
||||
monitor._id
|
||||
);
|
||||
for (const notification of notifications) {
|
||||
if (notification.type === "email") {
|
||||
await this.emailService.buildAndSendEmail(
|
||||
@@ -47,6 +34,43 @@ class NetworkService {
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error.message, {
|
||||
method: "handleNotification",
|
||||
service: this.SERVICE_NAME,
|
||||
monitorId: monitor._id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async handleStatusUpdate(job, isAlive) {
|
||||
try {
|
||||
const { _id } = job.data;
|
||||
const monitor = await this.db.getMonitorById(_id);
|
||||
|
||||
if (monitor === null || monitor === undefined) {
|
||||
logger.error(`Null Monitor: ${_id}`, {
|
||||
method: "handleStatusUpdate",
|
||||
service: this.SERVICE_NAME,
|
||||
jobId: job.id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (monitor.status === undefined || monitor.status !== isAlive) {
|
||||
const oldStatus = monitor.status;
|
||||
monitor.status = isAlive;
|
||||
await monitor.save();
|
||||
|
||||
if (oldStatus !== undefined && oldStatus !== isAlive) {
|
||||
this.handleNotification(monitor, isAlive);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error.message, {
|
||||
method: "handleStatusUpdate",
|
||||
service: this.SERVICE_NAME,
|
||||
jobId: job.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +129,7 @@ class NetworkService {
|
||||
};
|
||||
return await this.logAndStoreCheck(checkData, this.db.createCheck);
|
||||
} finally {
|
||||
this.handleNotification(job, isAlive);
|
||||
this.handleStatusUpdate(job, isAlive);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +177,7 @@ class NetworkService {
|
||||
|
||||
return await this.logAndStoreCheck(checkData, this.db.createCheck);
|
||||
} finally {
|
||||
this.handleNotification(job, isAlive);
|
||||
this.handleStatusUpdate(job, isAlive);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +197,6 @@ class NetworkService {
|
||||
*/
|
||||
async handlePagespeed(job) {
|
||||
let isAlive;
|
||||
|
||||
try {
|
||||
const url = job.data.url;
|
||||
const response = await axios.get(
|
||||
@@ -233,7 +256,7 @@ class NetworkService {
|
||||
};
|
||||
this.logAndStoreCheck(checkData, this.db.createPageSpeedCheck);
|
||||
} finally {
|
||||
this.handleNotification(job, isAlive);
|
||||
this.handleStatusUpdate(job, isAlive);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const joi = require("joi");
|
||||
const { normalize } = require("path");
|
||||
|
||||
//****************************************
|
||||
// Custom Validators
|
||||
@@ -160,6 +161,19 @@ const getMonitorByIdQueryValidation = joi.object({
|
||||
normalize: joi.boolean(),
|
||||
});
|
||||
|
||||
const getMonitorsAndSummaryByTeamIdParamValidation = joi.object({
|
||||
teamId: joi.string().required(),
|
||||
});
|
||||
|
||||
const getMonitorsAndSummaryByTeamIdQueryValidation = joi.object({
|
||||
type: joi
|
||||
.alternatives()
|
||||
.try(
|
||||
joi.string().valid("http", "ping", "pagespeed"),
|
||||
joi.array().items(joi.string().valid("http", "ping", "pagespeed"))
|
||||
),
|
||||
});
|
||||
|
||||
const getMonitorsByTeamIdValidation = joi.object({
|
||||
teamId: joi.string().required(),
|
||||
});
|
||||
@@ -175,6 +189,24 @@ const getMonitorsByTeamIdQueryValidation = joi.object({
|
||||
joi.string().valid("http", "ping", "pagespeed"),
|
||||
joi.array().items(joi.string().valid("http", "ping", "pagespeed"))
|
||||
),
|
||||
page: joi.number(),
|
||||
rowsPerPage: joi.number(),
|
||||
});
|
||||
|
||||
const getMonitorStatsByIdParamValidation = joi.object({
|
||||
monitorId: joi.string().required(),
|
||||
});
|
||||
const getMonitorStatsByIdQueryValidation = joi.object({
|
||||
status: joi.string(),
|
||||
limit: joi.number(),
|
||||
sortOrder: joi.string().valid("asc", "desc"),
|
||||
dateRange: joi.string().valid("day", "week", "month"),
|
||||
numToDisplay: joi.number(),
|
||||
normalize: joi.boolean(),
|
||||
});
|
||||
|
||||
const getCertificateParamValidation = joi.object({
|
||||
monitorId: joi.string().required(),
|
||||
});
|
||||
|
||||
const createMonitorBodyValidation = joi.object({
|
||||
@@ -201,6 +233,13 @@ const pauseMonitorParamValidation = joi.object({
|
||||
monitorId: joi.string().required(),
|
||||
});
|
||||
|
||||
const getMonitorAggregateStatsParamValidation = joi.object({
|
||||
monitorId: joi.string().required(),
|
||||
});
|
||||
const getMonitorAggregateStatsQueryValidation = joi.object({
|
||||
dateRange: joi.string().valid("day", "week", "month"),
|
||||
});
|
||||
|
||||
//****************************************
|
||||
// Alerts
|
||||
//****************************************
|
||||
@@ -292,6 +331,10 @@ const deleteChecksParamValidation = joi.object({
|
||||
monitorId: joi.string().required(),
|
||||
});
|
||||
|
||||
const deleteChecksByTeamIdParamValidation = joi.object({
|
||||
teamId: joi.string().required(),
|
||||
});
|
||||
|
||||
//****************************************
|
||||
// PageSpeedCheckValidation
|
||||
//****************************************
|
||||
@@ -351,8 +394,15 @@ module.exports = {
|
||||
createMonitorBodyValidation,
|
||||
getMonitorByIdParamValidation,
|
||||
getMonitorByIdQueryValidation,
|
||||
getMonitorsAndSummaryByTeamIdParamValidation,
|
||||
getMonitorsAndSummaryByTeamIdQueryValidation,
|
||||
getMonitorsByTeamIdValidation,
|
||||
getMonitorsByTeamIdQueryValidation,
|
||||
getMonitorStatsByIdParamValidation,
|
||||
getMonitorStatsByIdQueryValidation,
|
||||
getCertificateParamValidation,
|
||||
getMonitorAggregateStatsParamValidation,
|
||||
getMonitorAggregateStatsQueryValidation,
|
||||
editMonitorBodyValidation,
|
||||
pauseMonitorParamValidation,
|
||||
editUserParamValidation,
|
||||
@@ -372,6 +422,7 @@ module.exports = {
|
||||
getTeamChecksParamValidation,
|
||||
getTeamChecksQueryValidation,
|
||||
deleteChecksParamValidation,
|
||||
deleteChecksByTeamIdParamValidation,
|
||||
deleteUserParamValidation,
|
||||
getPageSpeedCheckParamValidation,
|
||||
createPageSpeedCheckParamValidation,
|
||||
|
||||