diff --git a/client/package-lock.json b/client/package-lock.json index 338de1e26..aaba3b828 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -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" diff --git a/client/package.json b/client/package.json index dd585636b..13d4714b7 100644 --- a/client/package.json +++ b/client/package.json @@ -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", diff --git a/client/src/Pages/Uptime/Monitors/Components/BulkEditNotificationsModal.tsx b/client/src/Pages/Uptime/Monitors/Components/BulkEditNotificationsModal.tsx new file mode 100644 index 000000000..948a74bef --- /dev/null +++ b/client/src/Pages/Uptime/Monitors/Components/BulkEditNotificationsModal.tsx @@ -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("add"); + const [selectedNotificationIds, setSelectedNotificationIds] = useState([]); + + const { data: notifications } = useGet("/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 ( + + + + + 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 && ( + + {selectedItems.map((notification, index) => ( + + {notification.notificationName} + { + setSelectedNotificationIds((prev) => + prev.filter((id) => id !== notification.id) + ); + }} + aria-label="Remove notification" + > + + + {index < selectedItems.length - 1 && } + + ))} + + )} + + + ); +}; diff --git a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx index d8951b6c5..f8bc46125 100644 --- a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx +++ b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx @@ -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>; }) => { const { t } = useTranslation(); const theme = useTheme(); @@ -151,6 +156,37 @@ export const MonitorTable = ({ ); const headers: Header[] = [ + { + id: "select", + content: ( + 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) => ( + { + e.stopPropagation(); + if (e.target.checked) { + setSelectedMonitors((prev) => [...prev, row.id]); + } else { + setSelectedMonitors((prev) => prev.filter((id) => id !== row.id)); + } + }} + /> + ), + }, { id: "name", content: ( diff --git a/client/src/Pages/Uptime/Monitors/index.tsx b/client/src/Pages/Uptime/Monitors/index.tsx index 80ee8e2d9..988d8414c 100644 --- a/client/src/Pages/Uptime/Monitors/index.tsx +++ b/client/src/Pages/Uptime/Monitors/index.tsx @@ -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(null); const isDialogOpen = Boolean(selectedMonitor); + const [selectedMonitors, setSelectedMonitors] = useState([]); + const [isBulkEditOpen, setIsBulkEditOpen] = useState(false); + const debouncedSearch = useDebounce(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)} > - - { - setSearch(event.target.value); - }} - /> + {selectedMonitors.length > 0 ? ( + + + {selectedMonitors.length} monitor(s) selected + + + + + ) : ( + <> + + { + setSearch(event.target.value); + }} + /> + + )} { 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} /> + setIsBulkEditOpen(false)} + onSuccess={() => { + setSelectedMonitors([]); + refetch(); + }} + /> ); };