Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions client/src/Components/design-elements/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ import useMediaQuery from "@mui/material/useMediaQuery";
export type Header<T> = {
id: number | string;
content: React.ReactNode;
mobileLabel?: React.ReactNode;
hideMobileLabel?: boolean;
align?: "left" | "center" | "right" | "justify" | "inherit";
onClick?: (event: React.MouseEvent<HTMLTableCellElement | null>, row: T) => void;
render: (row: T) => React.ReactNode;
};
Expand Down Expand Up @@ -109,6 +112,24 @@ export function DataTable<
key={key}
>
{headers.map((header) => {
if (header.hideMobileLabel) {
return (
<Grid2
container
key={header.id}
>
<Grid2
size={12}
display="flex"
alignItems="center"
justifyContent="flex-end"
>
{header.render(row)}
</Grid2>
</Grid2>
);
}

return (
<Grid2
container
Expand All @@ -123,7 +144,7 @@ export function DataTable<
component="div"
color={theme.palette.text.primary}
>
{header.content}
{header.mobileLabel ?? header.content}
</Typography>
</Grid2>
<Grid2
Expand Down Expand Up @@ -193,7 +214,7 @@ export function DataTable<
{headers.map((header, idx) => {
return (
<TableCell
align={idx === 0 ? "left" : "center"}
align={header.align ?? (idx === 0 ? "left" : "center")}
key={header.id}
>
{header.content}
Expand Down Expand Up @@ -222,7 +243,7 @@ export function DataTable<
{headers.map((header, index) => {
return (
<TableCell
align={index === 0 ? "left" : "center"}
align={header.align ?? (index === 0 ? "left" : "center")}
key={header.id}
onClick={
header.onClick ? (e) => header.onClick!(e, row) : undefined
Expand Down
102 changes: 102 additions & 0 deletions client/src/Components/monitors/BulkActionsBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React from "react";
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Paper from "@mui/material/Paper";
import Slide from "@mui/material/Slide";
import IconButton from "@mui/material/IconButton";
import { X } from "lucide-react";

import { useTranslation } from "react-i18next";
import { useTheme } from "@mui/material/styles";
import useMediaQuery from "@mui/material/useMediaQuery";
import { LAYOUT } from "@/Utils/Theme/constants";
import { useSidebar } from "@/Hooks/useSidebar";

interface BulkActionsBarProps {
selectedCount: number;
onCancel: () => void;
children?: React.ReactNode;
}

export const BulkActionsBar = ({
selectedCount,
onCancel,
children,
}: BulkActionsBarProps) => {
const { t } = useTranslation();
const theme = useTheme();
const isOpen = selectedCount > 0;

const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const { width, collapsedWidth, transition } = useSidebar();

return (
<Box
sx={{
position: "fixed",
bottom: theme.spacing(LAYOUT.XS),
left: isSmall ? collapsedWidth : width,
right: 0,
display: "flex",
justifyContent: "center",
pointerEvents: "none",
zIndex: theme.zIndex.snackbar,
transition: transition.replace("width", "left"),
}}
>
<Slide
direction="up"
in={isOpen}
mountOnEnter
unmountOnExit
>
<Paper
elevation={6}
sx={{
pointerEvents: "auto",
px: LAYOUT.MD,
py: LAYOUT.XS,
borderRadius: theme.shape.borderRadius,
backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
display: "flex",
alignItems: "center",
gap: LAYOUT.XS,
boxShadow: theme.shadows[8],
}}
>
<Stack
direction="row"
alignItems="center"
gap={LAYOUT.XXS}
>
<Typography
variant="body1"
fontWeight={600}
color={theme.palette.text.primary}
>
{t("pages.common.monitors.actions.bulkSelected", {
count: selectedCount,
})}
</Typography>
<IconButton
size="small"
onClick={onCancel}
aria-label={t("common.buttons.cancel")}
>
<X size={18} />
</IconButton>
</Stack>
<Stack
direction="row"
alignItems="center"
gap={LAYOUT.XXS}
>
{children}
</Stack>
</Paper>
</Slide>
</Box>
);
};
1 change: 1 addition & 0 deletions client/src/Components/monitors/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ export * from "./charts/HistogramPageSpeedDetails";
export * from "./charts/HistogramPageSpeedDetailsTooltip";
export * from "./charts/HistogramInfrastructure";
export * from "./HeaderMonitorsSummary";
export * from "./BulkActionsBar";
94 changes: 94 additions & 0 deletions client/src/Hooks/useBulkMonitorActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { useState, useEffect } from "react";
import axios from "axios";
import { post } from "@/Utils/ApiClient";
import { useToast } from "@/Hooks/UseToast";
import { useTranslation } from "react-i18next";
import { logger } from "@/Utils/logger";
import type { Monitor } from "@/Types/Monitor";

interface ApiResponse {
success: boolean;
msg: string;
data: Monitor[];
}

interface UseBulkMonitorActionsReturn {
selectedRows: string[];
setSelectedRows: (rows: string[]) => void;
handleBulkPause: () => Promise<void>;
handleBulkResume: () => Promise<void>;
handleCancelSelection: () => void;
}

export const useBulkMonitorActions = (
refetch: () => void,
page?: number
): UseBulkMonitorActionsReturn => {
const [selectedRows, setSelectedRows] = useState<string[]>([]);
const { toastSuccess, toastError, toastInfo } = useToast();
const { t } = useTranslation();

// Clear selection when page changes
useEffect(() => {
setSelectedRows([]);
}, [page]);

const executeBulkAction = async (pause: boolean) => {
try {
const res = await post<ApiResponse>("/monitors/bulk/pause", {
monitorIds: selectedRows,
pause,
});

const affectedCount = res.data?.data?.length ?? 0;

if (affectedCount === 0) {
const key = pause
? "pages.common.monitors.bulkPause.alreadyPaused"
: "pages.common.monitors.bulkPause.alreadyRunning";
toastInfo(t(key, { count: selectedRows.length }));
} else {
const key = pause
? "pages.common.monitors.bulkPause.paused"
: "pages.common.monitors.bulkPause.resumed";
toastSuccess(t(key, { count: affectedCount }));
}

setSelectedRows([]);
refetch();
} catch (err: unknown) {
let errMsg = "An error occurred";

if (axios.isAxiosError(err)) {
errMsg = err.response?.data?.msg || err.message || errMsg;
} else if (err instanceof Error) {
errMsg = err.message;
}

logger.error("Bulk pause/resume failed", err instanceof Error ? err : undefined, {
pause,
});
toastError(errMsg);
}
};

const handleBulkPause = async () => {
await executeBulkAction(true);
};

const handleBulkResume = async () => {
await executeBulkAction(false);
};

const handleCancelSelection = () => {
setSelectedRows([]);
};

return {
selectedRows,
setSelectedRows,
handleBulkPause,
handleBulkResume,
handleCancelSelection,
};
};
Loading