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:
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>
|
||||
);
|
||||
};
|
||||
@@ -14,3 +14,4 @@ export * from "./charts/HistogramPageSpeedDetails";
|
||||
export * from "./charts/HistogramPageSpeedDetailsTooltip";
|
||||
export * from "./charts/HistogramInfrastructure";
|
||||
export * from "./HeaderMonitorsSummary";
|
||||
export * from "./BulkActionsBar";
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -421,7 +421,10 @@
|
||||
"inviteMember": "دعوة عضو",
|
||||
"openSite": "فتح الموقع",
|
||||
"pause": "إيقاف مؤقت",
|
||||
"resume": "استئناف"
|
||||
"resume": "استئناف",
|
||||
"bulkSelected": "تم تحديد {{count}} جهاز(أجهزة) مراقبة",
|
||||
"selectAll": "تحديد جميع أجهزة المراقبة",
|
||||
"selectMonitor": "تحديد {{name}}"
|
||||
},
|
||||
"statBoxes": {
|
||||
"activeFor": "نشط منذ",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -421,7 +421,10 @@
|
||||
"inviteMember": "メンバーを招待",
|
||||
"openSite": "サイトを開く",
|
||||
"pause": "一時停止",
|
||||
"resume": "再開"
|
||||
"resume": "再開",
|
||||
"bulkSelected": "{{count}} 件のモニターを選択中",
|
||||
"selectAll": "すべてのモニターを選択",
|
||||
"selectMonitor": "{{name}} を選択"
|
||||
},
|
||||
"statBoxes": {
|
||||
"activeFor": "稼働期間",
|
||||
|
||||
@@ -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á",
|
||||
|
||||
@@ -421,7 +421,10 @@
|
||||
"inviteMember": "Пригласить участника",
|
||||
"openSite": "Открыть сайт",
|
||||
"pause": "Пауза",
|
||||
"resume": "Продолжить"
|
||||
"resume": "Продолжить",
|
||||
"bulkSelected": "Выбрано мониторов: {{count}}",
|
||||
"selectAll": "Выбрать все мониторы",
|
||||
"selectMonitor": "Выбрать {{name}}"
|
||||
},
|
||||
"statBoxes": {
|
||||
"activeFor": "Активен",
|
||||
|
||||
@@ -421,7 +421,10 @@
|
||||
"inviteMember": "เชิญสมาชิก",
|
||||
"openSite": "เปิดเว็บไซต์",
|
||||
"pause": "หยุดชั่วคราว",
|
||||
"resume": "ดำเนินการต่อ"
|
||||
"resume": "ดำเนินการต่อ",
|
||||
"bulkSelected": "เลือก {{count}} มอนิเตอร์แล้ว",
|
||||
"selectAll": "เลือกมอนิเตอร์ทั้งหมด",
|
||||
"selectMonitor": "เลือก {{name}}"
|
||||
},
|
||||
"statBoxes": {
|
||||
"activeFor": "ทำงานมาแล้ว",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -421,7 +421,10 @@
|
||||
"inviteMember": "Запросити учасника",
|
||||
"openSite": "Відкрити сайт",
|
||||
"pause": "Призупинити",
|
||||
"resume": "Відновити"
|
||||
"resume": "Відновити",
|
||||
"bulkSelected": "Вибрано моніторів: {{count}}",
|
||||
"selectAll": "Вибрати всі монітори",
|
||||
"selectMonitor": "Вибрати {{name}}"
|
||||
},
|
||||
"statBoxes": {
|
||||
"activeFor": "Активний протягом",
|
||||
|
||||
@@ -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ừ",
|
||||
|
||||
@@ -421,7 +421,10 @@
|
||||
"inviteMember": "邀请成员",
|
||||
"openSite": "打开站点",
|
||||
"pause": "暂停",
|
||||
"resume": "恢复"
|
||||
"resume": "恢复",
|
||||
"bulkSelected": "已选择 {{count}} 个监控",
|
||||
"selectAll": "选择所有监控",
|
||||
"selectMonitor": "选择 {{name}}"
|
||||
},
|
||||
"statBoxes": {
|
||||
"activeFor": "已活跃",
|
||||
|
||||
@@ -421,7 +421,10 @@
|
||||
"inviteMember": "邀請成員",
|
||||
"openSite": "開啟網站",
|
||||
"pause": "暫停",
|
||||
"resume": "恢復"
|
||||
"resume": "恢復",
|
||||
"bulkSelected": "已選擇 {{count}} 個監控",
|
||||
"selectAll": "選擇所有監控",
|
||||
"selectMonitor": "選擇 {{name}}"
|
||||
},
|
||||
"statBoxes": {
|
||||
"activeFor": "已啟用",
|
||||
|
||||
Reference in new issue
Block a user