Merge pull request #2419 from bluewave-labs/feat/edit-notification-channel
feat: implement editing notification channels
This commit is contained in:
15 files changed
+279
-32
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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" },
|
||||
];
|
||||
@@ -157,6 +157,12 @@ const Routes = () => {
|
||||
path="notifications/create"
|
||||
element={<CreateNotifications />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="notifications/:notificationId"
|
||||
element={<CreateNotifications />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="maintenance"
|
||||
element={<Maintenance />}
|
||||
|
||||
@@ -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 |
@@ -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,
|
||||
};
|
||||
@@ -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() {
|
||||
|
||||
Reference in new issue
Block a user