32 files changed
+930
-350
No files matched your search
+2
-2
@@ -2,11 +2,11 @@ language: "en-CA"
|
||||
tone_instructions: "His palms are sweaty, knees weak, arms are heavy There’s vomit on his sweater already, mom’s spaghetti"
|
||||
early_access: false
|
||||
reviews:
|
||||
profile: "assertive"
|
||||
profile: "chill"
|
||||
request_changes_workflow: false
|
||||
high_level_summary: false
|
||||
poem: false
|
||||
review_status: true
|
||||
review_status: false
|
||||
auto_review:
|
||||
enabled: true
|
||||
drafts: false
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
.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,5 +1,6 @@
|
||||
import { Button as MuiButton } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
import "./index.css";
|
||||
|
||||
const levelConfig = {
|
||||
primary: {
|
||||
@@ -48,11 +49,22 @@ const levelConfig = {
|
||||
* <Button type="submit" level="error" label="Error" disabled sx={{marginTop: "1rem"}}/>
|
||||
*/
|
||||
|
||||
const Button = ({ id, type, level, label, disabled, img, onClick, sx }) => {
|
||||
const Button = ({
|
||||
id,
|
||||
animate,
|
||||
type,
|
||||
level,
|
||||
label,
|
||||
disabled,
|
||||
img,
|
||||
onClick,
|
||||
sx,
|
||||
}) => {
|
||||
const { variant, color } = levelConfig[level];
|
||||
return (
|
||||
<MuiButton
|
||||
id={id}
|
||||
className={`button-${animate}`}
|
||||
type={type}
|
||||
variant={variant}
|
||||
color={color}
|
||||
@@ -60,15 +72,20 @@ const Button = ({ id, type, level, label, disabled, img, onClick, sx }) => {
|
||||
onClick={onClick}
|
||||
disableRipple
|
||||
sx={{
|
||||
lineHeight: 1,
|
||||
boxShadow: "none",
|
||||
textTransform: "none",
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"&:hover": {
|
||||
boxShadow: "none",
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{img && img}
|
||||
{label}
|
||||
<span>{label}</span>
|
||||
</MuiButton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -72,10 +72,15 @@ const ButtonSpinner = ({
|
||||
loading={isLoading}
|
||||
disableRipple
|
||||
sx={{
|
||||
boxShadow: "none",
|
||||
textTransform: "none",
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
"&:hover": {
|
||||
boxShadow: "none",
|
||||
transition: "none"
|
||||
},
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -177,7 +177,7 @@ Field.propTypes = {
|
||||
autoComplete: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
value: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
error: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -68,6 +68,62 @@ export const getUptimeMonitorsByUserId = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
export const updateUptimeMonitor = createAsyncThunk(
|
||||
"monitors/updateMonitor",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitor } = data;
|
||||
const updatedFields = {
|
||||
name: monitor.name,
|
||||
description: monitor.description,
|
||||
interval: monitor.interval,
|
||||
notifications: monitor.notifications
|
||||
};
|
||||
const res = await axiosInstance.post(
|
||||
`/monitors/edit/${monitor._id}`,
|
||||
updatedFields,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteUptimeMonitor = createAsyncThunk(
|
||||
"monitors/deleteMonitor",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitor } = data;
|
||||
const res = await axiosInstance.post(
|
||||
`/monitors/delete/${monitor._id}`,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
return thunkApi.rejectWithValue(error.message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const uptimeMonitorsSlice = createSlice({
|
||||
name: "uptimeMonitors",
|
||||
initialState,
|
||||
@@ -137,6 +193,44 @@ const uptimeMonitorsSlice = createSlice({
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to create uptime monitor";
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
// update Monitor
|
||||
// *****************************************************
|
||||
.addCase(updateUptimeMonitor.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(updateUptimeMonitor.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(updateUptimeMonitor.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to update uptime monitor";
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
// Delete Monitor
|
||||
// *****************************************************
|
||||
.addCase(deleteUptimeMonitor.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(deleteUptimeMonitor.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(deleteUptimeMonitor.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to delete uptime monitor";
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
.account .MuiButtonGroup-root button {
|
||||
border-color: var(--env-var-color-16);
|
||||
}
|
||||
.account button{
|
||||
height: 34px;
|
||||
}
|
||||
.account [class$="-form"] {
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
@@ -101,7 +101,9 @@
|
||||
/* ///// */
|
||||
/* LOGIN */
|
||||
/* ///// */
|
||||
.login-page span:not(.MuiTypography-root):not(.MuiButtonBase-root) {
|
||||
.login-page
|
||||
.MuiStack-root:not(:has(> .MuiButtonBase-root))
|
||||
span:not(.MuiTypography-root):not(.field-required) {
|
||||
color: var(--env-var-color-3);
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { Stack, Box, Typography, Grid } from "@mui/material";
|
||||
import { Stack, Typography, Grid } from "@mui/material";
|
||||
import Button from "../../Components/Button";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import Discord from "../../assets/icons/discord-icon.svg?react";
|
||||
import Slack from "../../assets/icons/slack-icon.svg?react";
|
||||
import Zapier from "../../assets/icons/zapier-icon.svg?react";
|
||||
|
||||
import "./index.css";
|
||||
|
||||
/**
|
||||
* Integrations component
|
||||
* @param {Object} props - Props for the IntegrationsComponent.
|
||||
* @param {string} props.url - The URL for the integration image.
|
||||
* @param {string} props.icon - The icon for the integration image.
|
||||
* @param {string} props.header - The header for the integration.
|
||||
* @param {string} props.info - Information about the integration.
|
||||
* @param {Function} props.onClick - The onClick handler for the integration button.
|
||||
* @returns {JSX.Element} The JSX representation of the IntegrationsComponent.
|
||||
*/
|
||||
const IntegrationsComponent = ({ url, header, info, onClick }) => {
|
||||
const IntegrationsComponent = ({ icon, header, info, onClick }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
@@ -24,14 +27,10 @@ const IntegrationsComponent = ({ url, header, info, onClick }) => {
|
||||
justifyContent="space-between"
|
||||
gap={theme.gap.large}
|
||||
p={theme.gap.ml}
|
||||
pl={theme.gap.large}
|
||||
height="100%"
|
||||
>
|
||||
<Box
|
||||
component="img"
|
||||
src={url}
|
||||
alt="Integration"
|
||||
width={80}
|
||||
height={80}
|
||||
/>
|
||||
{icon}
|
||||
<Stack gap={theme.gap.xs} flex={1}>
|
||||
<Typography component="h1">{header}</Typography>
|
||||
<Typography
|
||||
@@ -48,6 +47,7 @@ const IntegrationsComponent = ({ url, header, info, onClick }) => {
|
||||
level="primary"
|
||||
onClick={onClick}
|
||||
sx={{ alignSelf: "center" }}
|
||||
disabled={true}
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
@@ -56,7 +56,7 @@ const IntegrationsComponent = ({ url, header, info, onClick }) => {
|
||||
|
||||
// PropTypes for IntegrationsComponent
|
||||
IntegrationsComponent.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
icon: PropTypes.object.isRequired,
|
||||
header: PropTypes.string.isRequired,
|
||||
info: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
@@ -72,19 +72,34 @@ const Integrations = () => {
|
||||
|
||||
const integrations = [
|
||||
{
|
||||
url: "https://via.placeholder.com/80",
|
||||
icon: (
|
||||
<Slack
|
||||
alt="slack integration"
|
||||
style={{ width: "45px", height: "45px", alignSelf: "center" }}
|
||||
/>
|
||||
),
|
||||
header: "Slack",
|
||||
info: "Connect with Slack and see incidents in a channel",
|
||||
onClick: () => {},
|
||||
},
|
||||
{
|
||||
url: "https://via.placeholder.com/80",
|
||||
icon: (
|
||||
<Discord
|
||||
alt="discord integration"
|
||||
style={{ width: "42px", height: "42px", alignSelf: "center" }}
|
||||
/>
|
||||
),
|
||||
header: "Discord",
|
||||
info: "Connect with Discord and view incidents directly in a channel",
|
||||
onClick: () => {},
|
||||
},
|
||||
{
|
||||
url: "https://via.placeholder.com/80",
|
||||
icon: (
|
||||
<Zapier
|
||||
alt="zapier integration"
|
||||
style={{ width: "42px", height: "42px", alignSelf: "center" }}
|
||||
/>
|
||||
),
|
||||
header: "Zapier",
|
||||
info: "Send all incidents to Zapier, and then see them everywhere",
|
||||
onClick: () => {},
|
||||
@@ -102,7 +117,7 @@ const Integrations = () => {
|
||||
{integrations.map((integration, index) => (
|
||||
<IntegrationsComponent
|
||||
key={index}
|
||||
url={integration.url}
|
||||
icon={integration.icon}
|
||||
header={integration.header}
|
||||
info={integration.info}
|
||||
onClick={integration.onClick}
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
.configure-monitor .MuiSelect-select {
|
||||
color: var(--env-var-color-5);
|
||||
}
|
||||
.configure-monitor .MuiModal-root h2.MuiTypography-root {
|
||||
color: var(--env-var-color-1);
|
||||
}
|
||||
.configure-monitor p.MuiTypography-root,
|
||||
.configure-monitor .MuiSelect-select {
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
@@ -38,6 +41,7 @@
|
||||
padding-bottom: 60px;
|
||||
border: 1px solid var(--env-var-color-16);
|
||||
border-radius: var(--env-var-radius-1);
|
||||
background-color: var(--env-var-color-8);
|
||||
}
|
||||
.configure-monitor .config-box .MuiBox-root,
|
||||
.configure-monitor .config-box .MuiStack-root {
|
||||
@@ -50,6 +54,9 @@
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
}
|
||||
.configure-monitor .MuiInputBase-root:has(> .Mui-disabled) {
|
||||
background-color: var(--env-var-color-13);
|
||||
}
|
||||
|
||||
.MuiInputBase-root:has(#monitor-interval) {
|
||||
height: 34px;
|
||||
@@ -74,4 +81,4 @@
|
||||
body:has(.configure-monitor) .select-dropdown .MuiMenuItem-root,
|
||||
.configure-monitor .select-wrapper .select-component > .MuiSelect-select {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,7 @@ import { useDispatch, useSelector } from "react-redux";
|
||||
import { useEffect, useState } from "react";
|
||||
import Button from "../../../Components/Button";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import RadioButton from "../../../Components/RadioButton";
|
||||
import { Box, MenuItem, Stack, Typography } from "@mui/material";
|
||||
import { Box, Modal, Stack, Typography } from "@mui/material";
|
||||
import WestRoundedIcon from "@mui/icons-material/WestRounded";
|
||||
import GreenCheck from "../../../assets/icons/checkbox-green.svg?react";
|
||||
import RedCheck from "../../../assets/icons/checkbox-red.svg?react";
|
||||
@@ -15,6 +14,27 @@ import "./index.css";
|
||||
import { monitorValidation } from "../../../Validation/validation";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import { formatDurationRounded } from "../../../Utils/timeUtils";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import {
|
||||
updateUptimeMonitor,
|
||||
getUptimeMonitorsByUserId,
|
||||
deleteUptimeMonitor,
|
||||
} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import Checkbox from "../../../Components/Inputs/Checkbox";
|
||||
|
||||
/**
|
||||
* Parses a URL string and returns a URL object.
|
||||
*
|
||||
* @param {string} url - The URL string to parse.
|
||||
* @returns {URL} - The parsed URL object if valid, otherwise an empty string.
|
||||
*/
|
||||
const parseUrl = (url) => {
|
||||
try {
|
||||
return new URL(url);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to get duration since last check
|
||||
@@ -37,8 +57,11 @@ const Configure = () => {
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
const { monitors } = useSelector((state) => state.uptimeMonitors);
|
||||
const [monitor, setMonitor] = useState();
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [errors, setErrors] = useState({});
|
||||
const { monitorId } = useParams();
|
||||
|
||||
const idMap = {
|
||||
@@ -46,51 +69,93 @@ const Configure = () => {
|
||||
"monitor-name": "name",
|
||||
"monitor-checks-http": "type",
|
||||
"monitor-checks-ping": "type",
|
||||
"notify-email-default": "notification-email",
|
||||
};
|
||||
|
||||
const [config, setConfig] = useState();
|
||||
const [monitor, setMonitor] = useState();
|
||||
const [errors, setErrors] = useState({});
|
||||
useEffect(() => {
|
||||
const fetchMonitor = () => {
|
||||
const data = monitors.find((monitor) => monitor._id === monitorId);
|
||||
setConfig(data);
|
||||
setMonitor({
|
||||
name: data.name,
|
||||
url: data.url.replace(/^https?:\/\//, ""),
|
||||
type: data.type,
|
||||
interval: data.interval / MS_PER_MINUTE,
|
||||
});
|
||||
};
|
||||
fetchMonitor();
|
||||
}, [monitorId, authToken]);
|
||||
const data = monitors.find((monitor) => monitor._id === monitorId);
|
||||
setMonitor({
|
||||
...data,
|
||||
});
|
||||
setDuration(formatDurationRounded(data?.interval));
|
||||
}, [monitorId, authToken, monitors]);
|
||||
|
||||
const handleChange = (event, name) => {
|
||||
const { value, id } = event.target;
|
||||
let { value, id } = event.target;
|
||||
if (!name) name = idMap[id];
|
||||
setMonitor((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
|
||||
const validation = monitorValidation.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
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 {
|
||||
if (name === "interval") {
|
||||
value = value * MS_PER_MINUTE;
|
||||
}
|
||||
setMonitor((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
const validation = monitorValidation.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
|
||||
if (validation.error)
|
||||
updatedErrors[name] = validation.error.details[0].message;
|
||||
else delete updatedErrors[name];
|
||||
return updatedErrors;
|
||||
});
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
|
||||
if (validation.error)
|
||||
updatedErrors[name] = validation.error.details[0].message;
|
||||
else delete updatedErrors[name];
|
||||
return updatedErrors;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
// TODO
|
||||
const action = await dispatch(
|
||||
updateUptimeMonitor({ authToken, monitor: monitor })
|
||||
);
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
createToast({ body: "Monitor updated successfully!" });
|
||||
dispatch(getUptimeMonitorsByUserId(authToken));
|
||||
} else {
|
||||
createToast({ body: "Failed to update monitor." });
|
||||
}
|
||||
};
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const handleRemove = async (event) => {
|
||||
event.preventDefault();
|
||||
const action = await dispatch(deleteUptimeMonitor({ authToken, monitor }));
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
navigate("/monitors");
|
||||
} else {
|
||||
createToast({ body: "Failed to delete monitor." });
|
||||
}
|
||||
};
|
||||
|
||||
const frequencies = [
|
||||
@@ -101,11 +166,18 @@ const Configure = () => {
|
||||
{ _id: 5, name: "5 minutes" },
|
||||
];
|
||||
|
||||
// Parse the URL
|
||||
const parsedUrl = parseUrl(monitor?.url);
|
||||
const protocol = parsedUrl?.protocol?.replace(":", "") || "";
|
||||
|
||||
var lastChecked = formatDurationRounded(getLastChecked(monitor?.checks));
|
||||
|
||||
return (
|
||||
<Box className="configure-monitor">
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Back"
|
||||
animate="slideLeft"
|
||||
img={<WestRoundedIcon />}
|
||||
onClick={() => navigate(-1)}
|
||||
sx={{
|
||||
@@ -113,31 +185,39 @@ const Configure = () => {
|
||||
mb: theme.gap.medium,
|
||||
px: theme.gap.ml,
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
pr: theme.gap.small,
|
||||
mr: theme.gap.small,
|
||||
fill: theme.palette.otherColors.slateGray,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<form className="configure-monitor-form" noValidate spellCheck="false">
|
||||
<Stack direction="row" gap={theme.gap.small} mt={theme.gap.small}>
|
||||
{config?.status ? <GreenCheck /> : <RedCheck />}
|
||||
{monitor?.status ? <GreenCheck /> : <RedCheck />}
|
||||
<Box>
|
||||
<Typography component="h1" sx={{ lineHeight: 1 }}>
|
||||
{config?.url.replace(/^https?:\/\//, "") || "..."}
|
||||
</Typography>
|
||||
<Typography mt={theme.gap.small}>
|
||||
{parsedUrl?.host ? (
|
||||
<Typography
|
||||
component="h1"
|
||||
mb={theme.gap.small}
|
||||
sx={{ lineHeight: 1 }}
|
||||
>
|
||||
{parsedUrl.host || "..."}
|
||||
</Typography>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<Typography lineHeight={parsedUrl?.host ? 1 : theme.gap.large}>
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
color: config?.status
|
||||
color: monitor?.status
|
||||
? "var(--env-var-color-17)"
|
||||
: "var(--env-var-color-24)",
|
||||
}}
|
||||
>
|
||||
Your site is {config?.status ? "up" : "down"}.
|
||||
Your site is {monitor?.status ? "up" : "down"}.
|
||||
</Typography>{" "}
|
||||
Checking every {formatDurationRounded(config?.interval)}. Last
|
||||
time checked{" "}
|
||||
{formatDurationRounded(getLastChecked(config?.checks))} ago.
|
||||
Checking every {duration}.{" "}
|
||||
{lastChecked ? `Last time checked ${lastChecked} ago.` : ""}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack
|
||||
@@ -151,13 +231,14 @@ const Configure = () => {
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Pause"
|
||||
animate="rotate180"
|
||||
img={<PauseCircleOutlineIcon />}
|
||||
sx={{
|
||||
backgroundColor: "#f4f4f4",
|
||||
pl: theme.gap.small,
|
||||
pr: theme.gap.medium,
|
||||
"& svg": {
|
||||
pr: theme.gap.xs,
|
||||
mr: theme.gap.xs,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
@@ -168,6 +249,7 @@ const Configure = () => {
|
||||
boxShadow: "none",
|
||||
px: theme.gap.ml,
|
||||
}}
|
||||
onClick={() => setIsOpen(true)}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -186,12 +268,13 @@ const Configure = () => {
|
||||
</Box>
|
||||
<Stack gap={theme.gap.xl}>
|
||||
<Field
|
||||
type="url"
|
||||
type={monitor?.type === "http" ? "url" : "text"}
|
||||
https={protocol === "https"}
|
||||
id="monitor-url"
|
||||
label="URL to monitor"
|
||||
placeholder="google.com"
|
||||
value={monitor?.url || ""}
|
||||
onChange={handleChange}
|
||||
value={parsedUrl?.host || monitor?.url || ""}
|
||||
disabled={true}
|
||||
error={errors["url"]}
|
||||
/>
|
||||
<Field
|
||||
@@ -213,39 +296,60 @@ const Configure = () => {
|
||||
gap={theme.gap.xxl}
|
||||
>
|
||||
<Box>
|
||||
<Typography component="h2">Checks to perform</Typography>
|
||||
<Typography component="p" sx={{ mt: theme.gap.small }}>
|
||||
You can always add or remove checks after adding your site.
|
||||
<Typography component="h2">Incident notifications</Typography>
|
||||
<Typography component="p" mt={theme.gap.small}>
|
||||
When there is an incident, notify users.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.gap.xl}>
|
||||
<RadioButton
|
||||
id="monitor-checks-http"
|
||||
title="HTTP/website monitoring"
|
||||
desc="Use HTTP(s) to monitor your website or API endpoint."
|
||||
size="small"
|
||||
value="http"
|
||||
checked={monitor?.type === "http"}
|
||||
onChange={handleChange}
|
||||
<Stack gap={theme.gap.medium}>
|
||||
<Typography component="p" mt={theme.gap.small}>
|
||||
When there is a new incident,
|
||||
</Typography>
|
||||
<Checkbox
|
||||
id="notify-sms"
|
||||
label="Notify via SMS (coming soon)"
|
||||
isChecked={false}
|
||||
value=""
|
||||
onChange={() => console.log("disabled")}
|
||||
isDisabled={true}
|
||||
/>
|
||||
<RadioButton
|
||||
id="monitor-checks-ping"
|
||||
title="Ping monitoring"
|
||||
desc="Check whether your server is available or not."
|
||||
size="small"
|
||||
value="ping"
|
||||
checked={monitor?.type === "ping"}
|
||||
onChange={handleChange}
|
||||
<Checkbox
|
||||
id="notify-email-default"
|
||||
label={`Notify via email (to ${user.email})`}
|
||||
isChecked={
|
||||
monitor?.notifications?.some(
|
||||
(notification) => notification.type === "email"
|
||||
) || false
|
||||
}
|
||||
value={user?.email}
|
||||
onChange={(event) => handleChange(event)}
|
||||
/>
|
||||
<Box className="error-container">
|
||||
{errors["type"] ? (
|
||||
<Typography component="p" className="input-error">
|
||||
{errors["type"]}
|
||||
<Checkbox
|
||||
id="notify-email"
|
||||
label="Also notify via email to multiple addresses (coming soon)"
|
||||
isChecked={false}
|
||||
value=""
|
||||
onChange={() => console.log("disabled")}
|
||||
isDisabled={true}
|
||||
/>
|
||||
{monitor?.notifications?.some(
|
||||
(notification) => notification.type === "emails"
|
||||
) ? (
|
||||
<Box mx={`calc(${theme.gap.ml} * 2)`}>
|
||||
<Field
|
||||
id="notify-email-list"
|
||||
type="text"
|
||||
placeholder="name@gmail.com"
|
||||
value=""
|
||||
onChange={() => console.log("disabled")}
|
||||
/>
|
||||
<Typography mt={theme.gap.small}>
|
||||
You can separate multiple emails with a comma
|
||||
</Typography>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
@@ -261,16 +365,67 @@ const Configure = () => {
|
||||
<Select
|
||||
id="monitor-interval-configure"
|
||||
label="Check frequency"
|
||||
value={monitor?.interval || 1}
|
||||
value={monitor?.interval / MS_PER_MINUTE || 1}
|
||||
onChange={(event) => handleChange(event, "interval")}
|
||||
items={frequencies}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<Button level="primary" label="Save" sx={{ px: theme.gap.ml }} />
|
||||
<Button
|
||||
level="primary"
|
||||
label="Save"
|
||||
sx={{ px: theme.gap.large }}
|
||||
onClick={handleSubmit}
|
||||
/>
|
||||
</Stack>
|
||||
</form>
|
||||
<Modal
|
||||
aria-labelledby="modal-delete-monitor"
|
||||
aria-describedby="delete-monitor-confirmation"
|
||||
open={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
disablePortal
|
||||
>
|
||||
<Stack
|
||||
gap={theme.gap.xs}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: 400,
|
||||
bgcolor: "white",
|
||||
border: "solid 1px #f2f2f2",
|
||||
borderRadius: `${theme.shape.borderRadius}px`,
|
||||
boxShadow: 24,
|
||||
p: "30px",
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography id="modal-delete-monitor" component="h2">
|
||||
Really delete this monitor?
|
||||
</Typography>
|
||||
<Typography id="delete-monitor-confirmation">
|
||||
Once deleted, this monitor cannot be retrieved back.
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.gap.small}
|
||||
mt={theme.gap.large}
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Cancel"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
<Button level="error" label="Delete" onClick={handleRemove} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
.create-monitor p.MuiTypography-root {
|
||||
color: var(--env-var-color-2);
|
||||
}
|
||||
.create-monitor h6.MuiTypography-root {
|
||||
.create-monitor h6.MuiTypography-root,
|
||||
.create-monitor .MuiBox-root .field + p.MuiTypography-root {
|
||||
font-size: var(--env-var-font-size-small-plus);
|
||||
opacity: 0.8;
|
||||
}
|
||||
@@ -48,6 +49,7 @@
|
||||
border-radius: var(--env-var-radius-1);
|
||||
flex-direction: row;
|
||||
gap: var(--env-var-spacing-4);
|
||||
background-color: var(--env-var-color-8);
|
||||
}
|
||||
.create-monitor .config-box .MuiBox-root,
|
||||
.create-monitor .config-box .MuiStack-root {
|
||||
|
||||
@@ -12,6 +12,7 @@ import WestRoundedIcon from "@mui/icons-material/WestRounded";
|
||||
import Field from "../../../Components/Inputs/Field";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import Checkbox from "../../../Components/Inputs/Checkbox";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
|
||||
const CreateMonitor = () => {
|
||||
const MS_PER_MINUTE = 60000;
|
||||
@@ -20,86 +21,88 @@ const CreateMonitor = () => {
|
||||
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: "",
|
||||
notifications: [],
|
||||
interval: 1,
|
||||
});
|
||||
const [https, setHttps] = useState(true);
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
//General Settings Form
|
||||
const [generalSettings, setGeneralSettings] = useState({ url: "", name: "" });
|
||||
//Checks Form
|
||||
const [checks, setChecks] = useState({
|
||||
type: "",
|
||||
// port: ""
|
||||
});
|
||||
//Incidents Form
|
||||
// const [notifications, setNotifications] = useState({
|
||||
// viaSms: false,
|
||||
// viaEmail: false,
|
||||
// viaOther: false,
|
||||
// email: "",
|
||||
// });
|
||||
//Advanced Settings Form
|
||||
const [advancedSettings, setAdvancedSettings] = useState({
|
||||
interval: 1,
|
||||
// retries: "",
|
||||
// codes: "",
|
||||
// redirects: "",
|
||||
});
|
||||
//Proxy Settings Form
|
||||
// const [proxy, setProxy] = useState({
|
||||
// enabled: false,
|
||||
// protocol: "",
|
||||
// address: "",
|
||||
// proxy_port: "",
|
||||
// });
|
||||
const handleChange = (event, name) => {
|
||||
const { value, id } = event.target;
|
||||
if (!name) name = idMap[id];
|
||||
|
||||
const handleChange = (event, id, setState, checkbox) => {
|
||||
const { value } = event.target;
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
[id]: checkbox ? true : value,
|
||||
}));
|
||||
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 validation = monitorValidation.validate(
|
||||
{ [id]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
const { error } = monitorValidation.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
|
||||
if (validation.error) {
|
||||
updatedErrors[id] = validation.error.details[0].message;
|
||||
} else {
|
||||
delete updatedErrors[id];
|
||||
}
|
||||
return updatedErrors;
|
||||
});
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
if (error) updatedErrors[name] = error.details[0].message;
|
||||
else delete updatedErrors[name];
|
||||
return updatedErrors;
|
||||
});
|
||||
}
|
||||
};
|
||||
// const handleCheck = (id, setState) => {
|
||||
// setState((prev) => ({
|
||||
// ...prev,
|
||||
// [id]: !prev[id],
|
||||
// }));
|
||||
// };
|
||||
|
||||
const handleCreateMonitor = async (event) => {
|
||||
event.preventDefault();
|
||||
//obj to submit
|
||||
let monitor = {
|
||||
let form = {
|
||||
url:
|
||||
//preprending protocol for url
|
||||
checks.type === "http" || checks.type === "https"
|
||||
? `${checks.type}://` + generalSettings.url
|
||||
: generalSettings.url,
|
||||
name:
|
||||
generalSettings.name === ""
|
||||
? generalSettings.url
|
||||
: generalSettings.name,
|
||||
//there is no separate monitor type for https since the operations for the two protocols are identical
|
||||
//however the URL does need the correct prepend hence https is still being tracked but overwritten when prepping the monitor obj
|
||||
type: checks.type === "https" ? "http" : checks.type,
|
||||
monitor.type === "http"
|
||||
? `http${https ? "s" : ""}://` + monitor.url
|
||||
: monitor.url,
|
||||
name: monitor.name === "" ? monitor.url : monitor.name,
|
||||
type: monitor.type,
|
||||
interval: monitor.interval * MS_PER_MINUTE,
|
||||
};
|
||||
|
||||
const { error } = monitorValidation.validate(monitor, {
|
||||
const { error } = monitorValidation.validate(form, {
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
@@ -109,29 +112,27 @@ const CreateMonitor = () => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
createToast({ body: "Error validation data." });
|
||||
} else {
|
||||
monitor = {
|
||||
...monitor,
|
||||
description: monitor.name,
|
||||
form = {
|
||||
...form,
|
||||
description: form.name,
|
||||
userId: user._id,
|
||||
// ...advancedSettings
|
||||
interval: advancedSettings.interval * MS_PER_MINUTE,
|
||||
notifications: monitor.notifications,
|
||||
};
|
||||
try {
|
||||
const action = await dispatch(
|
||||
createUptimeMonitor({ authToken, monitor })
|
||||
);
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
navigate("/monitors");
|
||||
}
|
||||
} catch (error) {
|
||||
alert(error);
|
||||
const action = await dispatch(
|
||||
createUptimeMonitor({ authToken, monitor: form })
|
||||
);
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
createToast({ body: "Monitor created successfully!" });
|
||||
navigate("/monitors");
|
||||
} else {
|
||||
createToast({ body: "Failed to create monitor." });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//select values
|
||||
// const ports = ["Port 1", "Port 2", "Port 3"];
|
||||
const frequencies = [
|
||||
{ _id: 1, name: "1 minute" },
|
||||
{ _id: 2, name: "2 minutes" },
|
||||
@@ -145,6 +146,7 @@ const CreateMonitor = () => {
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Back"
|
||||
animate="slideLeft"
|
||||
img={<WestRoundedIcon />}
|
||||
onClick={() => navigate("/monitors")}
|
||||
sx={{
|
||||
@@ -152,7 +154,8 @@ const CreateMonitor = () => {
|
||||
mb: theme.gap.medium,
|
||||
px: theme.gap.ml,
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
pr: theme.gap.small,
|
||||
mr: theme.gap.small,
|
||||
fill: theme.palette.otherColors.slateGray,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
@@ -173,19 +176,13 @@ const CreateMonitor = () => {
|
||||
</Box>
|
||||
<Stack gap={theme.gap.xl}>
|
||||
<Field
|
||||
type={
|
||||
checks.type === "http" || checks.type === "https"
|
||||
? "url"
|
||||
: "text"
|
||||
}
|
||||
type={monitor.type === "http" ? "url" : "text"}
|
||||
id="monitor-url"
|
||||
label="URL to monitor"
|
||||
https={checks.type === "https"}
|
||||
https={https}
|
||||
placeholder="google.com"
|
||||
value={generalSettings.url}
|
||||
onChange={(event) =>
|
||||
handleChange(event, "url", setGeneralSettings)
|
||||
}
|
||||
value={monitor.url}
|
||||
onChange={handleChange}
|
||||
error={errors["url"]}
|
||||
/>
|
||||
<Field
|
||||
@@ -194,10 +191,8 @@ const CreateMonitor = () => {
|
||||
label="Friendly name"
|
||||
isOptional={true}
|
||||
placeholder="Google"
|
||||
value={generalSettings.name}
|
||||
onChange={(event) =>
|
||||
handleChange(event, "name", setGeneralSettings)
|
||||
}
|
||||
value={monitor.name}
|
||||
onChange={handleChange}
|
||||
error={errors["name"]}
|
||||
/>
|
||||
</Stack>
|
||||
@@ -217,33 +212,27 @@ const CreateMonitor = () => {
|
||||
desc="Use HTTP(s) to monitor your website or API endpoint."
|
||||
size="small"
|
||||
value="http"
|
||||
checked={checks.type === "http" || checks.type === "https"}
|
||||
onChange={(event) => handleChange(event, "type", setChecks)}
|
||||
checked={monitor.type === "http"}
|
||||
onChange={(event) => handleChange(event)}
|
||||
/>
|
||||
{checks.type === "http" || checks.type === "https" ? (
|
||||
{monitor.type === "http" ? (
|
||||
<ButtonGroup sx={{ ml: "32px" }}>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="HTTP"
|
||||
onClick={() =>
|
||||
setChecks((prev) => ({ ...prev, type: "http" }))
|
||||
}
|
||||
label="HTTPS"
|
||||
onClick={() => setHttps(true)}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
checks.type === "http" &&
|
||||
theme.palette.otherColors.fillGray,
|
||||
https && theme.palette.otherColors.fillGray,
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="HTTPS"
|
||||
onClick={() =>
|
||||
setChecks((prev) => ({ ...prev, type: "https" }))
|
||||
}
|
||||
label="HTTP"
|
||||
onClick={() => setHttps(false)}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
checks.type === "https" &&
|
||||
theme.palette.otherColors.fillGray,
|
||||
!https && theme.palette.otherColors.fillGray,
|
||||
}}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
@@ -257,35 +246,9 @@ const CreateMonitor = () => {
|
||||
desc="Check whether your server is available or not."
|
||||
size="small"
|
||||
value="ping"
|
||||
checked={checks.type === "ping"}
|
||||
onChange={(event) => handleChange(event, "type", setChecks)}
|
||||
checked={monitor.type === "ping"}
|
||||
onChange={(event) => handleChange(event)}
|
||||
/>
|
||||
{/* TODO */}
|
||||
{/* <RadioButton
|
||||
id="monitor-checks-port"
|
||||
title="Port monitoring"
|
||||
desc="Monitor a specific service on your server."
|
||||
value="port"
|
||||
checked={checks.type === "port"}
|
||||
onChange={(event) => handleChange(event, "type", setChecks)}
|
||||
/>
|
||||
<div className="monitors-dropdown-holder">
|
||||
<Select
|
||||
id="monitor-ports"
|
||||
value={checks.port || "placeholder"}
|
||||
inputProps={{ id: "monitor-ports-select" }}
|
||||
onChange={(event) => handleChange(event, "port", setChecks)}
|
||||
>
|
||||
<MenuItem id="port-placeholder" value="placeholder">
|
||||
Select a port to check
|
||||
</MenuItem>
|
||||
{ports.map((port, index) => (
|
||||
<MenuItem key={`port-${index}`} value={port}>
|
||||
{port}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</div> */}
|
||||
<Box className="error-container">
|
||||
{errors["type"] ? (
|
||||
<Typography component="p" className="input-error">
|
||||
@@ -297,36 +260,62 @@ const CreateMonitor = () => {
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Stack className="config-box">
|
||||
<Box>
|
||||
<Typography component="h2">Incident notifications</Typography>
|
||||
<Typography component="p" mt={theme.gap.small}>
|
||||
When there is an incident, notify users
|
||||
When there is an incident, notify users.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.gap.large}>
|
||||
<Stack gap={theme.gap.medium}>
|
||||
<Typography component="p" mt={theme.gap.small}>
|
||||
When there is a new incident,
|
||||
</Typography>
|
||||
<Checkbox id="notify-sms" label="Notify via SMS (coming soon)" />
|
||||
<Checkbox
|
||||
id="notify-sms"
|
||||
label="Notify via SMS (coming soon)"
|
||||
isChecked={false}
|
||||
value=""
|
||||
onChange={() => console.log("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 the following addresses:"
|
||||
/>
|
||||
<Field
|
||||
id="notify-email-list"
|
||||
type="text"
|
||||
label="You can separate multiple emails with a comma"
|
||||
placeholder="name@gmail.com"
|
||||
label="Also notify via email to multiple addresses (coming soon)"
|
||||
isChecked={false}
|
||||
value=""
|
||||
onChange={() => console.log("disabled")}
|
||||
isDisabled={true}
|
||||
/>
|
||||
{monitor.notifications.some(
|
||||
(notification) => notification.type === "emails"
|
||||
) ? (
|
||||
<Box mx={`calc(${theme.gap.ml} * 2)`}>
|
||||
<Field
|
||||
id="notify-email-list"
|
||||
type="text"
|
||||
placeholder="name@gmail.com"
|
||||
value=""
|
||||
onChange={() => console.log("disabled")}
|
||||
/>
|
||||
<Typography mt={theme.gap.small}>
|
||||
You can separate multiple emails with a comma
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Stack className="config-box">
|
||||
<Box>
|
||||
<Typography component="h2">Advanced settings</Typography>
|
||||
@@ -335,10 +324,8 @@ const CreateMonitor = () => {
|
||||
<Select
|
||||
id="monitor-interval"
|
||||
label="Check frequency"
|
||||
value={advancedSettings.interval || 1}
|
||||
onChange={(event) =>
|
||||
handleChange(event, "interval", setAdvancedSettings)
|
||||
}
|
||||
value={monitor.interval || 1}
|
||||
onChange={(event) => handleChange(event, "interval")}
|
||||
items={frequencies}
|
||||
/>
|
||||
{/* TODO */}
|
||||
|
||||
@@ -37,13 +37,18 @@
|
||||
.monitor-details .stat-box {
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
border: solid 1px var(--env-var-color-4);
|
||||
border: solid 1px;
|
||||
border-radius: var(--env-var-radius-1);
|
||||
padding: var(--env-var-spacing-1-plus) var(--env-var-spacing-2);
|
||||
padding: var(--env-var-spacing-1) 18px;
|
||||
background-color: var(--env-var-color-8);
|
||||
}
|
||||
.monitor-details .stat-box:not(:first-of-type) {
|
||||
margin-left: var(--env-var-spacing-2);
|
||||
}
|
||||
.monitor-details .stat-box h6.MuiTypography-root {
|
||||
margin-bottom: var(--env-var-spacing-1);
|
||||
}
|
||||
}
|
||||
.monitor-details button.MuiButtonBase-root,
|
||||
.monitor-details .stat-box {
|
||||
border-color: var(--env-var-color-16);
|
||||
}
|
||||
@@ -147,6 +147,7 @@ const DetailsPage = () => {
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Back to Monitors"
|
||||
animate="slideLeft"
|
||||
img={<WestRoundedIcon />}
|
||||
onClick={() => navigate("/monitors")}
|
||||
sx={{
|
||||
@@ -154,7 +155,8 @@ const DetailsPage = () => {
|
||||
mb: theme.gap.medium,
|
||||
px: theme.gap.ml,
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
pr: theme.gap.small,
|
||||
mr: theme.gap.small,
|
||||
fill: theme.palette.otherColors.slateGray,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
@@ -184,15 +186,16 @@ const DetailsPage = () => {
|
||||
<Button
|
||||
level="tertiary"
|
||||
label="Configure"
|
||||
animate="rotate90"
|
||||
img={<SettingsIcon />}
|
||||
onClick={() => navigate(`/monitors/configure/${monitorId}`)}
|
||||
sx={{
|
||||
ml: "auto",
|
||||
alignSelf: "flex-end",
|
||||
backgroundColor: "#f4f4f4",
|
||||
px: theme.gap.ml,
|
||||
px: theme.gap.medium,
|
||||
"& svg": {
|
||||
pr: theme.gap.xs,
|
||||
mr: "6px",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
@@ -222,19 +225,28 @@ const DetailsPage = () => {
|
||||
level="secondary"
|
||||
label="Day"
|
||||
onClick={() => setFilter("day")}
|
||||
sx={{ backgroundColor: filter === "day" && "#f4f4f4" }}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "day" && theme.palette.otherColors.fillGray,
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Week"
|
||||
onClick={() => setFilter("week")}
|
||||
sx={{ backgroundColor: filter === "week" && "#f4f4f4" }}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "week" && theme.palette.otherColors.fillGray,
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
level="secondary"
|
||||
label="Month"
|
||||
onClick={() => setFilter("month")}
|
||||
sx={{ backgroundColor: filter === "month" && "#f4f4f4" }}
|
||||
sx={{
|
||||
backgroundColor:
|
||||
filter === "month" && theme.palette.otherColors.fillGray,
|
||||
}}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
|
||||
@@ -111,7 +111,12 @@ const Monitors = () => {
|
||||
),
|
||||
},
|
||||
{ id: idx + 2, data: <ResponseTimeChart checks={reversedChecks} /> },
|
||||
{ id: idx + 3, data: monitor.type },
|
||||
{
|
||||
id: idx + 3,
|
||||
data: (
|
||||
<span style={{ textTransform: "uppercase" }}>{monitor.type}</span>
|
||||
),
|
||||
},
|
||||
{ id: idx + 4, data: "TODO" },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
padding-bottom: 60px;
|
||||
border: 1px solid var(--env-var-color-16);
|
||||
border-radius: var(--env-var-radius-1);
|
||||
background-color: var(--env-var-color-8);
|
||||
}
|
||||
.settings .config-box .MuiBox-root,
|
||||
.settings .config-box .MuiStack-root {
|
||||
@@ -39,4 +40,4 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--env-var-spacing-4);
|
||||
}
|
||||
}
|
||||
@@ -110,7 +110,7 @@ const Settings = () => {
|
||||
</Box>
|
||||
</Stack>
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<Button level="primary" label="Save" />
|
||||
<Button level="primary" label="Save" sx={{ px: theme.gap.large }} />
|
||||
</Stack>
|
||||
</form>
|
||||
</Box>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 -28.5 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||
<g>
|
||||
<path d="M216.856339,16.5966031 C200.285002,8.84328665 182.566144,3.2084988 164.041564,0 C161.766523,4.11318106 159.108624,9.64549908 157.276099,14.0464379 C137.583995,11.0849896 118.072967,11.0849896 98.7430163,14.0464379 C96.9108417,9.64549908 94.1925838,4.11318106 91.8971895,0 C73.3526068,3.2084988 55.6133949,8.86399117 39.0420583,16.6376612 C5.61752293,67.146514 -3.4433191,116.400813 1.08711069,164.955721 C23.2560196,181.510915 44.7403634,191.567697 65.8621325,198.148576 C71.0772151,190.971126 75.7283628,183.341335 79.7352139,175.300261 C72.104019,172.400575 64.7949724,168.822202 57.8887866,164.667963 C59.7209612,163.310589 61.5131304,161.891452 63.2445898,160.431257 C105.36741,180.133187 151.134928,180.133187 192.754523,160.431257 C194.506336,161.891452 196.298154,163.310589 198.110326,164.667963 C191.183787,168.842556 183.854737,172.420929 176.223542,175.320965 C180.230393,183.341335 184.861538,190.991831 190.096624,198.16893 C211.238746,191.588051 232.743023,181.531619 254.911949,164.955721 C260.227747,108.668201 245.831087,59.8662432 216.856339,16.5966031 Z M85.4738752,135.09489 C72.8290281,135.09489 62.4592217,123.290155 62.4592217,108.914901 C62.4592217,94.5396472 72.607595,82.7145587 85.4738752,82.7145587 C98.3405064,82.7145587 108.709962,94.5189427 108.488529,108.914901 C108.508531,123.290155 98.3405064,135.09489 85.4738752,135.09489 Z M170.525237,135.09489 C157.88039,135.09489 147.510584,123.290155 147.510584,108.914901 C147.510584,94.5396472 157.658606,82.7145587 170.525237,82.7145587 C183.391518,82.7145587 193.761324,94.5189427 193.539891,108.914901 C193.539891,123.290155 183.391518,135.09489 170.525237,135.09489 Z" fill="#5865F2" fill-rule="nonzero">
|
||||
␍</path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
␍<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
|
||||
␍<g fill-rule="evenodd" clip-rule="evenodd">
|
||||
␍<path fill="#E01E5A" d="M2.471 11.318a1.474 1.474 0 001.47-1.471v-1.47h-1.47A1.474 1.474 0 001 9.846c.001.811.659 1.469 1.47 1.47zm3.682-2.942a1.474 1.474 0 00-1.47 1.471v3.683c.002.811.66 1.468 1.47 1.47a1.474 1.474 0 001.47-1.47V9.846a1.474 1.474 0 00-1.47-1.47z"/>
|
||||
␍<path fill="#36C5F0" d="M4.683 2.471c.001.811.659 1.469 1.47 1.47h1.47v-1.47A1.474 1.474 0 006.154 1a1.474 1.474 0 00-1.47 1.47zm2.94 3.682a1.474 1.474 0 00-1.47-1.47H2.47A1.474 1.474 0 001 6.153c.002.812.66 1.469 1.47 1.47h3.684a1.474 1.474 0 001.47-1.47z"/>
|
||||
␍<path fill="#2EB67D" d="M9.847 7.624a1.474 1.474 0 001.47-1.47V2.47A1.474 1.474 0 009.848 1a1.474 1.474 0 00-1.47 1.47v3.684c.002.81.659 1.468 1.47 1.47zm3.682-2.941a1.474 1.474 0 00-1.47 1.47v1.47h1.47A1.474 1.474 0 0015 6.154a1.474 1.474 0 00-1.47-1.47z"/>
|
||||
␍<path fill="#ECB22E" d="M8.377 9.847c.002.811.659 1.469 1.47 1.47h3.683A1.474 1.474 0 0015 9.848a1.474 1.474 0 00-1.47-1.47H9.847a1.474 1.474 0 00-1.47 1.47zm2.94 3.682a1.474 1.474 0 00-1.47-1.47h-1.47v1.47c.002.812.659 1.469 1.47 1.47a1.474 1.474 0 001.47-1.47z"/>
|
||||
␍</g>
|
||||
␍</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g>
|
||||
<path d="M128.080089,-0.000183105 C135.311053,0.0131003068 142.422517,0.624138494 149.335663,1.77979593 L149.335663,1.77979593 L149.335663,76.2997796 L202.166953,23.6044907 C208.002065,27.7488446 213.460883,32.3582023 218.507811,37.3926715 C223.557281,42.4271407 228.192318,47.8867213 232.346817,53.7047992 L232.346817,53.7047992 L179.512985,106.400063 L254.227854,106.400063 C255.387249,113.29414 256,120.36111 256,127.587243 L256,127.587243 L256,127.759881 C256,134.986013 255.387249,142.066204 254.227854,148.960282 L254.227854,148.960282 L179.500273,148.960282 L232.346817,201.642324 C228.192318,207.460402 223.557281,212.919983 218.523066,217.954452 L218.523066,217.954452 L218.507811,217.954452 C213.460883,222.988921 208.002065,227.6115 202.182208,231.742607 L202.182208,231.742607 L149.335663,179.04709 L149.335663,253.5672 C142.435229,254.723036 135.323765,255.333244 128.092802,255.348499 L128.092802,255.348499 L127.907197,255.348499 C120.673691,255.333244 113.590195,254.723036 106.677048,253.5672 L106.677048,253.5672 L106.677048,179.04709 L53.8457596,231.742607 C42.1780766,223.466917 31.977435,213.278734 23.6658953,201.642324 L23.6658953,201.642324 L76.4997269,148.960282 L1.78485803,148.960282 C0.612750404,142.052729 0,134.946095 0,127.719963 L0,127.719963 L0,127.349037 C0.0121454869,125.473817 0.134939797,123.182933 0.311311815,120.812834 L0.36577283,120.099764 C0.887996182,113.428547 1.78485803,106.400063 1.78485803,106.400063 L1.78485803,106.400063 L76.4997269,106.400063 L23.6658953,53.7047992 C27.8076812,47.8867213 32.4300059,42.4403618 37.4769335,37.4193681 L37.4769335,37.4193681 L37.5023588,37.3926715 C42.5391163,32.3582023 48.0106469,27.7488446 53.8457596,23.6044907 L53.8457596,23.6044907 L106.677048,76.2997796 L106.677048,1.77979593 C113.590195,0.624138494 120.688946,0.0131003068 127.932622,-0.000183105 L127.932622,-0.000183105 L128.080089,-0.000183105 Z M128.067377,95.7600714 L127.945335,95.7600714 C118.436262,95.7600714 109.32891,97.5001809 100.910584,100.661566 C97.7553011,109.043534 96.0085811,118.129275 95.9958684,127.613685 L95.9958684,127.733184 C96.0085811,137.217594 97.7553011,146.303589 100.923296,154.685303 C109.32891,157.846943 118.436262,159.587052 127.945335,159.587052 L128.067377,159.587052 C137.576449,159.587052 146.683802,157.846943 155.089415,154.685303 C158.257411,146.290368 160.004131,137.217594 160.004131,127.733184 L160.004131,127.613685 C160.004131,118.129275 158.257411,109.043534 155.089415,100.661566 C146.683802,97.5001809 137.576449,95.7600714 128.067377,95.7600714 Z" fill="#FF4A00" fill-rule="nonzero">
|
||||
␍</path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -27,7 +27,8 @@ const createAlert = async (req, res, next) => {
|
||||
await createAlertBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.message = error.details[0].message;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
return;
|
||||
@@ -58,7 +59,8 @@ const getAlertsByUserId = async (req, res, next) => {
|
||||
await getAlertsByUserIdParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.message = error.details[0].message;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
return;
|
||||
@@ -82,7 +84,8 @@ const getAlertsByMonitorId = async (req, res, next) => {
|
||||
await getAlertsByMonitorIdParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.message = error.details[0].message;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
return;
|
||||
@@ -106,7 +109,8 @@ const getAlertById = async (req, res, next) => {
|
||||
await getAlertByIdParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.message = error.details[0].message;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
return;
|
||||
@@ -132,7 +136,8 @@ const editAlert = async (req, res, next) => {
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
error.status = 422;
|
||||
error.message = error.details[0].message;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
return;
|
||||
@@ -156,7 +161,8 @@ const deleteAlert = async (req, res, next) => {
|
||||
await deleteAlertParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.message = error.details[0].message;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
return;
|
||||
|
||||
@@ -54,7 +54,8 @@ const registerController = async (req, res, next) => {
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message = error.details[0].message;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
@@ -115,14 +116,20 @@ const registerController = async (req, res, next) => {
|
||||
*/
|
||||
const loginController = async (req, res, next) => {
|
||||
try {
|
||||
// Validate input
|
||||
await loginValidation.validateAsync(req.body);
|
||||
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Check if user exists
|
||||
const user = await req.db.getUserByEmail(req, res);
|
||||
|
||||
// Compare password
|
||||
|
||||
const match = await user.comparePassword(req.body.password);
|
||||
if (match !== true) {
|
||||
throw new Error(errorMessages.AUTH_INCORRECT_PASSWORD);
|
||||
@@ -157,7 +164,8 @@ const userEditController = async (req, res, next) => {
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message = error.details[0].message;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
@@ -216,8 +224,18 @@ const inviteController = async (req, res, next) => {
|
||||
// Only admins can invite
|
||||
const token = getTokenFromHeaders(req.headers);
|
||||
const { role, firstname } = jwt.decode(token);
|
||||
await inviteRoleValidation.validateAsync({ roles: role });
|
||||
await inviteBodyValidation.validateAsync(req.body);
|
||||
try {
|
||||
await inviteRoleValidation.validateAsync({ roles: role });
|
||||
await inviteBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const inviteToken = await req.db.requestInviteToken(req, res);
|
||||
req.emailService.buildAndSendEmail(
|
||||
"employeeActivationTemplate",
|
||||
@@ -242,6 +260,16 @@ const inviteController = async (req, res, next) => {
|
||||
const inviteVerifyController = async (req, res, next) => {
|
||||
try {
|
||||
await inviteVerifciationBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const invite = await req.db.getInviteToken(req, res);
|
||||
|
||||
res
|
||||
@@ -289,6 +317,16 @@ const checkAdminController = async (req, res) => {
|
||||
const recoveryRequestController = async (req, res, next) => {
|
||||
try {
|
||||
await recoveryValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await req.db.getUserByEmail(req, res);
|
||||
if (user) {
|
||||
const recoveryToken = await req.db.requestRecoveryToken(req, res);
|
||||
@@ -328,6 +366,16 @@ const recoveryRequestController = async (req, res, next) => {
|
||||
const validateRecoveryTokenController = async (req, res, next) => {
|
||||
try {
|
||||
await recoveryTokenValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await req.db.validateRecoveryToken(req, res);
|
||||
// TODO Redirect user to reset password after validating token
|
||||
return res.status(200).json({
|
||||
@@ -354,6 +402,15 @@ const validateRecoveryTokenController = async (req, res, next) => {
|
||||
const resetPasswordController = async (req, res, next) => {
|
||||
try {
|
||||
await newPasswordValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const user = await req.db.resetPassword(req, res);
|
||||
const token = issueToken(user._doc);
|
||||
res.status(200).json({
|
||||
@@ -392,9 +449,18 @@ const deleteUserController = async (req, res, next) => {
|
||||
};
|
||||
|
||||
// Validate user
|
||||
await deleteUserParamValidation.validateAsync(
|
||||
decodedTokenCastedAsRequest.body
|
||||
);
|
||||
try {
|
||||
await deleteUserParamValidation.validateAsync(
|
||||
decodedTokenCastedAsRequest.body
|
||||
);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the user exists
|
||||
const user = await req.db.getUserByEmail(decodedTokenCastedAsRequest);
|
||||
@@ -408,25 +474,20 @@ const deleteUserController = async (req, res, next) => {
|
||||
);
|
||||
|
||||
if (monitors) {
|
||||
// 2. Delete jobs associated with each monitor
|
||||
for (const monitor of monitors) {
|
||||
await req.jobQueue.deleteJob(monitor);
|
||||
}
|
||||
//2. Remove all jobs, delete checks and alerts
|
||||
await Promise.all(
|
||||
monitors.map(async (monitor) => {
|
||||
await req.jobQueue.deleteJob(monitor);
|
||||
await req.db.deleteChecks(monitor._id);
|
||||
await req.db.deleteAlertByMonitorId(monitor._id);
|
||||
await req.db.deleteNotificationsByMonitorId(monitor._id);
|
||||
})
|
||||
);
|
||||
|
||||
// 3. Delete all checks associated with each monitor
|
||||
for (const monitor of monitors) {
|
||||
await req.db.deleteChecks(monitor._id);
|
||||
}
|
||||
|
||||
// 4. Delete all alerts associated with each monitor
|
||||
for (const monitor of monitors) {
|
||||
await req.db.deleteAlertByMonitorId(monitor._id);
|
||||
}
|
||||
|
||||
// 5. Delete each monitor
|
||||
// 3. Delete each monitor
|
||||
await req.db.deleteMonitorsByUserId(user._id);
|
||||
|
||||
// 6. Delete the user by id
|
||||
// 4. Delete the user by id
|
||||
await req.db.deleteUser(decodedTokenCastedAsRequest);
|
||||
|
||||
return res.status(200).json({
|
||||
|
||||
@@ -14,7 +14,8 @@ const createCheck = async (req, res, next) => {
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message = error.details[0].message;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
@@ -37,7 +38,8 @@ const getChecks = async (req, res, next) => {
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message = error.details[0].message;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
@@ -59,20 +61,19 @@ const deleteChecks = async (req, res, next) => {
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message = error.details[0].message;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const deletedCount = await req.db.deleteChecks(req.params.monitorId);
|
||||
return res
|
||||
.status(200)
|
||||
.json({
|
||||
success: true,
|
||||
msg: successMessages.CHECK_DELETE,
|
||||
data: { deletedCount },
|
||||
});
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.CHECK_DELETE,
|
||||
data: { deletedCount },
|
||||
});
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
|
||||
@@ -15,7 +15,8 @@ const createMaintenanceWindow = async (req, res, next) => {
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message = error.details[0].message;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
@@ -48,7 +49,15 @@ const getMaintenanceWindowsByUserId = async (req, res, next) => {
|
||||
await getMaintenanceWindowsByUserIdParamValidation.validateAsync(
|
||||
req.params
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const maintenanceWindows = await req.db.getMaintenanceWindowsByUserId(
|
||||
req.params.userId
|
||||
);
|
||||
@@ -69,7 +78,16 @@ const getMaintenanceWindowsByMonitorId = async (req, res, next) => {
|
||||
await getMaintenanceWindowsByMonitorIdParamValidation.validateAsync(
|
||||
req.params
|
||||
);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const maintenanceWindows = await req.db.getMaintenanceWindowsByMonitorId(
|
||||
req.params.monitorId
|
||||
);
|
||||
|
||||
@@ -2,14 +2,14 @@ const {
|
||||
getMonitorByIdParamValidation,
|
||||
getMonitorByIdQueryValidation,
|
||||
getMonitorsByUserIdValidation,
|
||||
monitorValidation,
|
||||
createMonitorBodyValidation,
|
||||
editMonitorBodyValidation,
|
||||
getMonitorsByUserIdQueryValidation,
|
||||
} = require("../validation/joi");
|
||||
|
||||
const SERVICE_NAME = "monitorController";
|
||||
const { errorMessages, successMessages } = require("../utils/messages");
|
||||
const { error } = require("winston");
|
||||
const { runInNewContext } = require("vm");
|
||||
|
||||
/**
|
||||
* Returns all monitors
|
||||
@@ -47,7 +47,8 @@ const getMonitorById = async (req, res, next) => {
|
||||
await getMonitorByIdQueryValidation.validateAsync(req.query);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.message = error.details[0].message;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
@@ -83,6 +84,16 @@ const getMonitorsByUserId = async (req, res, next) => {
|
||||
try {
|
||||
await getMonitorsByUserIdValidation.validateAsync(req.params);
|
||||
await getMonitorsByUserIdQueryValidation.validateAsync(req.query);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const userId = req.params.userId;
|
||||
const monitors = await req.db.getMonitorsByUserId(req, res);
|
||||
|
||||
@@ -108,17 +119,29 @@ const getMonitorsByUserId = async (req, res, next) => {
|
||||
|
||||
const createMonitor = async (req, res, next) => {
|
||||
try {
|
||||
await monitorValidation.validateAsync(req.body);
|
||||
await createMonitorBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message = error.details[0].message;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const notifications = req.body.notifications;
|
||||
const monitor = await req.db.createMonitor(req, res);
|
||||
|
||||
if (notifications && notifications.length !== 0) {
|
||||
await Promise.all(
|
||||
notifications.map(async (notification) => {
|
||||
notification.monitorId = monitor._id;
|
||||
await req.db.createNotification(notification);
|
||||
})
|
||||
);
|
||||
}
|
||||
// Add monitor to job queue
|
||||
req.jobQueue.addJob(monitor._id, monitor);
|
||||
return res.status(201).json({
|
||||
@@ -143,21 +166,24 @@ const createMonitor = async (req, res, next) => {
|
||||
|
||||
const deleteMonitor = async (req, res, next) => {
|
||||
try {
|
||||
await getMonitorByIdValidation.validateAsync(req.params);
|
||||
await getMonitorByIdParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message = error.details[0].message;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const monitor = await req.db.deleteMonitor(req, res, next);
|
||||
// Delete associated checks and alerts
|
||||
// Delete associated checks,alerts,and notifications
|
||||
await req.jobQueue.deleteJob(monitor);
|
||||
await req.db.deleteChecks(monitor._id);
|
||||
await req.db.deleteAlertByMonitorId(monitor._id);
|
||||
await req.db.deleteNotificationsByMonitorId(monitor._id);
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* We should remove all checks and alerts associated with this monitor
|
||||
@@ -195,20 +221,38 @@ const deleteAllMonitors = async (req, res) => {
|
||||
*/
|
||||
const editMonitor = async (req, res, next) => {
|
||||
try {
|
||||
await getMonitorByIdValidation.validateAsync(req.params);
|
||||
await getMonitorByIdParamValidation.validateAsync(req.params);
|
||||
await editMonitorBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message = error.details[0].message;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const monitorBeforeEdit = await req.db.getMonitorById(req, res);
|
||||
|
||||
// Get notifications from the request body
|
||||
const notifications = req.body.notifications;
|
||||
|
||||
const editedMonitor = await req.db.editMonitor(req, res);
|
||||
|
||||
await req.db.deleteNotificationsByMonitorId(editedMonitor._id);
|
||||
|
||||
if (notifications && notifications.length !== 0) {
|
||||
await Promise.all(
|
||||
notifications.map(async (notification) => {
|
||||
notification.monitorId = editedMonitor._id;
|
||||
await req.db.createNotification(notification);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Delete the old job(editedMonitor has the same ID as the old monitor)
|
||||
await req.jobQueue.deleteJob(editedMonitor);
|
||||
await req.jobQueue.deleteJob(monitorBeforeEdit);
|
||||
// Add the new job back to the queue
|
||||
await req.jobQueue.addJob(editedMonitor._id, editedMonitor);
|
||||
return res.status(200).json({
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
const PageSpeedCheck = require("../models/PageSpeedCheck");
|
||||
const PageSpeedService = require("../service/pageSpeedService");
|
||||
const { successMessages } = require("../utils/messages");
|
||||
const pageSpeedService = new PageSpeedService();
|
||||
@@ -22,12 +21,19 @@ const getPageSpeedChecks = async (req, res, next) => {
|
||||
try {
|
||||
// Validate monitorId parameter
|
||||
await getPageSpeedCheckParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
return res.status(200).json({ msg: "Hit getPageSpeedChecks" });
|
||||
} catch (error) {
|
||||
if (error.isJoi) {
|
||||
return res.status(400).json({ error: error.details[0].message });
|
||||
}
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
@@ -42,12 +48,18 @@ const getPageSpeedChecks = async (req, res, next) => {
|
||||
*/
|
||||
const createPageSpeedCheck = async (req, res, next) => {
|
||||
try {
|
||||
// Validate monitorId parameter
|
||||
await createPageSpeedCheckParamValidation.validateAsync(req.params);
|
||||
|
||||
// Validate request body
|
||||
await createPageSpeedCheckBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { monitorId } = req.params;
|
||||
const { url } = req.body;
|
||||
|
||||
@@ -74,9 +86,6 @@ const createPageSpeedCheck = async (req, res, next) => {
|
||||
data: newPageSpeedCheck,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.isJoi) {
|
||||
return res.status(400).json({ error: error.details[0].message });
|
||||
}
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
}
|
||||
@@ -94,12 +103,17 @@ const deletePageSpeedCheck = async (req, res, next) => {
|
||||
try {
|
||||
// Validate monitorId parameter
|
||||
await deletePageSpeedCheckParamValidation.validateAsync(req.params);
|
||||
|
||||
} catch (error) {
|
||||
error.status = 422;
|
||||
error.service = SERVICE_NAME;
|
||||
error.message =
|
||||
error.details?.[0]?.message || error.message || "Validation Error";
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
return res.status(200).json({ msg: "Hit deletePageSpeedCheck" });
|
||||
} catch (error) {
|
||||
if (error.isJoi) {
|
||||
return res.status(400).json({ error: error.details[0].message });
|
||||
}
|
||||
error.service = SERVICE_NAME;
|
||||
next(error);
|
||||
}
|
||||
|
||||
@@ -65,8 +65,6 @@ const {
|
||||
getAllMonitors,
|
||||
getMonitorById,
|
||||
getMonitorsByUserId,
|
||||
getMonitorByIdForIncidents,
|
||||
getMonitorsByUserIdForIncidents,
|
||||
createMonitor,
|
||||
deleteMonitor,
|
||||
deleteAllMonitors,
|
||||
@@ -120,6 +118,12 @@ const {
|
||||
deleteMaintenanceWindowByUserId,
|
||||
} = require("./modules/maintenaceWindowModule");
|
||||
|
||||
const {
|
||||
createNotification,
|
||||
getNotificationsByMonitorId,
|
||||
deleteNotificationsByMonitorId,
|
||||
} = require("./modules/notificationModule");
|
||||
|
||||
module.exports = {
|
||||
connect,
|
||||
insertUser,
|
||||
@@ -136,8 +140,6 @@ module.exports = {
|
||||
getAllMonitors,
|
||||
getMonitorById,
|
||||
getMonitorsByUserId,
|
||||
getMonitorByIdForIncidents,
|
||||
getMonitorsByUserIdForIncidents,
|
||||
createMonitor,
|
||||
deleteMonitor,
|
||||
deleteAllMonitors,
|
||||
@@ -162,4 +164,7 @@ module.exports = {
|
||||
deleteMaintenaceWindowById,
|
||||
deleteMaintenanceWindowByMonitorId,
|
||||
deleteMaintenanceWindowByUserId,
|
||||
createNotification,
|
||||
getNotificationsByMonitorId,
|
||||
deleteNotificationsByMonitorId,
|
||||
};
|
||||
@@ -2,6 +2,7 @@ const Monitor = require("../../../models/Monitor");
|
||||
const Check = require("../../../models/Check");
|
||||
const PageSpeedCheck = require("../../../models/PageSpeedCheck");
|
||||
const { errorMessages } = require("../../../utils/messages");
|
||||
const Notification = require("../../../models/Notification");
|
||||
|
||||
/**
|
||||
* Get all monitors
|
||||
@@ -30,7 +31,6 @@ const getAllMonitors = async (req, res) => {
|
||||
*/
|
||||
const getMonitorById = async (req, res) => {
|
||||
try {
|
||||
|
||||
const { monitorId } = req.params;
|
||||
let { status, limit, sortOrder } = req.query;
|
||||
|
||||
@@ -64,7 +64,8 @@ const getMonitorById = async (req, res) => {
|
||||
createdAt: sortOrder,
|
||||
})
|
||||
.limit(limit);
|
||||
const monitorWithChecks = { ...monitor.toObject(), checks };
|
||||
const notifications = await Notification.find({ monitorId: monitor._id });
|
||||
const monitorWithChecks = { ...monitor.toObject(), checks, notifications };
|
||||
return monitorWithChecks;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -120,7 +121,12 @@ const getMonitorsByUserId = async (req, res) => {
|
||||
createdAt: sortOrder,
|
||||
})
|
||||
.limit(limit);
|
||||
return { ...monitor.toObject(), checks };
|
||||
|
||||
// Get notifications
|
||||
const notifications = await Notification.find({
|
||||
monitorId: monitor._id,
|
||||
});
|
||||
return { ...monitor.toObject(), checks, notifications };
|
||||
})
|
||||
);
|
||||
return monitorsWithChecks;
|
||||
@@ -140,6 +146,8 @@ const getMonitorsByUserId = async (req, res) => {
|
||||
const createMonitor = async (req, res) => {
|
||||
try {
|
||||
const monitor = new Monitor({ ...req.body });
|
||||
// Remove notifications fom monitor as they aren't needed here
|
||||
monitor.notifications = undefined;
|
||||
monitor.userId = req.user._id;
|
||||
await monitor.save();
|
||||
return monitor;
|
||||
@@ -208,6 +216,8 @@ const deleteMonitorsByUserId = async (userId) => {
|
||||
const editMonitor = async (req, res) => {
|
||||
const candidateId = req.params.monitorId;
|
||||
const candidateMonitor = req.body;
|
||||
candidateMonitor.notifications = undefined;
|
||||
|
||||
try {
|
||||
const editedMonitor = await Monitor.findByIdAndUpdate(
|
||||
candidateId,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
const Notification = require("../../../models/Notification");
|
||||
|
||||
/**
|
||||
* Creates a new notification.
|
||||
* @param {Object} notificationData - The data for the new notification.
|
||||
* @param {mongoose.Types.ObjectId} notificationData.monitorId - The ID of the monitor.
|
||||
* @param {string} notificationData.type - The type of the notification (e.g., "email", "sms").
|
||||
* @param {string} [notificationData.address] - The address for the notification (if applicable).
|
||||
* @param {string} [notificationData.phone] - The phone number for the notification (if applicable).
|
||||
* @returns {Promise<Object>} The created notification.
|
||||
* @throws Will throw an error if the notification cannot be created.
|
||||
*/
|
||||
const createNotification = async (notificationData) => {
|
||||
try {
|
||||
const notification = await new Notification({ ...notificationData }).save();
|
||||
return notification;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves notifications by monitor ID.
|
||||
* @param {mongoose.Types.ObjectId} monitorId - The ID of the monitor.
|
||||
* @returns {Promise<Array<Object>>} An array of notifications.
|
||||
* @throws Will throw an error if the notifications cannot be retrieved.
|
||||
*/
|
||||
const getNotificationsByMonitorId = async (monitorId) => {
|
||||
try {
|
||||
const notifications = await Notification.find({ monitorId });
|
||||
return notifications;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteNotificationsByMonitorId = async (monitorId) => {
|
||||
try {
|
||||
const result = await Notification.deleteMany({ monitorId });
|
||||
return result.deletedCount;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createNotification,
|
||||
getNotificationsByMonitorId,
|
||||
deleteNotificationsByMonitorId,
|
||||
};
|
||||
@@ -231,7 +231,7 @@ class JobQueue {
|
||||
*/
|
||||
async deleteJob(monitor) {
|
||||
try {
|
||||
const deleted = await this.queue.removeRepeatable(monitor.id, {
|
||||
const deleted = await this.queue.removeRepeatable(monitor._id, {
|
||||
every: monitor.interval,
|
||||
});
|
||||
if (deleted) {
|
||||
|
||||
@@ -144,7 +144,7 @@ const getMonitorsByUserIdQueryValidation = joi.object({
|
||||
),
|
||||
});
|
||||
|
||||
const monitorValidation = joi.object({
|
||||
const createMonitorBodyValidation = joi.object({
|
||||
_id: joi.string(),
|
||||
userId: joi.string().required(),
|
||||
name: joi.string().required(),
|
||||
@@ -153,12 +153,14 @@ const monitorValidation = joi.object({
|
||||
url: joi.string().required(),
|
||||
isActive: joi.boolean(),
|
||||
interval: joi.number(),
|
||||
notifications: joi.array().items(joi.object()),
|
||||
});
|
||||
|
||||
const editMonitorBodyValidation = joi.object({
|
||||
name: joi.string(),
|
||||
description: joi.string(),
|
||||
interval: joi.number(),
|
||||
notifications: joi.array().items(joi.object()),
|
||||
});
|
||||
|
||||
//****************************************
|
||||
@@ -286,7 +288,7 @@ module.exports = {
|
||||
inviteRoleValidation,
|
||||
inviteBodyValidation,
|
||||
inviteVerifciationBodyValidation,
|
||||
monitorValidation,
|
||||
createMonitorBodyValidation,
|
||||
getMonitorByIdParamValidation,
|
||||
getMonitorByIdQueryValidation,
|
||||
getMonitorsByUserIdValidation,
|
||||
|
||||
Reference in new issue
Block a user