diff --git a/client/src/Components/monitors/BulkActionsBar.tsx b/client/src/Components/monitors/BulkActionsBar.tsx
new file mode 100644
index 000000000..aef5e4871
--- /dev/null
+++ b/client/src/Components/monitors/BulkActionsBar.tsx
@@ -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 (
+ 0}>
+
+
+
+ {t("pages.common.monitors.actions.bulkSelected", {
+ count: selectedCount,
+ })}
+
+
+ }
+ onClick={onResume}
+ >
+ {t("common.buttons.resume")}
+
+ }
+ onClick={onPause}
+ >
+ {t("common.buttons.pause")}
+
+
+
+
+
+
+ );
+};
diff --git a/client/src/Components/monitors/index.tsx b/client/src/Components/monitors/index.tsx
index efba79a08..46532706f 100644
--- a/client/src/Components/monitors/index.tsx
+++ b/client/src/Components/monitors/index.tsx
@@ -14,3 +14,4 @@ export * from "./charts/HistogramPageSpeedDetails";
export * from "./charts/HistogramPageSpeedDetailsTooltip";
export * from "./charts/HistogramInfrastructure";
export * from "./HeaderMonitorsSummary";
+export * from "./BulkActionsBar";
diff --git a/client/src/Hooks/useBulkMonitorActions.ts b/client/src/Hooks/useBulkMonitorActions.ts
new file mode 100644
index 000000000..3bb520aa1
--- /dev/null
+++ b/client/src/Hooks/useBulkMonitorActions.ts
@@ -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;
+ handleBulkResume: () => Promise;
+ handleCancelSelection: () => void;
+}
+
+export const useBulkMonitorActions = (
+ refetch: () => void,
+ page?: number
+): UseBulkMonitorActionsReturn => {
+ const [selectedRows, setSelectedRows] = useState([]);
+ const { post: bulkPost } = usePost();
+
+ // 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,
+ };
+};
diff --git a/client/src/Hooks/useTableSelection.ts b/client/src/Hooks/useTableSelection.ts
new file mode 100644
index 000000000..890225ade
--- /dev/null
+++ b/client/src/Hooks/useTableSelection.ts
@@ -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,
+ };
+};
diff --git a/client/src/locales/ar.json b/client/src/locales/ar.json
index 11fa81309..70509feed 100644
--- a/client/src/locales/ar.json
+++ b/client/src/locales/ar.json
@@ -421,7 +421,10 @@
"inviteMember": "دعوة عضو",
"openSite": "فتح الموقع",
"pause": "إيقاف مؤقت",
- "resume": "استئناف"
+ "resume": "استئناف",
+ "bulkSelected": "تم تحديد {{count}} جهاز(أجهزة) مراقبة",
+ "selectAll": "تحديد جميع أجهزة المراقبة",
+ "selectMonitor": "تحديد {{name}}"
},
"statBoxes": {
"activeFor": "نشط منذ",
diff --git a/client/src/locales/cs.json b/client/src/locales/cs.json
index 454f0b673..e86677c29 100644
--- a/client/src/locales/cs.json
+++ b/client/src/locales/cs.json
@@ -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",
diff --git a/client/src/locales/de.json b/client/src/locales/de.json
index 8090aa5f3..cd93c4810 100644
--- a/client/src/locales/de.json
+++ b/client/src/locales/de.json
@@ -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",
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index d6e8bb4f3..2e22baa07 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -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",
diff --git a/client/src/locales/es.json b/client/src/locales/es.json
index 506c317e3..04f447e40 100644
--- a/client/src/locales/es.json
+++ b/client/src/locales/es.json
@@ -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",
diff --git a/client/src/locales/fi.json b/client/src/locales/fi.json
index 6238837f2..74afedd84 100644
--- a/client/src/locales/fi.json
+++ b/client/src/locales/fi.json
@@ -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",
diff --git a/client/src/locales/fr.json b/client/src/locales/fr.json
index 306a2bd61..8e3c78b85 100644
--- a/client/src/locales/fr.json
+++ b/client/src/locales/fr.json
@@ -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",
diff --git a/client/src/locales/ja.json b/client/src/locales/ja.json
index 923213a46..09db6971f 100644
--- a/client/src/locales/ja.json
+++ b/client/src/locales/ja.json
@@ -421,7 +421,10 @@
"inviteMember": "メンバーを招待",
"openSite": "サイトを開く",
"pause": "一時停止",
- "resume": "再開"
+ "resume": "再開",
+ "bulkSelected": "{{count}} 件のモニターを選択中",
+ "selectAll": "すべてのモニターを選択",
+ "selectMonitor": "{{name}} を選択"
},
"statBoxes": {
"activeFor": "稼働期間",
diff --git a/client/src/locales/pt-BR.json b/client/src/locales/pt-BR.json
index f7b7c4cbc..6c97a9869 100644
--- a/client/src/locales/pt-BR.json
+++ b/client/src/locales/pt-BR.json
@@ -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á",
diff --git a/client/src/locales/ru.json b/client/src/locales/ru.json
index 1a5d12452..778d481f9 100644
--- a/client/src/locales/ru.json
+++ b/client/src/locales/ru.json
@@ -421,7 +421,10 @@
"inviteMember": "Пригласить участника",
"openSite": "Открыть сайт",
"pause": "Пауза",
- "resume": "Продолжить"
+ "resume": "Продолжить",
+ "bulkSelected": "Выбрано мониторов: {{count}}",
+ "selectAll": "Выбрать все мониторы",
+ "selectMonitor": "Выбрать {{name}}"
},
"statBoxes": {
"activeFor": "Активен",
diff --git a/client/src/locales/th.json b/client/src/locales/th.json
index 433ee10cd..aab97dc93 100644
--- a/client/src/locales/th.json
+++ b/client/src/locales/th.json
@@ -421,7 +421,10 @@
"inviteMember": "เชิญสมาชิก",
"openSite": "เปิดเว็บไซต์",
"pause": "หยุดชั่วคราว",
- "resume": "ดำเนินการต่อ"
+ "resume": "ดำเนินการต่อ",
+ "bulkSelected": "เลือก {{count}} มอนิเตอร์แล้ว",
+ "selectAll": "เลือกมอนิเตอร์ทั้งหมด",
+ "selectMonitor": "เลือก {{name}}"
},
"statBoxes": {
"activeFor": "ทำงานมาแล้ว",
diff --git a/client/src/locales/tr.json b/client/src/locales/tr.json
index a77dd8eb3..6e6aaf365 100644
--- a/client/src/locales/tr.json
+++ b/client/src/locales/tr.json
@@ -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",
diff --git a/client/src/locales/uk.json b/client/src/locales/uk.json
index 5b467e4b4..f7ff24647 100644
--- a/client/src/locales/uk.json
+++ b/client/src/locales/uk.json
@@ -421,7 +421,10 @@
"inviteMember": "Запросити учасника",
"openSite": "Відкрити сайт",
"pause": "Призупинити",
- "resume": "Відновити"
+ "resume": "Відновити",
+ "bulkSelected": "Вибрано моніторів: {{count}}",
+ "selectAll": "Вибрати всі монітори",
+ "selectMonitor": "Вибрати {{name}}"
},
"statBoxes": {
"activeFor": "Активний протягом",
diff --git a/client/src/locales/vi.json b/client/src/locales/vi.json
index dc3e804de..90d8afd04 100644
--- a/client/src/locales/vi.json
+++ b/client/src/locales/vi.json
@@ -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ừ",
diff --git a/client/src/locales/zh-CN.json b/client/src/locales/zh-CN.json
index ed5e70c1e..73a4ce166 100644
--- a/client/src/locales/zh-CN.json
+++ b/client/src/locales/zh-CN.json
@@ -421,7 +421,10 @@
"inviteMember": "邀请成员",
"openSite": "打开站点",
"pause": "暂停",
- "resume": "恢复"
+ "resume": "恢复",
+ "bulkSelected": "已选择 {{count}} 个监控",
+ "selectAll": "选择所有监控",
+ "selectMonitor": "选择 {{name}}"
},
"statBoxes": {
"activeFor": "已活跃",
diff --git a/client/src/locales/zh-TW.json b/client/src/locales/zh-TW.json
index ff6e9f6dd..91fc7766e 100644
--- a/client/src/locales/zh-TW.json
+++ b/client/src/locales/zh-TW.json
@@ -421,7 +421,10 @@
"inviteMember": "邀請成員",
"openSite": "開啟網站",
"pause": "暫停",
- "resume": "恢復"
+ "resume": "恢復",
+ "bulkSelected": "已選擇 {{count}} 個監控",
+ "selectAll": "選擇所有監控",
+ "selectMonitor": "選擇 {{name}}"
},
"statBoxes": {
"activeFor": "已啟用",