From a89e5ed56508ebb303266bb895d320bb91845683 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 4 Feb 2026 19:22:05 +0000 Subject: [PATCH] MaintenanceWindow table --- .../Maintenance/MaintenanceWindowTable.tsx | 213 ++++++++++++++++++ client/src/Pages/Maintenance/index.tsx | 60 +++++ .../Pages/Maintenance/{index.jsx => old.jsx} | 0 client/src/Routes/index.jsx | 12 +- client/src/Types/MaintenanceWindow.ts | 12 + client/src/locales/en.json | 18 ++ 6 files changed, 312 insertions(+), 3 deletions(-) create mode 100644 client/src/Pages/Maintenance/MaintenanceWindowTable.tsx create mode 100644 client/src/Pages/Maintenance/index.tsx rename client/src/Pages/Maintenance/{index.jsx => old.jsx} (100%) create mode 100644 client/src/Types/MaintenanceWindow.ts diff --git a/client/src/Pages/Maintenance/MaintenanceWindowTable.tsx b/client/src/Pages/Maintenance/MaintenanceWindowTable.tsx new file mode 100644 index 000000000..19767238b --- /dev/null +++ b/client/src/Pages/Maintenance/MaintenanceWindowTable.tsx @@ -0,0 +1,213 @@ +import Typography from "@mui/material/Typography"; +import { Table, ValueLabel } from "@/Components/v2/design-elements"; +import { Pagination } from "@/Components/v2/design-elements/Table"; +import { ActionsMenu } from "@/Components/v2/actions-menu"; +import { DialogInput } from "@/Components/v2/inputs/Dialog"; + +import { useTheme } from "@mui/material"; +import type { Header } from "@/Components/v2/design-elements/Table"; +import type { ActionMenuItem } from "@/Components/v2/actions-menu"; +import type { MaintenanceWindow } from "@/Types/MaintenanceWindow"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { useSelector, useDispatch } from "react-redux"; +import type { RootState } from "@/Types/state"; +import Box from "@mui/material/Box"; +import { setRowsPerPage } from "@/Features/UI/uiSlice"; +import { formatDurationRounded } from "@/Utils/timeUtilsLegacy"; +import dayjs from "dayjs"; +import { useState } from "react"; +import { useDelete, usePatch } from "@/Hooks/UseApi"; + +interface MaintenanceWindowTableProps { + maintenanceWindows: MaintenanceWindow[]; + maintenanceWindowCount: number; + page: number; + setPage: (page: number) => void; + updateCallback: () => void; +} + +const getTimeToNextWindow = ( + startTime: string, + endTime: string, + repeat: number +): string => { + const now = dayjs(); + let start = dayjs(startTime); + let end = dayjs(endTime); + if (repeat > 0) { + while (start.isBefore(now) && end.isBefore(now)) { + start = start.add(repeat, "milliseconds"); + end = end.add(repeat, "milliseconds"); + } + } + + if (now.isAfter(start) && now.isBefore(end)) { + return "In maintenance window"; + } + + if (start.isAfter(now)) { + const diffInMinutes = start.diff(now, "minutes"); + const diffInHours = start.diff(now, "hours"); + const diffInDays = start.diff(now, "days"); + + if (diffInMinutes < 60) { + return diffInMinutes + " minutes"; + } else if (diffInHours < 24) { + return diffInHours + " hours"; + } else { + return diffInDays + " days"; + } + } + + return "N/A"; +}; + +export const MaintenanceWindowTable = ({ + maintenanceWindows, + maintenanceWindowCount, + page, + setPage, + updateCallback, +}: MaintenanceWindowTableProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + const navigate = useNavigate(); + const dispatch = useDispatch(); + const rowsPerPage = useSelector( + (state: RootState) => state?.ui?.maintenance?.rowsPerPage ?? 5 + ); + + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [selectedWindow, setSelectedWindow] = useState(null); + + const { deleteFn, loading: deleteLoading } = useDelete(); + const { patch } = usePatch(); + + const handleDelete = async () => { + if (!selectedWindow) return; + const result = await deleteFn(`/maintenance-window/${selectedWindow.id}`); + if (result) { + updateCallback(); + setDeleteDialogOpen(false); + setSelectedWindow(null); + } + }; + + const handlePause = async (maintenanceWindow: MaintenanceWindow) => { + const result = await patch(`/maintenance-window/${maintenanceWindow.id}`, { + active: !maintenanceWindow.active, + }); + if (result) { + updateCallback(); + } + }; + + const getActions = (maintenanceWindow: MaintenanceWindow): ActionMenuItem[] => [ + { + id: "edit", + label: t("pages.common.monitors.actions.configure"), + action: () => navigate(`/maintenance/create/${maintenanceWindow.id}`), + closeMenu: true, + }, + { + id: "pause", + label: maintenanceWindow.active + ? t("pages.common.monitors.actions.pause") + : t("pages.common.monitors.actions.resume"), + action: () => handlePause(maintenanceWindow), + closeMenu: true, + }, + { + id: "remove", + label: ( + + {t("pages.common.monitors.actions.delete")} + + ), + action: () => { + setSelectedWindow(maintenanceWindow); + setDeleteDialogOpen(true); + }, + closeMenu: true, + }, + ]; + + const getHeaders = (): Header[] => [ + { + id: "name", + content: t("common.table.headers.name"), + render: (row) => row.name, + }, + { + id: "status", + content: t("common.table.headers.status"), + render: (row) => ( + + ), + }, + { + id: "nextWindow", + content: t("pages.maintenanceWindow.table.headers.nextWindow"), + render: (row) => getTimeToNextWindow(row.start, row.end, row.repeat), + }, + { + id: "repeat", + content: t("pages.maintenanceWindow.table.headers.repeat"), + render: (row) => + row.repeat === 0 ? t("common.labels.na") : formatDurationRounded(row.repeat), + }, + { + id: "actions", + content: t("common.table.headers.actions"), + render: (row) => , + }, + ]; + + const handlePageChange = ( + _e: React.MouseEvent | null, + newPage: number + ) => { + setPage(newPage); + }; + + const handleRowsPerPageChange = ( + e: React.ChangeEvent + ) => { + dispatch(setRowsPerPage({ value: Number(e.target.value), table: "maintenance" })); + setPage(0); + }; + + return ( + + navigate(`/maintenance/create/${row.id}`)} + emptyViewText={t("common.table.empty")} + /> + + { + setDeleteDialogOpen(false); + setSelectedWindow(null); + }} + onConfirm={handleDelete} + confirmText={t("delete")} + loading={deleteLoading} + /> + + ); +}; diff --git a/client/src/Pages/Maintenance/index.tsx b/client/src/Pages/Maintenance/index.tsx new file mode 100644 index 000000000..9ba72daa1 --- /dev/null +++ b/client/src/Pages/Maintenance/index.tsx @@ -0,0 +1,60 @@ +import { BasePageWithStates } from "@/Components/v2/design-elements"; +import { useTranslation } from "react-i18next"; +import { useGet } from "@/Hooks/UseApi"; +import { MaintenanceWindowTable } from "./MaintenanceWindowTable"; +import { useState, useCallback } from "react"; +import { useSelector } from "react-redux"; +import type { RootState } from "@/Types/state"; +import type { MaintenanceWindow } from "@/Types/MaintenanceWindow"; + +interface MaintenanceWindowsResponse { + maintenanceWindows: MaintenanceWindow[]; + maintenanceWindowCount: number; +} + +const MaintenanceWindowPage = () => { + const { t } = useTranslation(); + const [page, setPage] = useState(0); + const rowsPerPage = useSelector( + (state: RootState) => state?.ui?.maintenance?.rowsPerPage ?? 5 + ); + + const { data, isLoading, error, refetch } = useGet( + `/maintenance-window/team?page=${page}&rowsPerPage=${rowsPerPage}` + ); + + const handleUpdate = useCallback(() => { + refetch(); + }, [refetch]); + + const handlePageChange = useCallback((newPage: number) => { + setPage(newPage); + }, []); + + const maintenanceWindows = data?.maintenanceWindows ?? []; + const maintenanceWindowCount = data?.maintenanceWindowCount ?? 0; + + return ( + + + + ); +}; + +export default MaintenanceWindowPage; diff --git a/client/src/Pages/Maintenance/index.jsx b/client/src/Pages/Maintenance/old.jsx similarity index 100% rename from client/src/Pages/Maintenance/index.jsx rename to client/src/Pages/Maintenance/old.jsx diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 908d7efe1..ee08dc581 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -48,11 +48,11 @@ import Account from "../Pages/Account/index.jsx"; import EditUser from "../Pages/Account/EditUser/index.jsx"; import Settings from "../Pages/Settings"; -import Maintenance from "../Pages/Maintenance/index.jsx"; +import Maintenance from "../Pages/Maintenance"; +import CreateNewMaintenanceWindow from "../Pages/Maintenance/CreateMaintenance/index.jsx"; import ProtectedRoute from "../Components/v1/ProtectedRoute"; import RoleProtectedRoute from "../Components/v1/RoleProtectedRoute"; -import CreateNewMaintenanceWindow from "../Pages/Maintenance/CreateMaintenance/index.jsx"; import withAdminCheck from "@/Components/v1/HOC/withAdminCheck"; import BulkImport from "../Pages/Uptime/BulkImport/index.jsx"; import Logs from "../Pages/Logs/index.jsx"; @@ -303,7 +303,13 @@ const Routes = () => { } + element={ + <> + + + + + } />