feat(monitors): integrate bulk actions into uptime and infrastructure tables

- Mounts selection checkboxes in UptimeMonitorsTable and MonitorsTable
- Enhances global Table.tsx to cleanly render checkboxes on mobile cards via mobileLabel
- Injects BulkActionsBar into main list views

Fixes #3602
This commit is contained in:
Retr01-sudo committed 2026-05-01 19:44:07 +05:30
1 parent 6b9759e455
commit 4e284c1468
5 files changed
+208 -59

No files matched your search

@@ -35,6 +35,7 @@ import useMediaQuery from "@mui/material/useMediaQuery";
export type Header<T> = {
id: number | string;
content: React.ReactNode;
mobileLabel?: React.ReactNode;
onClick?: (event: React.MouseEvent<HTMLTableCellElement | null>, row: T) => void;
render: (row: T) => React.ReactNode;
};
@@ -109,6 +110,24 @@ export function DataTable<
key={key}
>
{headers.map((header) => {
if (header.mobileLabel === null) {
return (
<Grid2
container
key={header.id}
>
<Grid2
size={12}
display="flex"
alignItems="center"
justifyContent="flex-end"
>
{header.render(row)}
</Grid2>
</Grid2>
);
}
return (
<Grid2
container
@@ -123,7 +142,9 @@ export function DataTable<
component="div"
color={theme.palette.text.primary}
>
{header.content}
{header.mobileLabel !== undefined
? header.mobileLabel
: header.content}
</Typography>
</Grid2>
<Grid2
@@ -12,6 +12,25 @@ import { useNavigate } from "react-router-dom";
import { usePost } from "@/Hooks/UseApi";
import type { Monitor } from "@/Types/Monitor";
import { Checkbox } from "@/Components/inputs";
import { useTableSelection } from "@/Hooks/useTableSelection";
interface InfraMonitorsTableProps {
monitors: Monitor[];
refetch: () => void;
setSelectedMonitor: (monitor: Monitor | null) => void;
sortField: string;
setSortField: (field: string) => void;
sortOrder: "asc" | "desc";
setSortOrder: (order: "asc" | "desc") => void;
count: number;
page: number;
setPage: (page: number) => void;
rowsPerPage: number;
setRowsPerPage: (rowsPerPage: number) => void;
selectedRows?: string[];
onSelectionChange?: (selected: string[]) => void;
}
export const InfraMonitorsTable = ({
monitors,
@@ -26,29 +45,22 @@ export const InfraMonitorsTable = ({
setPage,
rowsPerPage,
setRowsPerPage,
}: {
monitors: Monitor[];
refetch: Function;
setSelectedMonitor: Function;
sortField: string;
setSortField: (field: string) => void;
sortOrder: "asc" | "desc";
setSortOrder: (order: "asc" | "desc") => void;
count: number;
page: number;
setPage: (page: number) => void;
rowsPerPage: number;
setRowsPerPage: (rowsPerPage: number) => void;
}) => {
selectedRows = [],
onSelectionChange,
}: InfraMonitorsTableProps) => {
const { t } = useTranslation();
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const navigate = useNavigate();
const { post } = usePost<Record<string, never>, Monitor>();
const {
post,
// loading: isPatching,
// error: postError,
} = usePost<any, Monitor>();
isAllSelected,
isSomeSelected,
handleSelectAll,
handleSelectRow,
isRowSelected,
} = useTableSelection(monitors, selectedRows, onSelectionChange);
const handlePageChange = (
_e: React.MouseEvent<HTMLButtonElement> | null,
@@ -65,7 +77,7 @@ export const InfraMonitorsTable = ({
setRowsPerPage(value);
};
const handleSort = (e: any, field: string) => {
const handleSort = (e: React.MouseEvent, field: string) => {
e.preventDefault();
e.stopPropagation();
if (sortField === field) {
@@ -101,13 +113,6 @@ export const InfraMonitorsTable = ({
navigate(`/infrastructure/configure/${monitor.id}`);
},
},
// {
// id: 5,
// label: "Clone",
// action: () => {
// },
// },
{
id: 6,
label:
@@ -115,7 +120,7 @@ export const InfraMonitorsTable = ({
? t("common.buttons.resume")
: t("common.buttons.pause"),
action: async () => {
await post(`/monitors/pause/${monitor.id}`, {});
await post(`/monitors/pause/${monitor.id}`, {} as Record<string, never>);
refetch();
},
closeMenu: true,
@@ -152,8 +157,34 @@ export const InfraMonitorsTable = ({
</Box>
);
const headers: Header<Monitor>[] = [
{
id: "selection",
content: (
<Checkbox
aria-label={t("pages.common.monitors.actions.selectAll")}
indeterminate={isSomeSelected}
checked={isAllSelected}
onChange={(e) => handleSelectAll(e.target.checked)}
/>
),
mobileLabel: null,
render: (row) => (
<Checkbox
aria-label={t("pages.common.monitors.actions.selectMonitor", {
name: row.name,
})}
checked={isRowSelected(row.id)}
onChange={(e) => {
handleSelectRow(row.id, e.target.checked);
}}
onClick={(e) => e.stopPropagation()}
/>
),
onClick: (e) => e?.stopPropagation(),
},
{
id: "name",
mobileLabel: t("common.table.headers.name"),
content: (
<Typography
component="div"
@@ -173,6 +204,7 @@ export const InfraMonitorsTable = ({
},
{
id: "status",
mobileLabel: t("common.table.headers.status"),
content: (
<Typography
component="div"
@@ -248,6 +280,11 @@ export const InfraMonitorsTable = ({
onRowClick={(row) => {
navigate(`/infrastructure/${row.id}`);
}}
getRowSx={(row) => ({
backgroundColor: isRowSelected(row.id)
? theme.palette.action.selected
: "inherit",
})}
/>
<Pagination
component="div"
@@ -2,7 +2,11 @@ import Stack from "@mui/material/Stack";
import useMediaQuery from "@mui/material/useMediaQuery";
import { MonitorBasePageWithStates } from "@/Components/design-elements";
import { HeaderCreate } from "@/Components/common";
import { ControlsFilter, HeaderMonitorsSummary } from "@/Components/monitors";
import {
ControlsFilter,
HeaderMonitorsSummary,
BulkActionsBar,
} from "@/Components/monitors";
import { TextField, Dialog } from "@/Components/inputs";
import { useGet, useDelete } from "@/Hooks/UseApi";
@@ -16,6 +20,7 @@ import { setRowsPerPage } from "@/Features/UI/uiSlice.js";
import type { RootState } from "@/Types/state";
import { InfraMonitorsTable } from "./Components/MonitorsTable";
import useDebounce from "@/Hooks/useDebounce";
import { useBulkMonitorActions } from "@/Hooks/useBulkMonitorActions";
const InfrastructureMonitors = () => {
const { t } = useTranslation();
@@ -37,6 +42,7 @@ const InfrastructureMonitors = () => {
const [sortField, setSortField] = useState<string>("");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
const [selectedMonitor, setSelectedMonitor] = useState<Monitor | null>(null);
const isDialogOpen = Boolean(selectedMonitor);
const debouncedSearch = useDebounce<string>(search, 300);
@@ -97,6 +103,15 @@ const InfrastructureMonitors = () => {
const { summary, count } = monitorsWithChecksData ?? { summary: null, count: 0 };
const isLoading = monitorsWithChecksLoading;
// Bulk actions
const {
selectedRows,
setSelectedRows,
handleBulkPause,
handleBulkResume,
handleCancelSelection,
} = useBulkMonitorActions(refetch, page);
// Check if any filters are active
const hasActiveFilters = Boolean(selectedStatus || selectedState || search);
@@ -159,6 +174,16 @@ const InfrastructureMonitors = () => {
}}
/>
</Stack>
{!isLoading && (
<BulkActionsBar
selectedCount={selectedRows.length}
onResume={handleBulkResume}
onPause={handleBulkPause}
onCancel={handleCancelSelection}
/>
)}
<InfraMonitorsTable
monitors={monitorsWithChecksData?.monitors || []}
refetch={refetch}
@@ -180,6 +205,8 @@ const InfrastructureMonitors = () => {
);
setPage(0);
}}
selectedRows={selectedRows}
onSelectionChange={setSelectedRows}
/>
<Dialog
open={isDialogOpen}
@@ -20,6 +20,25 @@ import { useSelector } from "react-redux";
import type { Monitor } from "@/Types/Monitor";
import type { ActionMenuItem } from "@/Components/actions-menu";
import type { RootState } from "@/Types/state";
import { Checkbox } from "@/Components/inputs";
import { useTableSelection } from "@/Hooks/useTableSelection";
interface MonitorTableProps {
monitors: Monitor[];
refetch: () => void;
setSelectedMonitor: (monitor: Monitor | null) => void;
sortField: string;
setSortField: (field: string) => void;
sortOrder: "asc" | "desc";
setSortOrder: (order: "asc" | "desc") => void;
count: number;
page: number;
setPage: (page: number) => void;
rowsPerPage: number;
setRowsPerPage: (rowsPerPage: number) => void;
selectedRows?: string[];
onSelectionChange?: (selected: string[]) => void;
}
export const MonitorTable = ({
monitors,
@@ -34,31 +53,24 @@ export const MonitorTable = ({
setPage,
rowsPerPage,
setRowsPerPage,
}: {
monitors: Monitor[];
refetch: Function;
setSelectedMonitor: (monitor: Monitor | null) => void;
sortField: string;
setSortField: (field: string) => void;
sortOrder: "asc" | "desc";
setSortOrder: (order: "asc" | "desc") => void;
count: number;
page: number;
setPage: (page: number) => void;
rowsPerPage: number;
setRowsPerPage: (rowsPerPage: number) => void;
}) => {
selectedRows = [],
onSelectionChange,
}: MonitorTableProps) => {
const { t } = useTranslation();
const theme = useTheme();
const navigate = useNavigate();
const chartType = useSelector((state: RootState) => state.ui?.chartType ?? "histogram");
const {
post,
// loading: isPosting,
// error: postError,
} = usePost<any, Monitor>();
const { post } = usePost<Record<string, never>, Monitor>();
const handleSort = (e: any, field: string) => {
const {
isAllSelected,
isSomeSelected,
handleSelectAll,
handleSelectRow,
isRowSelected,
} = useTableSelection(monitors, selectedRows, onSelectionChange);
const handleSort = (e: React.MouseEvent, field: string) => {
e.preventDefault();
e.stopPropagation();
if (sortField === field) {
@@ -110,13 +122,6 @@ export const MonitorTable = ({
navigate(`/uptime/configure/${monitor.id}`);
},
},
// {
// id: 5,
// label: "Clone",
// action: () => {
// },
// },
{
id: 6,
label:
@@ -124,7 +129,7 @@ export const MonitorTable = ({
? t("pages.common.monitors.actions.resume")
: t("pages.common.monitors.actions.pause"),
action: async () => {
await post(`/monitors/pause/${monitor.id}`, {});
await post(`/monitors/pause/${monitor.id}`, {} as Record<string, never>);
refetch();
},
closeMenu: true,
@@ -160,8 +165,34 @@ export const MonitorTable = ({
</Box>
);
const headers: Header<Monitor>[] = [
{
id: "selection",
content: (
<Checkbox
aria-label={t("pages.common.monitors.actions.selectAll")}
indeterminate={isSomeSelected}
checked={isAllSelected}
onChange={(e) => handleSelectAll(e.target.checked)}
/>
),
mobileLabel: null,
render: (row) => (
<Checkbox
aria-label={t("pages.common.monitors.actions.selectMonitor", {
name: row.name,
})}
checked={isRowSelected(row.id)}
onChange={(e) => {
handleSelectRow(row.id, e.target.checked);
}}
onClick={(e) => e.stopPropagation()}
/>
),
onClick: (e) => e?.stopPropagation(),
},
{
id: "name",
mobileLabel: t("common.table.headers.name"),
content: (
<Stack
gap={theme.spacing(4)}
@@ -181,11 +212,11 @@ export const MonitorTable = ({
},
{
id: "status",
mobileLabel: t("common.table.headers.status"),
content: (
<Stack
gap={theme.spacing(4)}
direction={"row"}
justifyContent={"center"}
alignItems={"center"}
onClick={(e) => handleSort(e, "status")}
sx={{ cursor: "pointer" }}
@@ -212,6 +243,7 @@ export const MonitorTable = ({
},
{
id: "type",
mobileLabel: t("common.table.headers.type"),
content: (
<Stack
gap={theme.spacing(4)}
@@ -251,6 +283,11 @@ export const MonitorTable = ({
onRowClick={(row) => {
navigate(`/uptime/${row.id}`);
}}
getRowSx={(row) => ({
backgroundColor: isRowSelected(row.id)
? theme.palette.action.selected
: "inherit",
})}
/>
<Pagination
component="div"
+28 -1
View File
@@ -1,4 +1,8 @@
import { ControlsFilter, HeaderMonitorsSummary } from "@/Components/monitors";
import {
ControlsFilter,
HeaderMonitorsSummary,
BulkActionsBar,
} from "@/Components/monitors";
import { MonitorBasePageWithStates } from "@/Components/design-elements";
import { TextField, Dialog } from "@/Components/inputs";
import Stack from "@mui/material/Stack";
@@ -16,6 +20,7 @@ import { useIsAdmin } from "@/Hooks/useIsAdmin";
import type { RootState } from "@/Types/state";
import { useTheme } from "@mui/material";
import useDebounce from "@/Hooks/useDebounce";
import { useBulkMonitorActions } from "@/Hooks/useBulkMonitorActions";
const UptimeMonitorsPage = () => {
const { t } = useTranslation();
@@ -37,6 +42,7 @@ const UptimeMonitorsPage = () => {
const [sortField, setSortField] = useState<string>("");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
const [selectedMonitor, setSelectedMonitor] = useState<Monitor | null>(null);
const isDialogOpen = Boolean(selectedMonitor);
const debouncedSearch = useDebounce<string>(search, 300);
@@ -102,6 +108,15 @@ const UptimeMonitorsPage = () => {
count,
} = monitorsWithChecksData ?? { monitors: null, summary: null, count: 0 };
// Bulk actions
const {
selectedRows,
setSelectedRows,
handleBulkPause,
handleBulkResume,
handleCancelSelection,
} = useBulkMonitorActions(refetch, page);
// Delete hook
const { deleteFn, loading: isDeleting } = useDelete();
@@ -177,6 +192,16 @@ const UptimeMonitorsPage = () => {
}}
/>
</Stack>
{!isLoading && (
<BulkActionsBar
selectedCount={selectedRows.length}
onResume={handleBulkResume}
onPause={handleBulkPause}
onCancel={handleCancelSelection}
/>
)}
<MonitorTable
monitors={monitorsWithChecks || []}
refetch={refetch}
@@ -198,6 +223,8 @@ const UptimeMonitorsPage = () => {
);
setPage(0);
}}
selectedRows={selectedRows}
onSelectionChange={setSelectedRows}
/>
<Dialog
open={isDialogOpen}