Merge pull request #2419 from bluewave-labs/feat/edit-notification-channel

feat: implement editing notification channels
This commit is contained in:
Alexander Holliday authored and GitHub committed 2025-06-10 13:47:25 +08:00
commit 64390bae2b
15 files changed
+279 -32

No files matched your search

+3 -5
View File
@@ -38,7 +38,7 @@ import ChangeLog from "../../assets/icons/changeLog.svg?react";
import Docs from "../../assets/icons/docs.svg?react";
import StatusPages from "../../assets/icons/status-pages.svg?react";
import Discussions from "../../assets/icons/discussions.svg?react";
import NotificationAddOutlinedIcon from "@mui/icons-material/NotificationAddOutlined";
import Notifications from "../../assets/icons/notifications.svg?react";
import "./index.css";
@@ -59,7 +59,7 @@ const getMenu = (t) => [
{
name: t("menu.notifications"),
path: "notifications",
icon: <NotificationAddOutlinedIcon />,
icon: <Notifications />,
},
{ name: t("menu.incidents"), path: "incidents", icon: <Incidents /> },
@@ -129,9 +129,7 @@ function Sidebar() {
const [anchorEl, setAnchorEl] = useState(null);
const [popup, setPopup] = useState();
const { user } = useSelector((state) => state.auth);
const distributedUptimeEnabled = useSelector(
(state) => state.ui.distributedUptimeEnabled
);
const sidebarRef = useRef(null);
const [sidebarReady, setSidebarReady] = useState(false);
const TRANSITION_DURATION = 200;
+71 -1
View File
@@ -4,6 +4,7 @@ import { networkService } from "../main";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import { NOTIFICATION_TYPES } from "../Pages/Notifications/utils";
const useCreateNotification = () => {
const navigate = useNavigate();
@@ -88,4 +89,73 @@ const useDeleteNotification = () => {
return [deleteNotification, isLoading, error];
};
export { useCreateNotification, useGetNotificationsByTeamId, useDeleteNotification };
const useGetNotificationById = (id, setNotification) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const getNotificationById = useCallback(async () => {
try {
setIsLoading(true);
const response = await networkService.getNotificationById({ id });
const notification = response?.data?.data ?? null;
const notificationData = {
userId: notification?.userId,
teamId: notification?.teamId,
address: notification?.address,
notificationName: notification?.notificationName,
type: NOTIFICATION_TYPES.find((type) => type.value === notification?.type)?._id,
config: notification?.config,
};
setNotification(notificationData);
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
}, [id, setNotification]);
useEffect(() => {
if (id) {
getNotificationById();
}
}, [getNotificationById, id]);
return [isLoading, error];
};
const useEditNotification = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const { t } = useTranslation();
const editNotification = async (id, notification) => {
try {
setIsLoading(true);
await networkService.editNotification({ id, notification });
createToast({
body: t("notifications.edit.success"),
});
} catch (error) {
setError(error);
createToast({
body: t("notifications.edit.failed"),
});
} finally {
setIsLoading(false);
}
};
return [editNotification, isLoading, error];
};
export {
useCreateNotification,
useGetNotificationsByTeamId,
useDeleteNotification,
useGetNotificationById,
useEditNotification,
};
@@ -42,8 +42,8 @@ const ForgotPasswordLabel = ({ email, errorEmail }) => {
};
ForgotPasswordLabel.propTypes = {
email: PropTypes.string.isRequired,
errorEmail: PropTypes.string.isRequired,
email: PropTypes.string,
errorEmail: PropTypes.string,
};
export default ForgotPasswordLabel;
@@ -3,9 +3,7 @@ import { networkService } from "../../../../main";
const useHardwareMonitorsFetch = ({ monitorId, dateRange }) => {
// Abort early if creating monitor
if (!monitorId) {
return { monitor: undefined, isLoading: false, networkError: undefined };
}
const [isLoading, setIsLoading] = useState(true);
const [networkError, setNetworkError] = useState(false);
const [monitor, setMonitor] = useState(undefined);
@@ -13,6 +11,9 @@ const useHardwareMonitorsFetch = ({ monitorId, dateRange }) => {
useEffect(() => {
const fetchData = async () => {
try {
if (!monitorId) {
return { monitor: undefined, isLoading: false, networkError: undefined };
}
const response = await networkService.getHardwareDetailsByMonitorId({
monitorId: monitorId,
dateRange: dateRange,
@@ -0,0 +1,70 @@
// Components
import Menu from "@mui/material/Menu";
import IconButton from "@mui/material/IconButton";
import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
import MenuItem from "@mui/material/MenuItem";
// Utils
import { useState } from "react";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import PropTypes from "prop-types";
const ActionMenu = ({ notification, onDelete }) => {
const theme = useTheme();
const navigate = useNavigate();
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
// Handlers
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const handleRemove = () => {
onDelete(notification._id);
handleClose();
};
const handleConfigure = () => {
navigate(`/notifications/${notification._id}`);
handleClose();
};
return (
<>
<IconButton
aria-label="monitor actions"
onClick={handleClick}
>
<SettingsOutlinedIcon />
</IconButton>
<Menu
anchorEl={anchorEl}
open={open}
onClose={handleClose}
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
>
<MenuItem onClick={handleConfigure}>Configure</MenuItem>
<MenuItem
onClick={handleRemove}
sx={{ "&.MuiButtonBase-root": { color: theme.palette.error.main } }}
>
Remove
</MenuItem>
</Menu>
</>
);
};
ActionMenu.propTypes = {
notification: PropTypes.object,
onDelete: PropTypes.func,
};
export default ActionMenu;
+22 -11
View File
@@ -12,7 +12,11 @@ import TextInput from "../../../Components/Inputs/TextInput";
import { useState } from "react";
import { useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
import { useCreateNotification } from "../../../Hooks/useNotifications";
import {
useCreateNotification,
useGetNotificationById,
useEditNotification,
} from "../../../Hooks/useNotifications";
import {
notificationEmailValidation,
notificationWebhookValidation,
@@ -20,19 +24,17 @@ import {
} from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";
import { NOTIFICATION_TYPES } from "../utils";
// Setup
const NOTIFICATION_TYPES = [
{ _id: 1, name: "E-mail", value: "email" },
{ _id: 2, name: "Slack", value: "webhook" },
{ _id: 3, name: "PagerDuty", value: "pager_duty" },
{ _id: 4, name: "Webhook", value: "webhook" },
];
const CreateNotifications = () => {
const { notificationId } = useParams();
const theme = useTheme();
const [createNotification, isLoading, error] = useCreateNotification();
const [createNotification, isCreating, createNotificationError] =
useCreateNotification();
const [editNotification, isEditing, editNotificationError] = useEditNotification();
const BREADCRUMBS = [
{ name: "notifications", path: "/notifications" },
{ name: "create", path: "/notifications/create" },
@@ -57,6 +59,11 @@ const CreateNotifications = () => {
const [errors, setErrors] = useState({});
const { t } = useTranslation();
const [notificationIsLoading, getNotificationError] = useGetNotificationById(
notificationId,
setNotification
);
// handlers
const onSubmit = (e) => {
e.preventDefault();
@@ -106,7 +113,11 @@ const CreateNotifications = () => {
return;
}
createNotification(form);
if (notificationId) {
editNotification(notificationId, form);
} else {
createNotification(form);
}
};
const onChange = (e) => {
@@ -348,7 +359,7 @@ const CreateNotifications = () => {
justifyContent="flex-end"
>
<Button
loading={isLoading}
loading={isCreating || isEditing || notificationIsLoading}
type="submit"
variant="contained"
color="accent"
+8 -9
View File
@@ -5,6 +5,8 @@ import Breadcrumbs from "../../Components/Breadcrumbs";
import Button from "@mui/material/Button";
import DataTable from "../../Components/Table";
import Fallback from "../../Components/Fallback";
import ActionMenu from "./components/ActionMenu";
// Utils
import { useIsAdmin } from "../../Hooks/useIsAdmin";
import { useState } from "react";
@@ -15,7 +17,7 @@ import {
useDeleteNotification,
} from "../../Hooks/useNotifications";
import { useTranslation } from "react-i18next";
// Setup
import { useParams } from "react-router-dom";
const Notifications = () => {
const navigate = useNavigate();
@@ -70,13 +72,10 @@ const Notifications = () => {
content: "Actions",
render: (row) => {
return (
<Button
variant="contained"
color="error"
onClick={() => onDelete(row._id)}
>
Delete
</Button>
<ActionMenu
notification={row}
onDelete={onDelete}
/>
);
},
},
@@ -87,7 +86,7 @@ const Notifications = () => {
<Fallback
vowelStart={false}
title={t("notifications.fallback.title")}
checks={[t("notifications.fallback.checks")]}
checks={t("notifications.fallback.checks", { returnObjects: true })}
link="/notifications/create"
isAdmin={isAdmin}
/>
+6
View File
@@ -0,0 +1,6 @@
export const NOTIFICATION_TYPES = [
{ _id: 1, name: "E-mail", value: "email" },
{ _id: 2, name: "Slack", value: "webhook" },
{ _id: 3, name: "PagerDuty", value: "pager_duty" },
{ _id: 4, name: "Webhook", value: "webhook" },
];
+6
View File
@@ -157,6 +157,12 @@ const Routes = () => {
path="notifications/create"
element={<CreateNotifications />}
/>
<Route
path="notifications/:notificationId"
element={<CreateNotifications />}
/>
<Route
path="maintenance"
element={<Maintenance />}
+10
View File
@@ -1049,6 +1049,16 @@ class NetworkService {
const { id } = config;
return this.axiosInstance.delete(`/notifications/${id}`);
}
async getNotificationById(config) {
const { id } = config;
return this.axiosInstance.get(`/notifications/${id}`);
}
async editNotification(config) {
const { id, notification } = config;
return this.axiosInstance.put(`/notifications/${id}`, notification);
}
}
export default NetworkService;
@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M9.35395 21C10.0591 21.6224 10.9853 22 11.9998 22C13.0142 22 13.9405 21.6224 14.6456 21M17.9998 8V2M14.9998 5H20.9998M12.9998 2.08389C12.6715 2.02841 12.3371 2 11.9998 2C10.4085 2 8.88235 2.63214 7.75713 3.75736C6.63192 4.88258 5.99977 6.4087 5.99977 8C5.99977 11.0902 5.22024 13.206 4.34944 14.6054C3.61491 15.7859 3.24763 16.3761 3.2611 16.5408C3.27601 16.7231 3.31463 16.7926 3.46155 16.9016C3.59423 17 4.19237 17 5.38863 17H18.6109C19.8071 17 20.4052 17 20.5379 16.9016C20.6848 16.7926 20.7235 16.7231 20.7384 16.5408C20.7518 16.3761 20.3845 15.7858 19.65 14.6052C19.1579 13.8144 18.695 12.7948 18.3855 11.5"
stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 826 B

+10 -1
View File
@@ -293,6 +293,7 @@
"webhookPlaceholder": "https://your-server.com/webhook"
}
},
"notificationConfig": {
"title": "Notifications",
"description": "Select the notifications channels you want to use"
@@ -300,7 +301,11 @@
"notifications": {
"fallback": {
"title": "notification channel",
"checks": "Alert teams about downtime or performance issues"
"checks": [
"Alert teams about downtime or performance issues",
"Let engineers know when incidents happen",
"Keep administrators informed of system changes"
]
},
"createButton": "Create notification channel",
"createTitle": "Notification channel",
@@ -354,6 +359,10 @@
"delete": {
"success": "Notification deleted successfully",
"failed": "Failed to delete notification"
},
"edit": {
"success": "Notification updated successfully",
"failed": "Failed to update notification"
}
},
"testLocale": "testLocale",
@@ -217,6 +217,39 @@ class NotificationController {
next(handleError(error, SERVICE_NAME, "deleteNotification"));
}
};
getNotificationById = async (req, res, next) => {
try {
const notification = await this.db.getNotificationById(req.params.id);
return res.success({
msg: "Notification fetched successfully",
data: notification,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getNotificationById"));
}
};
editNotification = async (req, res, next) => {
try {
await createNotificationBodyValidation.validateAsync(req.body, {
abortEarly: false,
});
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const notification = await this.db.editNotification(req.params.id, req.body);
return res.success({
msg: "Notification updated successfully",
data: notification,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "editNotification"));
}
};
}
export default NotificationController;
@@ -84,6 +84,30 @@ const deleteNotificationById = async (id) => {
}
};
const getNotificationById = async (id) => {
try {
const notification = await Notification.findById(id);
return notification;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getNotificationById";
throw error;
}
};
const editNotification = async (id, notificationData) => {
try {
const notification = await Notification.findByIdAndUpdate(id, notificationData, {
new: true,
});
return notification;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "editNotification";
throw error;
}
};
export {
createNotification,
getNotificationsByTeamId,
@@ -91,4 +115,6 @@ export {
getNotificationsByMonitorId,
deleteNotificationsByMonitorId,
deleteNotificationById,
getNotificationById,
editNotification,
};
+3
View File
@@ -23,6 +23,9 @@ class NotificationRoutes {
);
this.router.delete("/:id", this.notificationController.deleteNotification);
this.router.get("/:id", this.notificationController.getNotificationById);
this.router.put("/:id", this.notificationController.editNotification);
}
getRouter() {