Merge pull request #518 from bluewave-labs/develop

Merge to Master
This commit is contained in:
Alexander Holliday authored and GitHub committed 2024-08-01 10:38:29 -07:00
commit c9651c04f0
32 files changed
+930 -350

No files matched your search

+2 -2
View File
@@ -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
+33
View File
@@ -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);
}
+19 -2
View File
@@ -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,
}}
>
+1 -1
View File
@@ -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";
});
},
});
+3
View File
@@ -31,6 +31,9 @@
.account .MuiButtonGroup-root button {
border-color: var(--env-var-color-16);
}
.account button{
height: 34px;
}
.account [class$="-form"] {
width: inherit;
}
+3 -1
View File
@@ -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;
+30 -15
View File
@@ -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;
}
}
+236 -81
View File
@@ -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 {
+142 -155
View File
@@ -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 */}
+8 -3
View File
@@ -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);
}
+18 -6
View File
@@ -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>
+6 -1
View File
@@ -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" },
],
};
+2 -1
View File
@@ -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);
}
}
+1 -1
View File
@@ -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>
+8
View File
@@ -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

+10
View File
@@ -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

+8
View File
@@ -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

+12 -6
View File
@@ -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;
+87 -26
View File
@@ -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({
+11 -10
View File
@@ -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
);
+55 -11
View File
@@ -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({
+28 -14
View File
@@ -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);
}
+9 -4
View File
@@ -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,
};
+13 -3
View File
@@ -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,
};
+1 -1
View File
@@ -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) {
+4 -2
View File
@@ -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,