feat(ui): add bulk edit notifications modal and checkmark selection for uptime monitors

This commit is contained in:
ashu130698 committed 2026-03-06 21:07:29 +05:30
1 parent c5f0316990
commit 27dd565bc6
5 files changed
+263 -81

No files matched your search

+35 -65
View File
@@ -18,6 +18,7 @@
"@mui/x-date-pickers": "7.29.4",
"@reduxjs/toolkit": "2.7.0",
"@types/maplibre-gl": "^1.13.2",
"@types/react-map-gl": "^6.1.8",
"axios": "^1.7.4",
"dayjs": "1.11.13",
"flag-icons": "7.3.2",
@@ -434,7 +435,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -451,7 +451,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -468,7 +467,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -485,7 +483,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -502,7 +499,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -519,7 +515,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -536,7 +531,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -553,7 +547,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -570,7 +563,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -587,7 +579,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -604,7 +595,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -621,7 +611,6 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -638,7 +627,6 @@
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -655,7 +643,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -672,7 +659,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -689,7 +675,6 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -704,7 +689,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -721,7 +705,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -738,7 +721,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -755,7 +737,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -772,7 +753,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -789,7 +769,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -806,7 +785,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -823,7 +801,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -840,7 +817,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -857,7 +833,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1666,7 +1641,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1680,7 +1654,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1694,7 +1667,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1708,7 +1680,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1722,7 +1693,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1736,7 +1706,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1750,7 +1719,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1764,7 +1732,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1778,7 +1745,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1792,7 +1758,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1806,7 +1771,6 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1820,7 +1784,6 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1834,7 +1797,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1848,7 +1810,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1862,7 +1823,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1876,7 +1836,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1890,7 +1849,6 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1902,7 +1860,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1916,7 +1873,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1930,7 +1886,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1944,7 +1899,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1958,7 +1912,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1972,7 +1925,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1986,7 +1938,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2000,7 +1951,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2339,6 +2289,15 @@
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
"license": "MIT"
},
"node_modules/@types/mapbox-gl": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-3.4.1.tgz",
"integrity": "sha512-NsGKKtgW93B+UaLPti6B7NwlxYlES5DpV5Gzj9F75rK5ALKsqSk15CiEHbOnTr09RGbr6ZYiCdI+59NNNcAImg==",
"license": "MIT",
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/maplibre-gl": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@types/maplibre-gl/-/maplibre-gl-1.13.2.tgz",
@@ -2350,7 +2309,7 @@
},
"node_modules/@types/node": {
"version": "24.5.2",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.12.0"
@@ -2366,7 +2325,6 @@
},
"node_modules/@types/react": {
"version": "18.3.27",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -2381,6 +2339,18 @@
"@types/react": "^18.0.0"
}
},
"node_modules/@types/react-map-gl": {
"version": "6.1.8",
"resolved": "https://registry.npmjs.org/@types/react-map-gl/-/react-map-gl-6.1.8.tgz",
"integrity": "sha512-M0WLRx7Q7m47aFoNRu7NxwaboY/K12J+zgSesJnwisIq5EgqkGUdl9AUb/yRd0cJtx6oOTeOMVPb7emEdlXpQg==",
"license": "MIT",
"dependencies": {
"@types/geojson": "*",
"@types/mapbox-gl": "<3.5",
"@types/react": "*",
"@types/viewport-mercator-project": "*"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.12",
"license": "MIT",
@@ -2401,6 +2371,15 @@
"version": "0.0.6",
"license": "MIT"
},
"node_modules/@types/viewport-mercator-project": {
"version": "6.1.6",
"resolved": "https://registry.npmjs.org/@types/viewport-mercator-project/-/viewport-mercator-project-6.1.6.tgz",
"integrity": "sha512-uWrbqhRXFeiT6CAvRjf0BkQKRkKED+ofrPhglKpUktQML3463dEPiA4iwe7cZQs6m49Zo/g03rL7ChMLiE5Z8w==",
"license": "MIT",
"dependencies": {
"gl-matrix": "^3.2.0"
}
},
"node_modules/@ungap/structured-clone": {
"version": "1.3.0",
"dev": true,
@@ -3566,7 +3545,6 @@
},
"node_modules/esbuild": {
"version": "0.25.12",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
@@ -3884,7 +3862,6 @@
},
"node_modules/fdir": {
"version": "6.5.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
@@ -4018,7 +3995,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -5317,7 +5293,6 @@
},
"node_modules/nanoid": {
"version": "3.3.11",
"dev": true,
"funding": [
{
"type": "github",
@@ -5650,7 +5625,6 @@
},
"node_modules/postcss": {
"version": "8.5.6",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -6158,7 +6132,6 @@
},
"node_modules/rollup": {
"version": "4.57.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.8"
@@ -6530,7 +6503,6 @@
},
"node_modules/source-map-js": {
"version": "1.2.1",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -6818,7 +6790,6 @@
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
@@ -6948,7 +6919,7 @@
},
"node_modules/typescript": {
"version": "5.9.3",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -6992,7 +6963,7 @@
},
"node_modules/undici-types": {
"version": "7.12.0",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/union-value": {
@@ -7082,7 +7053,6 @@
},
"node_modules/vite": {
"version": "6.3.6",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
@@ -7366,7 +7336,7 @@
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
"dev": true,
"devOptional": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
+1
View File
@@ -26,6 +26,7 @@
"@mui/x-date-pickers": "7.29.4",
"@reduxjs/toolkit": "2.7.0",
"@types/maplibre-gl": "^1.13.2",
"@types/react-map-gl": "^6.1.8",
"axios": "^1.7.4",
"dayjs": "1.11.13",
"flag-icons": "7.3.2",
@@ -0,0 +1,134 @@
import { Dialog, Select, Autocomplete } from "@/Components/inputs";
import { useState, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useGet, usePatch } from "@/Hooks/UseApi";
import type { Notification } from "@/Types/Notification";
import Stack from "@mui/material/Stack";
import MenuItem from "@mui/material/MenuItem";
import Typography from "@mui/material/Typography";
import Divider from "@mui/material/Divider";
import IconButton from "@mui/material/IconButton";
import { Trash2 } from "lucide-react";
import { useTheme } from "@mui/material/styles";
import { LAYOUT } from "@/Utils/Theme/constants";
type ActionType = "add" | "remove" | "set";
export const BulkEditNotificationsModal = ({
open,
monitorIds,
onClose,
onSuccess,
}: {
open: boolean;
monitorIds: string[];
onClose: () => void;
onSuccess: () => void;
}) => {
const { t } = useTranslation();
const theme = useTheme();
const [action, setAction] = useState<ActionType>("add");
const [selectedNotificationIds, setSelectedNotificationIds] = useState<string[]>([]);
const { data: notifications } = useGet<Notification[]>("/notifications/team");
const { patch, loading } = usePatch();
const handleConfirm = async () => {
if (selectedNotificationIds.length === 0 && action !== "set") return;
const result = await patch("/notifications", {
monitorIds,
notificationIds: selectedNotificationIds,
action,
});
if (result?.success) {
onSuccess();
onClose();
}
};
const notificationOptions = useMemo(() => {
return (notifications ?? []).map((n) => ({
...n,
name: n.notificationName,
}));
}, [notifications]);
const selectedItems = useMemo(() => {
return notificationOptions.filter((n) => selectedNotificationIds.includes(n.id));
}, [notificationOptions, selectedNotificationIds]);
return (
<Dialog
open={open}
title={t("pages.uptime.bulkEdit.title", { defaultValue: "Bulk Edit Notifications" })}
content={t("pages.uptime.bulkEdit.description", {
defaultValue: `Apply notification changes to ${monitorIds.length} selected monitors.`,
})}
onConfirm={handleConfirm}
onCancel={onClose}
loading={loading}
confirmText={t("common.buttons.save")}
>
<Stack spacing={theme.spacing(LAYOUT.MD)} sx={{ mt: 2 }}>
<Select
value={action}
onChange={(e) => setAction(e.target.value as ActionType)}
fieldLabel={t("pages.uptime.bulkEdit.actionLabel", { defaultValue: "Action" })}
>
<MenuItem value="add">
{t("pages.uptime.bulkEdit.actionAdd", { defaultValue: "Add Notifications" })}
</MenuItem>
<MenuItem value="remove">
{t("pages.uptime.bulkEdit.actionRemove", { defaultValue: "Remove Notifications" })}
</MenuItem>
<MenuItem value="set">
{t("pages.uptime.bulkEdit.actionSet", { defaultValue: "Set Notifications (Replace All)" })}
</MenuItem>
</Select>
<Autocomplete
multiple
options={notificationOptions}
value={selectedItems}
getOptionLabel={(option) => option.name}
onChange={(_: unknown, newValue: typeof notificationOptions) => {
setSelectedNotificationIds(newValue.map((n) => n.id));
}}
isOptionEqualToValue={(option, value) => option.id === value.id}
fieldLabel={t("pages.uptime.bulkEdit.notificationsLabel", {
defaultValue: "Notifications",
})}
/>
{selectedItems.length > 0 && (
<Stack flex={1} width="100%">
{selectedItems.map((notification, index) => (
<Stack
direction="row"
alignItems="center"
key={notification.id}
width="100%"
>
<Typography flexGrow={1}>{notification.notificationName}</Typography>
<IconButton
size="small"
onClick={() => {
setSelectedNotificationIds((prev) =>
prev.filter((id) => id !== notification.id)
);
}}
aria-label="Remove notification"
>
<Trash2 size={16} />
</IconButton>
{index < selectedItems.length - 1 && <Divider />}
</Stack>
))}
</Stack>
)}
</Stack>
</Dialog>
);
};
@@ -16,6 +16,7 @@ import { useTheme } from "@mui/material/styles";
import { useNavigate } from "react-router-dom";
import { usePost } from "@/Hooks/UseApi";
import { useSelector } from "react-redux";
import { Checkbox } from "@/Components/inputs";
import type { Monitor } from "@/Types/Monitor";
import type { ActionMenuItem } from "@/Components/actions-menu";
@@ -34,6 +35,8 @@ export const MonitorTable = ({
setPage,
rowsPerPage,
setRowsPerPage,
selectedMonitors,
setSelectedMonitors,
}: {
monitors: Monitor[];
refetch: Function;
@@ -47,6 +50,8 @@ export const MonitorTable = ({
setPage: (page: number) => void;
rowsPerPage: number;
setRowsPerPage: (rowsPerPage: number) => void;
selectedMonitors: string[];
setSelectedMonitors: React.Dispatch<React.SetStateAction<string[]>>;
}) => {
const { t } = useTranslation();
const theme = useTheme();
@@ -151,6 +156,37 @@ export const MonitorTable = ({
</Box>
);
const headers: Header<Monitor>[] = [
{
id: "select",
content: (
<Checkbox
checked={monitors.length > 0 && selectedMonitors.length === monitors.length}
indeterminate={selectedMonitors.length > 0 && selectedMonitors.length < monitors.length}
onChange={(e) => {
e.stopPropagation();
if (e.target.checked) {
setSelectedMonitors(monitors.map((m) => m.id));
} else {
setSelectedMonitors([]);
}
}}
/>
),
onClick: (e) => e.stopPropagation(),
render: (row) => (
<Checkbox
checked={selectedMonitors.includes(row.id)}
onChange={(e) => {
e.stopPropagation();
if (e.target.checked) {
setSelectedMonitors((prev) => [...prev, row.id]);
} else {
setSelectedMonitors((prev) => prev.filter((id) => id !== row.id));
}
}}
/>
),
},
{
id: "name",
content: (
+57 -16
View File
@@ -2,8 +2,11 @@ import { ControlsFilter, HeaderMonitorsSummary } from "@/Components/monitors";
import { MonitorBasePageWithStates } from "@/Components/design-elements";
import { TextField, Dialog } from "@/Components/inputs";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { MonitorTable } from "@/Pages/Uptime/Monitors/Components/UptimeMonitorsTable";
import { HeaderCreate } from "@/Components/common";
import { Button } from "@/Components/inputs";
import { BulkEditNotificationsModal } from "./Components/BulkEditNotificationsModal";
import { useTranslation } from "react-i18next";
import useMediaQuery from "@mui/material/useMediaQuery";
@@ -39,6 +42,9 @@ const UptimeMonitorsPage = () => {
const [selectedMonitor, setSelectedMonitor] = useState<Monitor | null>(null);
const isDialogOpen = Boolean(selectedMonitor);
const [selectedMonitors, setSelectedMonitors] = useState<string[]>([]);
const [isBulkEditOpen, setIsBulkEditOpen] = useState(false);
const debouncedSearch = useDebounce<string>(search, 300);
// Convert filter selections to API filter values
@@ -159,22 +165,46 @@ const UptimeMonitorsPage = () => {
justifyContent={isSmall ? "flex-start" : "space-between"}
gap={theme.spacing(4)}
>
<ControlsFilter
selectedTypes={selectedTypes}
setSelectedTypes={setSelectedTypes}
selectedStatus={selectedStatus}
setSelectedStatus={setSelectedStatus}
selectedState={selectedState}
setSelectedState={setSelectedState}
onClearFilters={handleClearFilters}
/>
<TextField
placeholder={t("pages.uptime.filters.search.placeholder")}
value={search}
onChange={(event) => {
setSearch(event.target.value);
}}
/>
{selectedMonitors.length > 0 ? (
<Stack direction="row" alignItems="center" spacing={2} width="100%">
<Typography variant="body1" fontWeight="medium">
{selectedMonitors.length} monitor(s) selected
</Typography>
<Button
variant="contained"
color="primary"
onClick={() => setIsBulkEditOpen(true)}
>
Bulk Edit Notifications
</Button>
<Button
variant="text"
color="inherit"
onClick={() => setSelectedMonitors([])}
>
Cancel Selection
</Button>
</Stack>
) : (
<>
<ControlsFilter
selectedTypes={selectedTypes}
setSelectedTypes={setSelectedTypes}
selectedStatus={selectedStatus}
setSelectedStatus={setSelectedStatus}
selectedState={selectedState}
setSelectedState={setSelectedState}
onClearFilters={handleClearFilters}
/>
<TextField
placeholder={t("pages.uptime.filters.search.placeholder")}
value={search}
onChange={(event) => {
setSearch(event.target.value);
}}
/>
</>
)}
</Stack>
<MonitorTable
monitors={monitorsWithChecks || []}
@@ -188,6 +218,8 @@ const UptimeMonitorsPage = () => {
setSortField={setSortField}
sortOrder={sortOrder}
setSortOrder={setSortOrder}
selectedMonitors={selectedMonitors}
setSelectedMonitors={setSelectedMonitors}
setRowsPerPage={(rowsPerPage: number) => {
dispatch(
setRowsPerPage({
@@ -206,6 +238,15 @@ const UptimeMonitorsPage = () => {
onCancel={handleCancel}
loading={isDeleting}
/>
<BulkEditNotificationsModal
open={isBulkEditOpen}
monitorIds={selectedMonitors}
onClose={() => setIsBulkEditOpen(false)}
onSuccess={() => {
setSelectedMonitors([]);
refetch();
}}
/>
</MonitorBasePageWithStates>
);
};