feat(monitors): create bulk actions ui components and selection hooks

- Extracts generic table selection logic into useTableSelection hook
- Creates BulkActionsBar component with Collapse animations
- Implements useBulkMonitorActions to handle API logic and toasts
- Adds i18n translation strings across all 16 locales

Fixes #3602
This commit is contained in:
Retr01-sudo committed 2026-05-01 19:43:27 +05:30
1 parent 094def117a
commit 6b9759e455
20 files changed
+279 -16

No files matched your search

@@ -0,0 +1,78 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import Collapse from "@mui/material/Collapse";
import { Play, Pause } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useTheme } from "@mui/material/styles";
interface BulkActionsBarProps {
selectedCount: number;
onResume: () => void;
onPause: () => void;
onCancel: () => void;
}
export const BulkActionsBar = ({
selectedCount,
onResume,
onPause,
onCancel,
}: BulkActionsBarProps) => {
const { t } = useTranslation();
const theme = useTheme();
return (
<Collapse in={selectedCount > 0}>
<Box
p={theme.spacing(2, 4)}
bgcolor={theme.palette.action.hover}
borderRadius={1}
border={1}
borderColor={theme.palette.divider}
sx={{ borderStyle: "dashed" }}
>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
>
<Typography variant="body2">
{t("pages.common.monitors.actions.bulkSelected", {
count: selectedCount,
})}
</Typography>
<Stack
direction="row"
gap={theme.spacing(2)}
>
<Button
size="small"
startIcon={<Play size={16} />}
onClick={onResume}
>
{t("common.buttons.resume")}
</Button>
<Button
size="small"
startIcon={<Pause size={16} />}
onClick={onPause}
>
{t("common.buttons.pause")}
</Button>
<Button
size="small"
variant="text"
color="inherit"
onClick={onCancel}
>
{t("common.buttons.cancel")}
</Button>
</Stack>
</Stack>
</Box>
</Collapse>
);
};
+1
View File
@@ -14,3 +14,4 @@ export * from "./charts/HistogramPageSpeedDetails";
export * from "./charts/HistogramPageSpeedDetailsTooltip";
export * from "./charts/HistogramInfrastructure";
export * from "./HeaderMonitorsSummary";
export * from "./BulkActionsBar";
+62
View File
@@ -0,0 +1,62 @@
import { useState, useEffect } from "react";
import { usePost } from "@/Hooks/UseApi";
interface BulkPauseBody {
monitorIds: string[];
pause: boolean;
}
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 { post: bulkPost } = usePost<BulkPauseBody>();
// Clear selection when page changes
useEffect(() => {
setSelectedRows([]);
}, [page]);
const handleBulkPause = async () => {
const result = await bulkPost("/monitors/bulk/pause", {
monitorIds: selectedRows,
pause: true,
});
if (result) {
setSelectedRows([]);
refetch();
}
};
const handleBulkResume = async () => {
const result = await bulkPost("/monitors/bulk/pause", {
monitorIds: selectedRows,
pause: false,
});
if (result) {
setSelectedRows([]);
refetch();
}
};
const handleCancelSelection = () => {
setSelectedRows([]);
};
return {
selectedRows,
setSelectedRows,
handleBulkPause,
handleBulkResume,
handleCancelSelection,
};
};
+74
View File
@@ -0,0 +1,74 @@
import { useMemo, useCallback } from "react";
interface SelectableItem {
id: string;
}
interface UseTableSelectionReturn {
isAllSelected: boolean;
isSomeSelected: boolean;
handleSelectAll: (checked: boolean) => void;
handleSelectRow: (itemId: string, checked: boolean) => void;
isRowSelected: (itemId: string) => boolean;
}
export const useTableSelection = (
items: SelectableItem[],
selectedIds: string[],
onSelectionChange?: (selected: string[]) => void
): UseTableSelectionReturn => {
const selectedSet = useMemo(() => new Set(selectedIds), [selectedIds]);
const isAllSelected = useMemo(
() =>
items.length > 0 &&
items.every((item) => selectedSet.has(item.id)) &&
selectedIds.length >= items.length,
[items, selectedSet, selectedIds.length]
);
const isSomeSelected = useMemo(
() => selectedIds.length > 0 && !isAllSelected,
[selectedIds.length, isAllSelected]
);
const handleSelectAll = useCallback(
(checked: boolean) => {
if (onSelectionChange) {
if (checked) {
const allIds = items.map((item) => item.id);
onSelectionChange(allIds);
} else {
onSelectionChange([]);
}
}
},
[items, onSelectionChange]
);
const handleSelectRow = useCallback(
(itemId: string, checked: boolean) => {
if (onSelectionChange) {
if (checked) {
onSelectionChange([...selectedIds, itemId]);
} else {
onSelectionChange(selectedIds.filter((id) => id !== itemId));
}
}
},
[selectedIds, onSelectionChange]
);
const isRowSelected = useCallback(
(itemId: string) => selectedSet.has(itemId),
[selectedSet]
);
return {
isAllSelected,
isSomeSelected,
handleSelectAll,
handleSelectRow,
isRowSelected,
};
};
+4 -1
View File
@@ -421,7 +421,10 @@
"inviteMember": "دعوة عضو",
"openSite": "فتح الموقع",
"pause": "إيقاف مؤقت",
"resume": "استئناف"
"resume": "استئناف",
"bulkSelected": "تم تحديد {{count}} جهاز(أجهزة) مراقبة",
"selectAll": "تحديد جميع أجهزة المراقبة",
"selectMonitor": "تحديد {{name}}"
},
"statBoxes": {
"activeFor": "نشط منذ",
+4 -1
View File
@@ -421,7 +421,10 @@
"inviteMember": "Pozvat člena",
"openSite": "Otevřít stránku",
"pause": "Pozastavit",
"resume": "Obnovit"
"resume": "Obnovit",
"bulkSelected": "{{count}} monitor(ů) vybráno",
"selectAll": "Vybrat všechny monitory",
"selectMonitor": "Vybrat {{name}}"
},
"statBoxes": {
"activeFor": "Aktivní po dobu",
+4 -1
View File
@@ -421,7 +421,10 @@
"inviteMember": "Mitglied einladen",
"openSite": "Seite öffnen",
"pause": "Pausieren",
"resume": "Fortsetzen"
"resume": "Fortsetzen",
"bulkSelected": "{{count}} Monitor(e) ausgewählt",
"selectAll": "Alle Monitore auswählen",
"selectMonitor": "{{name}} auswählen"
},
"statBoxes": {
"activeFor": "Aktiv seit",
+4 -1
View File
@@ -421,7 +421,10 @@
"inviteMember": "Invite member",
"openSite": "Open site",
"pause": "Pause",
"resume": "Resume"
"resume": "Resume",
"bulkSelected": "{{count}} Monitor(s) selected",
"selectAll": "Select all monitors",
"selectMonitor": "Select {{name}}"
},
"statBoxes": {
"activeFor": "Active for",
+4 -1
View File
@@ -428,7 +428,10 @@
"inviteMember": "Invitar miembro",
"openSite": "Abrir sitio",
"pause": "Pausar",
"resume": "Reanudar"
"resume": "Reanudar",
"bulkSelected": "{{count}} monitor(es) seleccionado(s)",
"selectAll": "Seleccionar todos los monitores",
"selectMonitor": "Seleccionar {{name}}"
},
"statBoxes": {
"activeFor": "Activo desde hace",
+4 -1
View File
@@ -421,7 +421,10 @@
"inviteMember": "Kutsu jäsen",
"openSite": "Avaa sivusto",
"pause": "Keskeytä",
"resume": "Jatka"
"resume": "Jatka",
"bulkSelected": "{{count}} monitori(a) valittu",
"selectAll": "Valitse kaikki monitorit",
"selectMonitor": "Valitse {{name}}"
},
"statBoxes": {
"activeFor": "Aktiivinen",
+4 -1
View File
@@ -421,7 +421,10 @@
"inviteMember": "Inviter un membre",
"openSite": "Ouvrir le site",
"pause": "Pause",
"resume": "Reprendre"
"resume": "Reprendre",
"bulkSelected": "{{count}} moniteur(s) sélectionné(s)",
"selectAll": "Sélectionner tous les moniteurs",
"selectMonitor": "Sélectionner {{name}}"
},
"statBoxes": {
"activeFor": "Actif depuis",
+4 -1
View File
@@ -421,7 +421,10 @@
"inviteMember": "メンバーを招待",
"openSite": "サイトを開く",
"pause": "一時停止",
"resume": "再開"
"resume": "再開",
"bulkSelected": "{{count}} 件のモニターを選択中",
"selectAll": "すべてのモニターを選択",
"selectMonitor": "{{name}} を選択"
},
"statBoxes": {
"activeFor": "稼働期間",
+4 -1
View File
@@ -421,7 +421,10 @@
"inviteMember": "Convidar membro",
"openSite": "Abrir site",
"pause": "Pausar",
"resume": "Retomar"
"resume": "Retomar",
"bulkSelected": "{{count}} monitor(es) selecionado(s)",
"selectAll": "Selecionar todos os monitores",
"selectMonitor": "Selecionar {{name}}"
},
"statBoxes": {
"activeFor": "Ativo há",
+4 -1
View File
@@ -421,7 +421,10 @@
"inviteMember": "Пригласить участника",
"openSite": "Открыть сайт",
"pause": "Пауза",
"resume": "Продолжить"
"resume": "Продолжить",
"bulkSelected": "Выбрано мониторов: {{count}}",
"selectAll": "Выбрать все мониторы",
"selectMonitor": "Выбрать {{name}}"
},
"statBoxes": {
"activeFor": "Активен",
+4 -1
View File
@@ -421,7 +421,10 @@
"inviteMember": "เชิญสมาชิก",
"openSite": "เปิดเว็บไซต์",
"pause": "หยุดชั่วคราว",
"resume": "ดำเนินการต่อ"
"resume": "ดำเนินการต่อ",
"bulkSelected": "เลือก {{count}} มอนิเตอร์แล้ว",
"selectAll": "เลือกมอนิเตอร์ทั้งหมด",
"selectMonitor": "เลือก {{name}}"
},
"statBoxes": {
"activeFor": "ทำงานมาแล้ว",
+4 -1
View File
@@ -421,7 +421,10 @@
"inviteMember": "Üye davet et",
"openSite": "Siteyi aç",
"pause": "Duraklat",
"resume": "Devam et"
"resume": "Devam et",
"bulkSelected": "{{count}} monitör seçildi",
"selectAll": "Tüm monitörleri seç",
"selectMonitor": "{{name}} seç"
},
"statBoxes": {
"activeFor": "Aktif süresi",
+4 -1
View File
@@ -421,7 +421,10 @@
"inviteMember": "Запросити учасника",
"openSite": "Відкрити сайт",
"pause": "Призупинити",
"resume": "Відновити"
"resume": "Відновити",
"bulkSelected": "Вибрано моніторів: {{count}}",
"selectAll": "Вибрати всі монітори",
"selectMonitor": "Вибрати {{name}}"
},
"statBoxes": {
"activeFor": "Активний протягом",
+4 -1
View File
@@ -421,7 +421,10 @@
"inviteMember": "Mời thành viên",
"openSite": "Mở trang web",
"pause": "Tạm dừng",
"resume": "Tiếp tục"
"resume": "Tiếp tục",
"bulkSelected": "Đã chọn {{count}} monitor",
"selectAll": "Chọn tất cả monitor",
"selectMonitor": "Chọn {{name}}"
},
"statBoxes": {
"activeFor": "Hoạt động từ",
+4 -1
View File
@@ -421,7 +421,10 @@
"inviteMember": "邀请成员",
"openSite": "打开站点",
"pause": "暂停",
"resume": "恢复"
"resume": "恢复",
"bulkSelected": "已选择 {{count}} 个监控",
"selectAll": "选择所有监控",
"selectMonitor": "选择 {{name}}"
},
"statBoxes": {
"activeFor": "已活跃",
+4 -1
View File
@@ -421,7 +421,10 @@
"inviteMember": "邀請成員",
"openSite": "開啟網站",
"pause": "暫停",
"resume": "恢復"
"resume": "恢復",
"bulkSelected": "已選擇 {{count}} 個監控",
"selectAll": "選擇所有監控",
"selectMonitor": "選擇 {{name}}"
},
"statBoxes": {
"activeFor": "已啟用",