diff --git a/client/src/Hooks/useNotifications.js b/client/src/Hooks/useNotifications.js index 4ece55acd..c8dc6b3db 100644 --- a/client/src/Hooks/useNotifications.js +++ b/client/src/Hooks/useNotifications.js @@ -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, +}; diff --git a/client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx b/client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx index 5fc2430c7..fe8c688ac 100644 --- a/client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx +++ b/client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx @@ -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; diff --git a/client/src/Pages/Infrastructure/Details/Hooks/useHardwareMonitorsFetch.jsx b/client/src/Pages/Infrastructure/Details/Hooks/useHardwareMonitorsFetch.jsx index f7cb2abb1..c0b61ef62 100644 --- a/client/src/Pages/Infrastructure/Details/Hooks/useHardwareMonitorsFetch.jsx +++ b/client/src/Pages/Infrastructure/Details/Hooks/useHardwareMonitorsFetch.jsx @@ -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, diff --git a/client/src/Pages/Notifications/components/ActionMenu.jsx b/client/src/Pages/Notifications/components/ActionMenu.jsx new file mode 100644 index 000000000..bd719bfee --- /dev/null +++ b/client/src/Pages/Notifications/components/ActionMenu.jsx @@ -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 ( + <> + + + + + + Configure + + Remove + + + + ); +}; + +ActionMenu.propTypes = { + notification: PropTypes.object, + onDelete: PropTypes.func, +}; + +export default ActionMenu; diff --git a/client/src/Pages/Notifications/create/index.jsx b/client/src/Pages/Notifications/create/index.jsx index b1469bff0..b123dcbd7 100644 --- a/client/src/Pages/Notifications/create/index.jsx +++ b/client/src/Pages/Notifications/create/index.jsx @@ -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" > + ); }, }, diff --git a/client/src/Pages/Notifications/utils.js b/client/src/Pages/Notifications/utils.js new file mode 100644 index 000000000..7135e5c63 --- /dev/null +++ b/client/src/Pages/Notifications/utils.js @@ -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" }, +]; diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 48343cd22..3f815a665 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -157,6 +157,12 @@ const Routes = () => { path="notifications/create" element={} /> + + } + /> + } diff --git a/client/src/Utils/NetworkService.js b/client/src/Utils/NetworkService.js index 26c6bd460..56ec4dcb7 100644 --- a/client/src/Utils/NetworkService.js +++ b/client/src/Utils/NetworkService.js @@ -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; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 5510e5c54..92f490aba 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -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", diff --git a/server/controllers/notificationController.js b/server/controllers/notificationController.js index 7cb642977..df8fe8ca3 100755 --- a/server/controllers/notificationController.js +++ b/server/controllers/notificationController.js @@ -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; diff --git a/server/db/mongo/modules/notificationModule.js b/server/db/mongo/modules/notificationModule.js index d172ee8cf..08d57b1a9 100755 --- a/server/db/mongo/modules/notificationModule.js +++ b/server/db/mongo/modules/notificationModule.js @@ -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, }; diff --git a/server/routes/notificationRoute.js b/server/routes/notificationRoute.js index fe6b1c2dd..aa8433e53 100755 --- a/server/routes/notificationRoute.js +++ b/server/routes/notificationRoute.js @@ -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() {