From a89e5ed56508ebb303266bb895d320bb91845683 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 4 Feb 2026 19:22:05 +0000 Subject: [PATCH 1/7] 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 0000000000..19767238b5 --- /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 0000000000..9ba72daa10 --- /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 908d7efe11..ee08dc581f 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={ + <> + + + + + } /> Date: Wed, 4 Feb 2026 19:30:41 +0000 Subject: [PATCH 2/7] put -> patch --- .../src/Components/v2/design-elements/BasePage.tsx | 2 +- .../src/Pages/Maintenance/MaintenanceWindowTable.tsx | 8 ++++---- client/src/Pages/Maintenance/index.tsx | 12 ++---------- server/src/routes/maintenanceWindowRoute.ts | 2 +- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/client/src/Components/v2/design-elements/BasePage.tsx b/client/src/Components/v2/design-elements/BasePage.tsx index e5091091e2..4daf0cd163 100644 --- a/client/src/Components/v2/design-elements/BasePage.tsx +++ b/client/src/Components/v2/design-elements/BasePage.tsx @@ -121,7 +121,7 @@ export const BasePageWithStates = ({ }: BasePageWithStatesProps) => { const showLoading = loading && (!items || items.length === 0); - if (isEmpty(items)) { + if (!loading && isEmpty(items)) { return ( void; - updateCallback: () => void; + refetch: () => void; } const getTimeToNextWindow = ( @@ -68,7 +68,7 @@ export const MaintenanceWindowTable = ({ maintenanceWindowCount, page, setPage, - updateCallback, + refetch, }: MaintenanceWindowTableProps) => { const theme = useTheme(); const { t } = useTranslation(); @@ -88,7 +88,7 @@ export const MaintenanceWindowTable = ({ if (!selectedWindow) return; const result = await deleteFn(`/maintenance-window/${selectedWindow.id}`); if (result) { - updateCallback(); + refetch(); setDeleteDialogOpen(false); setSelectedWindow(null); } @@ -99,7 +99,7 @@ export const MaintenanceWindowTable = ({ active: !maintenanceWindow.active, }); if (result) { - updateCallback(); + refetch(); } }; diff --git a/client/src/Pages/Maintenance/index.tsx b/client/src/Pages/Maintenance/index.tsx index 9ba72daa10..5a1d29fc0a 100644 --- a/client/src/Pages/Maintenance/index.tsx +++ b/client/src/Pages/Maintenance/index.tsx @@ -23,14 +23,6 @@ const MaintenanceWindowPage = () => { `/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; @@ -50,8 +42,8 @@ const MaintenanceWindowPage = () => { maintenanceWindows={maintenanceWindows} maintenanceWindowCount={maintenanceWindowCount} page={page} - setPage={handlePageChange} - updateCallback={handleUpdate} + setPage={setPage} + refetch={refetch} /> ); diff --git a/server/src/routes/maintenanceWindowRoute.ts b/server/src/routes/maintenanceWindowRoute.ts index 0230ba4bda..539f2413f1 100755 --- a/server/src/routes/maintenanceWindowRoute.ts +++ b/server/src/routes/maintenanceWindowRoute.ts @@ -16,7 +16,7 @@ class MaintenanceWindowRoutes { this.router.get("/monitor/:monitorId", this.mwController.getMaintenanceWindowsByMonitorId); this.router.get("/:id", this.mwController.getMaintenanceWindowById); - this.router.put("/:id", this.mwController.editMaintenanceWindow); + this.router.patch("/:id", this.mwController.editMaintenanceWindow); this.router.delete("/:id", this.mwController.deleteMaintenanceWindow); } From b536099d039475725d155fdd45b4c9bcc345f873 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 4 Feb 2026 19:40:32 +0000 Subject: [PATCH 3/7] create header --- client/src/Pages/Maintenance/index.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/client/src/Pages/Maintenance/index.tsx b/client/src/Pages/Maintenance/index.tsx index 5a1d29fc0a..a197b69ab3 100644 --- a/client/src/Pages/Maintenance/index.tsx +++ b/client/src/Pages/Maintenance/index.tsx @@ -1,11 +1,13 @@ import { BasePageWithStates } from "@/Components/v2/design-elements"; +import { HeaderCreate } from "@/Components/v2/common"; import { useTranslation } from "react-i18next"; import { useGet } from "@/Hooks/UseApi"; import { MaintenanceWindowTable } from "./MaintenanceWindowTable"; -import { useState, useCallback } from "react"; +import { useState } from "react"; import { useSelector } from "react-redux"; import type { RootState } from "@/Types/state"; import type { MaintenanceWindow } from "@/Types/MaintenanceWindow"; +import { useIsAdmin } from "@/Hooks/useIsAdmin"; interface MaintenanceWindowsResponse { maintenanceWindows: MaintenanceWindow[]; @@ -14,12 +16,13 @@ interface MaintenanceWindowsResponse { const MaintenanceWindowPage = () => { const { t } = useTranslation(); + const isAdmin = useIsAdmin(); const [page, setPage] = useState(0); const rowsPerPage = useSelector( (state: RootState) => state?.ui?.maintenance?.rowsPerPage ?? 5 ); - const { data, isLoading, error, refetch } = useGet( + const { data, isLoading, isValidating, error, refetch } = useGet( `/maintenance-window/team?page=${page}&rowsPerPage=${rowsPerPage}` ); @@ -38,6 +41,11 @@ const MaintenanceWindowPage = () => { actionButtonText={t("pages.maintenanceWindow.fallback.actionButton")} actionLink="/maintenance/create" > + Date: Wed, 4 Feb 2026 19:53:19 +0000 Subject: [PATCH 4/7] pretty-ms --- .../Maintenance/MaintenanceWindowTable.tsx | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/client/src/Pages/Maintenance/MaintenanceWindowTable.tsx b/client/src/Pages/Maintenance/MaintenanceWindowTable.tsx index b3033e9ddc..9922c9cc6c 100644 --- a/client/src/Pages/Maintenance/MaintenanceWindowTable.tsx +++ b/client/src/Pages/Maintenance/MaintenanceWindowTable.tsx @@ -1,4 +1,5 @@ import Typography from "@mui/material/Typography"; +import prettyMilliseconds from "pretty-ms"; import { Table, ValueLabel } from "@/Components/v2/design-elements"; import { Pagination } from "@/Components/v2/design-elements/Table"; import { ActionsMenu } from "@/Components/v2/actions-menu"; @@ -35,29 +36,23 @@ const getTimeToNextWindow = ( const now = dayjs(); let start = dayjs(startTime); let end = dayjs(endTime); + + // For repeating windows, advance to next occurrence if (repeat > 0) { - while (start.isBefore(now) && end.isBefore(now)) { + while (end.isBefore(now)) { start = start.add(repeat, "milliseconds"); end = end.add(repeat, "milliseconds"); } } + // Currently in maintenance window if (now.isAfter(start) && now.isBefore(end)) { return "In maintenance window"; } + // Window is in the future 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 prettyMilliseconds(start.diff(now), { unitCount: 2, hideSeconds: true }); } return "N/A"; From c54582ca7078d882c72c9e7cfc50522385317b9c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 4 Feb 2026 19:54:01 +0000 Subject: [PATCH 5/7] format --- client/package-lock.json | 2 +- client/src/Pages/Maintenance/index.tsx | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 1a59d98906..d289d83129 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -6203,4 +6203,4 @@ } } } -} \ No newline at end of file +} diff --git a/client/src/Pages/Maintenance/index.tsx b/client/src/Pages/Maintenance/index.tsx index a197b69ab3..09b2a9ae7e 100644 --- a/client/src/Pages/Maintenance/index.tsx +++ b/client/src/Pages/Maintenance/index.tsx @@ -22,9 +22,10 @@ const MaintenanceWindowPage = () => { (state: RootState) => state?.ui?.maintenance?.rowsPerPage ?? 5 ); - const { data, isLoading, isValidating, error, refetch } = useGet( - `/maintenance-window/team?page=${page}&rowsPerPage=${rowsPerPage}` - ); + const { data, isLoading, isValidating, error, refetch } = + useGet( + `/maintenance-window/team?page=${page}&rowsPerPage=${rowsPerPage}` + ); const maintenanceWindows = data?.maintenanceWindows ?? []; const maintenanceWindowCount = data?.maintenanceWindowCount ?? 0; From 7cb866ce4a0e9d1fad67428d884ee48428c820f4 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 4 Feb 2026 19:57:36 +0000 Subject: [PATCH 6/7] legacy put -> patch --- client/src/Utils/NetworkService.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/client/src/Utils/NetworkService.js b/client/src/Utils/NetworkService.js index 9c6ad74a06..d9b151a103 100644 --- a/client/src/Utils/NetworkService.js +++ b/client/src/Utils/NetworkService.js @@ -230,7 +230,7 @@ class NetworkService { async updateMonitor(config) { const { monitorId, updatedFields } = config; const payload = updatedFields; - return this.axiosInstance.put(`/monitors/${monitorId}`, payload, { + return this.axiosInstance.patch(`/monitors/${monitorId}`, payload, { headers: { "Content-Type": "application/json", }, @@ -378,7 +378,7 @@ class NetworkService { * */ async updateUser(config) { - return this.axiosInstance.put(`/auth/user`, config.form); + return this.axiosInstance.patch(`/auth/user`, config.form); } /** @@ -611,7 +611,7 @@ class NetworkService { * */ async updateCheckStatus(config) { - return this.axiosInstance.put(`/checks/check/${config.checkId}`, { + return this.axiosInstance.patch(`/checks/check/${config.checkId}`, { ack: config.ack, }); } @@ -629,7 +629,7 @@ class NetworkService { * */ async updateMonitorChecksStatus(config) { - return this.axiosInstance.put(`/checks/monitor/${config.monitorId}`, { + return this.axiosInstance.patch(`/checks/monitor/${config.monitorId}`, { ack: config.ack, }); } @@ -646,7 +646,7 @@ class NetworkService { * */ async updateAllChecksStatus(config) { - return this.axiosInstance.put(`/checks/team/`, { + return this.axiosInstance.patch(`/checks/team/`, { ack: config.ack, }); } @@ -663,7 +663,7 @@ class NetworkService { * */ async updateChecksTTL(config) { - return this.axiosInstance.put( + return this.axiosInstance.patch( `/checks/team/ttl`, { ttl: config.ttl }, { @@ -705,7 +705,7 @@ class NetworkService { * @returns {Promise} The response from the axios POST request. */ async updateAppSettings(config) { - return this.axiosInstance.put(`/settings`, config.settings, { + return this.axiosInstance.patch(`/settings`, config.settings, { headers: { "Content-Type": "application/json", }, @@ -775,7 +775,7 @@ class NetworkService { */ async editMaintenanceWindow(config) { - return this.axiosInstance.put( + return this.axiosInstance.patch( `/maintenance-window/${config.maintenanceWindowId}`, config.maintenanceWindow, { @@ -955,7 +955,7 @@ class NetworkService { return this.axiosInstance.post(`/status-page`, fd, {}); } - return this.axiosInstance.put(`/status-page/${id}`, fd, {}); + return this.axiosInstance.patch(`/status-page/${id}`, fd, {}); } async deleteStatusPage(config) { @@ -1125,7 +1125,7 @@ class NetworkService { async editUser(config) { const { userId, user } = config; - return this.axiosInstance.put(`auth/users/${userId}`, user, { + return this.axiosInstance.patch(`auth/users/${userId}`, user, { headers: { "Content-Type": "application/json", }, @@ -1134,7 +1134,7 @@ class NetworkService { async changePasswordByAdmin(config) { const { userId, passwordForm } = config; - return this.axiosInstance.put(`auth/users/${userId}/password`, passwordForm, { + return this.axiosInstance.patch(`auth/users/${userId}/password`, passwordForm, { headers: { "Content-Type": "application/json", }, @@ -1164,7 +1164,7 @@ class NetworkService { resolveIncidentManually = async (incidentId, options = {}) => { const body = {}; if (options.comment) body.comment = options.comment; - return this.axiosInstance.put(`/incidents/${incidentId}/resolve`, body); + return this.axiosInstance.patch(`/incidents/${incidentId}/resolve`, body); }; getIncidentSummary = async (config = {}) => { From 1a096b66f7559c6c0c4ca9e2ba92302bb051de2c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 4 Feb 2026 20:03:05 +0000 Subject: [PATCH 7/7] remove unused --- .../MaintenanceTable/ActionsMenu/index.jsx | 178 ------------- .../Maintenance/MaintenanceTable/index.jsx | 237 ------------------ client/src/Pages/Maintenance/index.css | 0 client/src/Pages/Maintenance/old.jsx | 95 ------- 4 files changed, 510 deletions(-) delete mode 100644 client/src/Pages/Maintenance/MaintenanceTable/ActionsMenu/index.jsx delete mode 100644 client/src/Pages/Maintenance/MaintenanceTable/index.jsx delete mode 100644 client/src/Pages/Maintenance/index.css delete mode 100644 client/src/Pages/Maintenance/old.jsx diff --git a/client/src/Pages/Maintenance/MaintenanceTable/ActionsMenu/index.jsx b/client/src/Pages/Maintenance/MaintenanceTable/ActionsMenu/index.jsx deleted file mode 100644 index 709bca2123..0000000000 --- a/client/src/Pages/Maintenance/MaintenanceTable/ActionsMenu/index.jsx +++ /dev/null @@ -1,178 +0,0 @@ -import { useState } from "react"; -import { useTheme } from "@emotion/react"; -import { useNavigate } from "react-router-dom"; -import { useSelector } from "react-redux"; -import { IconButton, Menu, MenuItem } from "@mui/material"; -import { logger } from "@/Utils/Logger.js"; -import Icon from "@/Components/v1/Icon"; -import PropTypes from "prop-types"; -import { networkService } from "../../../../main.jsx"; -import { createToast } from "@/Utils/toastUtils.jsx"; -import { useTranslation } from "react-i18next"; - -import Dialog from "@/Components/v1/Dialog/index.jsx"; - -const ActionsMenu = ({ /* isAdmin, */ maintenanceWindow, updateCallback }) => { - maintenanceWindow; - const [anchorEl, setAnchorEl] = useState(null); - const [isOpen, setIsOpen] = useState(false); - const [isLoading, setIsLoading] = useState(false); - - const theme = useTheme(); - const { t } = useTranslation(); - - const handleRemove = async (event) => { - event.preventDefault(); - event.stopPropagation(); - try { - setIsLoading(true); - await networkService.deleteMaintenanceWindow({ - maintenanceWindowId: maintenanceWindow.id, - }); - updateCallback(); - createToast({ body: "Maintenance window deleted successfully." }); - } catch (error) { - createToast({ body: "Failed to delete maintenance window." }); - logger.error("Failed to delete maintenance window", error); - } finally { - setIsLoading(false); - } - setIsOpen(false); - }; - - const handlePause = async () => { - try { - setIsLoading(true); - const data = { - active: !maintenanceWindow.active, - }; - await networkService.editMaintenanceWindow({ - maintenanceWindowId: maintenanceWindow.id, - maintenanceWindow: data, - }); - updateCallback(); - } catch (error) { - logger.error(error); - createToast({ body: "Failed to pause maintenance window." }); - } finally { - setIsLoading(false); - } - }; - - const handleEdit = () => { - navigate(`/maintenance/create/${maintenanceWindow.id}`); - }; - - const openMenu = (event) => { - event.preventDefault(); - event.stopPropagation(); - setAnchorEl(event.currentTarget); - }; - - const openRemove = (e) => { - closeMenu(e); - setIsOpen(true); - }; - - const closeMenu = (e) => { - e.stopPropagation(); - setAnchorEl(null); - }; - - const navigate = useNavigate(); - - return ( - <> - { - event.stopPropagation(); - openMenu(event); - }} - sx={{ - "&:focus": { - outline: "none", - }, - "& svg path": { - stroke: theme.palette.primary.contrastTextTertiary, - }, - }} - > - - - - closeMenu(e)} - disableScrollLock - slotProps={{ - paper: { - sx: { - "& ul": { p: theme.spacing(2.5) }, - "& li": { m: 0 }, - "& li:last-of-type": { - color: theme.palette.error.main, - }, - }, - }, - }} - > - { - closeMenu(e); - e.stopPropagation(); - handleEdit(); - }} - > - {t("edit")} - - { - handlePause(); - closeMenu(e); - e.stopPropagation(); - }} - > - {`${maintenanceWindow.active === true ? t("pause") : t("resume")}`} - - - { - e.stopPropagation(); - openRemove(e); - }} - > - {t("remove")} - - - { - e.stopPropagation(); - setIsOpen(false); - }} - confirmationButtonLabel={t("delete")} - onConfirm={(e) => { - e.stopPropagation(e); - handleRemove(e); - }} - isLoading={isLoading} - /> - - ); -}; - -ActionsMenu.propTypes = { - maintenanceWindow: PropTypes.object, - isAdmin: PropTypes.bool, - updateCallback: PropTypes.func, -}; - -export default ActionsMenu; diff --git a/client/src/Pages/Maintenance/MaintenanceTable/index.jsx b/client/src/Pages/Maintenance/MaintenanceTable/index.jsx deleted file mode 100644 index 11875bb6ad..0000000000 --- a/client/src/Pages/Maintenance/MaintenanceTable/index.jsx +++ /dev/null @@ -1,237 +0,0 @@ -import PropTypes from "prop-types"; -import { Box } from "@mui/material"; -import DataTable from "@/Components/v1/Table/index.jsx"; -import Pagination from "@/Components/v1/Table/TablePagination/index.jsx"; -import Icon from "@/Components/v1/Icon"; -import ActionsMenu from "./ActionsMenu/index.jsx"; -import { memo } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import { formatDurationRounded } from "../../../Utils/timeUtilsLegacy.js"; -import { StatusLabel } from "@/Components/v1/Label/index.jsx"; -import { setRowsPerPage } from "../../../Features/UI/uiSlice.js"; -import { useTranslation } from "react-i18next"; -import { useTheme } from "@emotion/react"; -import { useNavigate } from "react-router-dom"; -import dayjs from "dayjs"; -/** - * Component for pagination actions (first, previous, next, last). - * - * @component - * @param {Object} props - * @param {number} props.count - Total number of items. - * @param {number} props.page - Current page number. - * @param {number} props.rowsPerPage - Number of rows per page. - * @param {function} props.onPageChange - Callback function to handle page change. - * - * @returns {JSX.Element} Pagination actions component. - */ - -const MaintenanceTable = ({ - page, - setPage, - sort, - setSort, - maintenanceWindows, - maintenanceWindowCount, - updateCallback, -}) => { - const rowsPerPage = useSelector((state) => state?.ui?.maintenance?.rowsPerPage ?? 5); - const dispatch = useDispatch(); - const theme = useTheme(); - const navigate = useNavigate(); - const handleChangePage = (event, newPage) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (event) => { - dispatch( - setRowsPerPage({ - value: parseInt(event.target.value, 10), - table: "maintenance", - }) - ); - setPage(0); - }; - - const { t } = useTranslation(); - - const headers = [ - { - id: "name", - content: ( - handleSort("name")}> - {t("maintenanceWindowName")} - - {sort.order === "asc" ? ( - - ) : ( - - )} - - - ), - render: (row) => row.name, - }, - { - id: "status", - content: ( - handleSort("status")}> - {" "} - {t("status")} - - {sort.order === "asc" ? ( - - ) : ( - - )} - - - ), - render: (row) => { - const status = row.active ? "up" : "paused"; - const text = row.active ? "active" : "paused"; - - return ( - - ); - }, - }, - { - id: "nextWindow", - content: t("nextWindow"), - render: (row) => { - return getTimeToNextWindow(row.start, row.end, row.repeat); - }, - }, - { - id: "repeat", - content: t("repeat"), - render: (row) => { - return row.repeat === 0 ? "N/A" : formatDurationRounded(row.repeat); - }, - }, - { - id: "actions", - content: t("actions"), - render: (row) => ( - - ), - }, - ]; - - const getTimeToNextWindow = (startTime, endTime, repeat) => { - //1. Advance time closest to next window as possible - const now = dayjs(); - let start = dayjs(startTime); - let end = dayjs(endTime); - if (repeat > 0) { - // Advance time closest to next window as possible - while (start.isBefore(now) && end.isBefore(now)) { - start = start.add(repeat, "milliseconds"); - end = end.add(repeat, "milliseconds"); - } - } - - //Check if we are in a window - 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 if (diffInDays < 7) { - return diffInDays + " days"; - } else { - return diffInDays + " days"; - } - } - }; - - const handleSort = async (field) => { - let order = ""; - if (sort.field !== field) { - order = "desc"; - } else { - order = sort.order === "asc" ? "desc" : "asc"; - } - setSort({ field, order }); - }; - - return ( - <> - { - navigate(`/maintenance/create/${row.id}`); - }, - }} - headers={headers} - data={maintenanceWindows} - /> - - - ); -}; - -MaintenanceTable.propTypes = { - isAdmin: PropTypes.bool, - page: PropTypes.number, - setPage: PropTypes.func, - rowsPerPage: PropTypes.number, - setRowsPerPage: PropTypes.func, - sort: PropTypes.object, - setSort: PropTypes.func, - maintenanceWindows: PropTypes.array, - maintenanceWindowCount: PropTypes.number, - updateCallback: PropTypes.func, -}; - -const MemoizedMaintenanceTable = memo(MaintenanceTable); -export default MemoizedMaintenanceTable; diff --git a/client/src/Pages/Maintenance/index.css b/client/src/Pages/Maintenance/index.css deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/client/src/Pages/Maintenance/old.jsx b/client/src/Pages/Maintenance/old.jsx deleted file mode 100644 index b49cd9f6eb..0000000000 --- a/client/src/Pages/Maintenance/old.jsx +++ /dev/null @@ -1,95 +0,0 @@ -import { Stack, Button } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import { useState, useEffect } from "react"; -import "./index.css"; -import MaintenanceTable from "./MaintenanceTable/index.jsx"; -import { useSelector } from "react-redux"; -import { networkService } from "../../main.jsx"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import { useNavigate } from "react-router-dom"; -import { useTranslation } from "react-i18next"; -import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx"; - -const Maintenance = () => { - const theme = useTheme(); - const { t } = useTranslation(); - const navigate = useNavigate(); - const rowsPerPage = useSelector((state) => state?.ui?.maintenance?.rowsPerPage ?? 5); - const [maintenanceWindows, setMaintenanceWindows] = useState(undefined); - const [maintenanceWindowCount, setMaintenanceWindowCount] = useState(0); - const [page, setPage] = useState(0); - const [sort, setSort] = useState({}); - const [updateTrigger, setUpdateTrigger] = useState(false); - const [networkError, setNetworkError] = useState(false); - const [isLoading, setIsLoading] = useState(true); - - const handleActionMenuDelete = () => { - setUpdateTrigger((prev) => !prev); - }; - - useEffect(() => { - const fetchMaintenanceWindows = async () => { - try { - setNetworkError(false); - setIsLoading(true); - const response = await networkService.getMaintenanceWindowsByTeamId({ - page: page, - rowsPerPage: rowsPerPage, - }); - const { maintenanceWindows, maintenanceWindowCount } = response.data.data; - setMaintenanceWindows(maintenanceWindows ?? []); - setMaintenanceWindowCount(maintenanceWindowCount ?? 0); - } catch (error) { - setNetworkError(true); - } finally { - setIsLoading(false); - } - }; - fetchMaintenanceWindows(); - }, [page, rowsPerPage, updateTrigger]); - - return ( - <> - - - - - - - - - - - ); -}; - -export default Maintenance;