implement editing notification channels

This commit is contained in:
Alex Holliday
2025-06-10 11:13:19 +08:00
parent f41709bd12
commit 0ac1f13043
13 changed files with 265 additions and 25 deletions

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,
};

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;

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"

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}
/>
);
},
},

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" },
];

View File

@@ -157,6 +157,12 @@ const Routes = () => {
path="notifications/create"
element={<CreateNotifications />}
/>
<Route
path="notifications/:notificationId"
element={<CreateNotifications />}
/>
<Route
path="maintenance"
element={<Maintenance />}

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;

View File

@@ -293,6 +293,7 @@
"webhookPlaceholder": "https://your-server.com/webhook"
}
},
"notificationConfig": {
"title": "Notifications",
"description": "Select the notifications channels you want to use"
@@ -354,6 +355,10 @@
"delete": {
"success": "Notification deleted successfully",
"failed": "Failed to delete notification"
},
"edit": {
"success": "Notification updated successfully",
"failed": "Failed to update notification"
}
},
"testLocale": "testLocale",

View File

@@ -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;

View File

@@ -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,
};

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() {