feat(ui): add bulk edit notifications modal and checkmark selection for uptime monitors
This commit is contained in:
1 parent
c5f0316990
commit
27dd565bc6
5 files changed
+263
-81
No files matched your search
@@ -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: (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user