diff --git a/client/src/Components/monitors/BulkEditNotificationsModal.tsx b/client/src/Components/monitors/BulkEditNotificationsModal.tsx new file mode 100644 index 000000000..f5c813a90 --- /dev/null +++ b/client/src/Components/monitors/BulkEditNotificationsModal.tsx @@ -0,0 +1,145 @@ +import React, { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material/styles"; +import { Dialog, Select, RadioWithDescription } from "@/Components/inputs"; +import Stack from "@mui/material/Stack"; +import RadioGroup from "@mui/material/RadioGroup"; +import MenuItem from "@mui/material/MenuItem"; +import Typography from "@mui/material/Typography"; +import { useGet, usePatch } from "@/Hooks/UseApi"; +import type { Notification } from "@/Types/Notification"; + +interface BulkEditNotificationsModalProps { + open: boolean; + onClose: () => void; + selectedMonitors: string[]; + onSuccess: () => void; +} + +export const BulkEditNotificationsModal: React.FC = ({ + open, + onClose, + selectedMonitors, + onSuccess, +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + + const [action, setAction] = useState<"add" | "remove" | "set">("add"); + const [notificationIds, setNotificationIds] = useState([]); + + // Fetch available notifications + const { data: notifications } = useGet( + "/notifications/team", + undefined, + { + keepPreviousData: true, + } + ); + + const { patch, loading: isPatching } = usePatch(); + + // Reset state when modal opens + useEffect(() => { + if (open) { + setAction("add"); + setNotificationIds([]); + } + }, [open]); + + const handleConfirm = async () => { + const res = await patch("/monitors/notifications", { + monitorIds: selectedMonitors, + notificationIds, + action, + }); + + if (res) { + onSuccess(); + } + }; + + // Prevent submitting empty arrays unless the action is 'set' (as per PR feedback!) + const isMissingSelection = action !== "set" && notificationIds.length === 0; + + return ( + + + + {t("pages.common.monitors.bulkEdit.selectedText", { + count: selectedMonitors.length, + defaultValue: `You are applying changes to {{count}} monitor(s).`, + })} + + + setAction(e.target.value as "add" | "remove" | "set")} + > + + + + + + + + + ); +}; diff --git a/client/src/Components/monitors/index.tsx b/client/src/Components/monitors/index.tsx index 46532706f..7bea0339a 100644 --- a/client/src/Components/monitors/index.tsx +++ b/client/src/Components/monitors/index.tsx @@ -15,3 +15,4 @@ export * from "./charts/HistogramPageSpeedDetailsTooltip"; export * from "./charts/HistogramInfrastructure"; export * from "./HeaderMonitorsSummary"; export * from "./BulkActionsBar"; +export * from "./BulkEditNotificationsModal"; diff --git a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx index 31b8b05c6..e9e730ffb 100644 --- a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx +++ b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx @@ -44,6 +44,9 @@ export const MonitorTable = ({ monitors, refetch, setSelectedMonitor, + selectedMonitors, + onSelectMonitor, + onSelectAll, sortField, setSortField, sortOrder, diff --git a/client/src/Pages/Uptime/Monitors/index.tsx b/client/src/Pages/Uptime/Monitors/index.tsx index 85db2cb11..0a6102098 100644 --- a/client/src/Pages/Uptime/Monitors/index.tsx +++ b/client/src/Pages/Uptime/Monitors/index.tsx @@ -9,6 +9,7 @@ import Stack from "@mui/material/Stack"; import { MonitorTable } from "@/Pages/Uptime/Monitors/Components/UptimeMonitorsTable"; import { HeaderCreate } from "@/Components/common"; import { Play, Pause } from "lucide-react"; +import { BulkEditNotificationsModal } from "@/Components/monitors"; import { useTranslation } from "react-i18next"; import useMediaQuery from "@mui/material/useMediaQuery"; @@ -43,7 +44,7 @@ const UptimeMonitorsPage = () => { const [sortField, setSortField] = useState(""); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); const [selectedMonitor, setSelectedMonitor] = useState(null); - + const [isBulkEditModalOpen, setIsBulkEditModalOpen] = useState(false); const isDialogOpen = Boolean(selectedMonitor); const debouncedSearch = useDebounce(search, 300); @@ -136,6 +137,12 @@ const UptimeMonitorsPage = () => { refetch(); }; + const handleBulkEditSuccess = () => { + setIsBulkEditModalOpen(false); + setSelectedRows([]); + refetch(); + }; + const handleCancel = () => { setSelectedMonitor(null); }; @@ -213,6 +220,14 @@ const UptimeMonitorsPage = () => { > {t("common.buttons.pause")} + )} @@ -248,6 +263,12 @@ const UptimeMonitorsPage = () => { onCancel={handleCancel} loading={isDeleting} /> + setIsBulkEditModalOpen(false)} + selectedMonitors={selectedRows} + onSuccess={handleBulkEditSuccess} + /> ); }; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index b964eec84..af2f317d3 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -1,5 +1,6 @@ { "common": { + "selected": "selected", "auth": { "roles": { "admin": "Admin", @@ -425,6 +426,19 @@ }, "common": { "monitors": { + "bulkEdit": { + "editButton": "Edit Notifications", + "title": "Edit Notifications", + "selectedText": "You are applying changes to {{count}} monitor(s).", + "actionAdd": "Add", + "actionAddDesc": "Adds the selected notifications to these monitors without removing existing ones.", + "actionRemove": "Remove", + "actionRemoveDesc": "Removes exclusively the selected notifications from these monitors.", + "actionSet": "Replace all", + "actionSetDesc": "Replaces all existing notifications. Leave empty to clear all notifications.", + "selectLabel": "Notification Channels", + "selectPlaceholder": "Select channels..." + }, "actions": { "configure": "Configure", "delete": "Delete",