add notificaitons channel to Create Uptime

This commit is contained in:
Alex Holliday
2025-06-09 12:55:45 +08:00
parent 358381ddc2
commit e36abfa9a6
+112 -190
View File
@@ -1,15 +1,12 @@
// React, Redux, Router
import { useTheme } from "@emotion/react";
import { useNavigate, useParams } from "react-router-dom";
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { useTranslation } from "react-i18next";
// Utility and Network
import { checkEndpointResolution } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import { monitorValidation } from "../../../Validation/validation";
import { getUptimeMonitorById } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import { createUptimeMonitor } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
// MUI
import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
@@ -22,11 +19,37 @@ import TextInput from "../../../Components/Inputs/TextInput";
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
import { createToast } from "../../../Utils/toastUtils";
import Radio from "../../../Components/Inputs/Radio";
import Checkbox from "../../../Components/Inputs/Checkbox";
import Select from "../../../Components/Inputs/Select";
import ConfigBox from "../../../Components/ConfigBox";
import NotificationIntegrationModal from "../../../Components/NotificationIntegrationModal/Components/NotificationIntegrationModal";
import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications";
import NotificationsConfig from "../../../Components/NotificationConfig";
const CreateMonitor = () => {
// Redux state
const { user } = useSelector((state) => state.auth);
const { isLoading } = useSelector((state) => state.uptimeMonitors);
const dispatch = useDispatch();
// Local state
const [errors, setErrors] = useState({});
const [https, setHttps] = useState(true);
const [useAdvancedMatching, setUseAdvancedMatching] = useState(false);
const [monitor, setMonitor] = useState({
url: "",
name: "",
type: "http",
matchMethod: "equal",
notifications: [],
interval: 1,
ignoreTlsErrors: false,
});
// Setup
const theme = useTheme();
const { t } = useTranslation();
const navigate = useNavigate();
const [notifications, notificationsAreLoading, error] = useGetNotificationsByTeamId();
const MS_PER_MINUTE = 60000;
const SELECT_VALUES = [
{ _id: 1, name: "1 minute" },
@@ -71,37 +94,19 @@ const CreateMonitor = () => {
},
};
const { user } = useSelector((state) => state.auth);
const { isLoading } = useSelector((state) => state.uptimeMonitors);
const dispatch = useDispatch();
const navigate = useNavigate();
const theme = useTheme();
const { monitorId } = useParams();
const crumbs = [
const BREADCRUMBS = [
{ name: "uptime", path: "/uptime" },
{ name: "create", path: `/uptime/create` },
];
// State
const [isNotificationModalOpen, setIsNotificationModalOpen] = useState(false);
// Handlers
const handleOpenNotificationModal = () => {
setIsNotificationModalOpen(true);
};
const [errors, setErrors] = useState({});
const [https, setHttps] = useState(true);
const [monitor, setMonitor] = useState({
url: "",
name: "",
type: "http",
ignoreTlsErrors: false,
notifications: [],
interval: 1,
});
const handleCreateMonitor = async (event) => {
const onSubmit = async (event) => {
event.preventDefault();
const { notifications, ...rest } = monitor;
let form = {
...rest,
url:
//prepending protocol for url
monitor.type === "http"
@@ -110,14 +115,14 @@ const CreateMonitor = () => {
port: monitor.type === "port" ? monitor.port : undefined,
name: monitor.name || monitor.url.substring(0, 50),
type: monitor.type,
ignoreTlsErrors: monitor.ignoreTlsErrors,
interval: monitor.interval * MS_PER_MINUTE,
};
if (monitor.type === "http") {
form.expectedValue = monitor.expectedValue;
form.jsonPath = monitor.jsonPath;
form.matchMethod = monitor.matchMethod;
// If not using advanced matching, remove advanced settings
if (!useAdvancedMatching) {
form.matchMethod = undefined;
form.expectedValue = undefined;
form.jsonPath = undefined;
}
const { error } = monitorValidation.validate(form, {
@@ -141,6 +146,7 @@ const CreateMonitor = () => {
userId: user._id,
notifications: monitor.notifications,
};
const action = await dispatch(createUptimeMonitor({ monitor: form }));
if (action.meta.requestStatus === "fulfilled") {
createToast({ body: "Monitor created successfully!" });
@@ -150,115 +156,60 @@ const CreateMonitor = () => {
}
};
const handleChange = (event, formName) => {
const { type, checked, value } = event.target;
const newVal = type === "checkbox" ? checked : value;
const newMonitor = {
...monitor,
[formName]: newVal,
};
if (formName === "type") {
newMonitor.url = "";
const onChange = (event) => {
const { name, value, checked } = event.target;
let newValue = value;
if (name === "ignoreTlsErrors") {
newValue = checked;
}
setMonitor(newMonitor);
const updatedMonitor = {
...monitor,
[name]: newValue,
};
setMonitor(updatedMonitor);
const { error } = monitorValidation.validate(
{ type: monitor.type, [formName]: newVal },
{ type: monitor.type, [name]: newValue },
{ abortEarly: false }
);
setErrors((prev) => ({
...prev,
url: undefined,
...(error ? { [formName]: error.details[0].message } : { [formName]: undefined }),
...(error ? { [name]: error.details[0].message } : { [name]: undefined }),
}));
};
const handleNotifications = (event, type) => {
const { value } = event.target;
let notifications = [...monitor.notifications];
const notificationExists = notifications.some((notification) => {
if (notification.type === type && notification.address === value) {
return true;
}
return false;
});
if (notificationExists) {
notifications = notifications.filter((notification) => {
if (notification.type === type && notification.address === value) {
return false;
}
return true;
});
} else {
notifications.push({ type, address: value });
}
setMonitor((prev) => ({
...prev,
notifications,
}));
};
useEffect(() => {
const fetchMonitor = async () => {
if (monitorId) {
const action = await dispatch(getUptimeMonitorById({ monitorId }));
if (action.payload.success) {
const data = action.payload.data;
const { name, ...rest } = data; //data.name is read-only
if (rest.type === "http") {
const url = new URL(rest.url);
rest.url = url.host;
}
rest.name = `${name} (Clone)`;
rest.interval /= MS_PER_MINUTE;
setMonitor({
...rest,
});
} else {
navigate("/not-found", { replace: true });
createToast({
body: "There was an error cloning the monitor.",
});
}
}
};
fetchMonitor();
}, [monitorId, dispatch, navigate]);
const { t } = useTranslation();
return (
<Box className="create-monitor">
<Breadcrumbs list={crumbs} />
<Stack
component="form"
gap={theme.spacing(12)}
mt={theme.spacing(6)}
onSubmit={handleCreateMonitor}
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<Typography
component="h1"
variant="h1"
>
<Typography
component="h1"
variant="h1"
component="span"
fontSize="inherit"
>
<Typography
component="span"
fontSize="inherit"
>
{t("createYour")}{" "}
</Typography>
<Typography
component="span"
variant="h2"
fontSize="inherit"
fontWeight="inherit"
>
{t("monitor")}
</Typography>
{t("createYour")}{" "}
</Typography>
<Typography
component="span"
variant="h2"
fontSize="inherit"
fontWeight="inherit"
>
{t("monitor")}
</Typography>
</Typography>
<Stack
component="form"
noValidate
gap={theme.spacing(12)}
mt={theme.spacing(6)}
onSubmit={onSubmit}
>
<ConfigBox>
<Box>
<Typography
@@ -274,13 +225,13 @@ const CreateMonitor = () => {
<Stack gap={theme.spacing(12)}>
<Stack gap={theme.spacing(6)}>
<Radio
id="monitor-checks-http"
name="type"
title={t("websiteMonitoring")}
desc={t("websiteMonitoringDescription")}
size="small"
value="http"
checked={monitor.type === "http"}
onChange={(event) => handleChange(event, "type")}
onChange={onChange}
/>
{monitor.type === "http" ? (
<ButtonGroup sx={{ ml: theme.spacing(16) }}>
@@ -304,31 +255,31 @@ const CreateMonitor = () => {
)}
</Stack>
<Radio
id="monitor-checks-ping"
name="type"
title={t("pingMonitoring")}
desc={t("pingMonitoringDescription")}
size="small"
value="ping"
checked={monitor.type === "ping"}
onChange={(event) => handleChange(event, "type")}
onChange={onChange}
/>
<Radio
id="monitor-checks-docker"
name="type"
title={t("dockerContainerMonitoring")}
desc={t("dockerContainerMonitoringDescription")}
size="small"
value="docker"
checked={monitor.type === "docker"}
onChange={(event) => handleChange(event, "type")}
onChange={onChange}
/>
<Radio
id="monitor-checks-port"
name="type"
title={t("portMonitoring")}
desc={t("portMonitoringDescription")}
size="small"
value="port"
checked={monitor.type === "port"}
onChange={(event) => handleChange(event, "type")}
onChange={onChange}
/>
{errors["type"] ? (
<Box className="error-container">
@@ -359,8 +310,8 @@ const CreateMonitor = () => {
</Box>
<Stack gap={theme.spacing(15)}>
<TextInput
name="url"
type={monitor.type === "http" ? "url" : "text"}
id="monitor-url"
startAdornment={
monitor.type === "http" ? <HttpAdornment https={https} /> : null
}
@@ -368,29 +319,29 @@ const CreateMonitor = () => {
https={https}
placeholder={monitorTypeMaps[monitor.type].placeholder || ""}
value={monitor.url}
onChange={(event) => handleChange(event, "url")}
onChange={onChange}
error={errors["url"] ? true : false}
helperText={errors["url"]}
/>
<TextInput
name="port"
type="number"
id="monitor-port"
label={t("portToMonitor")}
placeholder="5173"
value={monitor.port}
onChange={(event) => handleChange(event, "port")}
onChange={onChange}
error={errors["port"] ? true : false}
helperText={errors["port"]}
hidden={monitor.type !== "port"}
/>
<TextInput
name="name"
type="text"
id="monitor-name"
label={t("displayName")}
isOptional={true}
placeholder={monitorTypeMaps[monitor.type].namePlaceholder || ""}
value={monitor.name}
onChange={(event) => handleChange(event, "name")}
onChange={onChange}
error={errors["name"] ? true : false}
helperText={errors["name"]}
/>
@@ -398,37 +349,15 @@ const CreateMonitor = () => {
</ConfigBox>
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("distributedUptimeCreateIncidentNotification")}
</Typography>
<Typography component="h2">Notifications</Typography>
<Typography component="p">
{t("distributedUptimeCreateIncidentDescription")}
Select the notifications you want to send out
</Typography>
</Box>
<Stack gap={theme.spacing(6)}>
<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) => handleNotifications(event, "email")}
/>
<Box mt={theme.spacing(2)}>
<Button
variant="contained"
color="accent"
onClick={handleOpenNotificationModal}
>
{t("notifications.integrationButton")}
</Button>
</Box>
</Stack>
<NotificationsConfig
notifications={notifications}
setMonitor={setMonitor}
/>
</ConfigBox>
<ConfigBox>
<Box>
@@ -445,9 +374,9 @@ const CreateMonitor = () => {
sx={{ marginLeft: 0 }}
control={
<Switch
name="ignore-error"
name="ignoreTlsErrors"
checked={monitor.ignoreTlsErrors}
onChange={(event) => handleChange(event, "ignoreTlsErrors")}
onChange={onChange}
sx={{ mr: theme.spacing(2) }}
/>
}
@@ -466,32 +395,32 @@ const CreateMonitor = () => {
</Box>
<Stack gap={theme.spacing(12)}>
<Select
id="monitor-interval"
name="interval"
label="Check frequency"
value={monitor.interval || 1}
onChange={(event) => handleChange(event, "interval")}
onChange={onChange}
items={SELECT_VALUES}
/>
{monitor.type === "http" && (
<>
<Select
id="match-method"
name="matchMethod"
label="Match Method"
value={monitor.matchMethod || "equal"}
onChange={(event) => handleChange(event, "matchMethod")}
onChange={onChange}
items={matchMethodOptions}
/>
<Stack>
<TextInput
name="expectedValue"
type="text"
id="expected-value"
label="Expected value"
isOptional={true}
placeholder={
expectedValuePlaceholders[monitor.matchMethod || "equal"]
}
value={monitor.expectedValue}
onChange={(event) => handleChange(event, "expectedValue")}
onChange={onChange}
error={errors["expectedValue"] ? true : false}
helperText={errors["expectedValue"]}
/>
@@ -505,13 +434,13 @@ const CreateMonitor = () => {
</Stack>
<Stack>
<TextInput
name="jsonPath"
type="text"
id="json-path"
label="JSON Path"
isOptional={true}
placeholder="data.status"
value={monitor.jsonPath}
onChange={(event) => handleChange(event, "jsonPath")}
onChange={onChange}
error={errors["jsonPath"] ? true : false}
helperText={errors["jsonPath"]}
/>
@@ -541,9 +470,9 @@ const CreateMonitor = () => {
justifyContent="flex-end"
>
<Button
type="submit"
variant="contained"
color="accent"
onClick={handleCreateMonitor}
disabled={!Object.values(errors).every((value) => value === undefined)}
loading={isLoading}
>
@@ -551,14 +480,7 @@ const CreateMonitor = () => {
</Button>
</Stack>
</Stack>
<NotificationIntegrationModal
open={isNotificationModalOpen}
onClose={() => setIsNotificationModalOpen(false)}
monitor={monitor}
setMonitor={setMonitor}
/>
</Box>
</Stack>
);
};