remove monitorHooks

This commit is contained in:
Alex Holliday
2026-02-09 23:16:14 +00:00
parent 3d1bd94279
commit 2091d8dcd7
7 changed files with 48 additions and 609 deletions
@@ -1,228 +0,0 @@
// Components
import IconButton from "@mui/material/IconButton";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import Icon from "../Icon";
import Dialog from "../Dialog/index.jsx";
// Utils
import { useState } from "react";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import { createToast } from "../../../Utils/toastUtils.jsx";
import PropTypes from "prop-types";
import { usePauseMonitor, useDeleteMonitor } from "../../../Hooks/monitorHooks.js";
const ActionsMenu = ({
monitor,
isAdmin,
updateRowCallback,
pauseCallback,
setIsLoading = () => {},
}) => {
const [anchorEl, setAnchorEl] = useState(null);
const [actions, setActions] = useState({});
const [isOpen, setIsOpen] = useState(false);
const theme = useTheme();
const [pauseMonitor, isPausing, error] = usePauseMonitor();
const [deleteMonitor, isDeleting] = useDeleteMonitor();
const handleRemove = async (event) => {
event.preventDefault();
event.stopPropagation();
let monitor = { id: actions.id };
await deleteMonitor({ monitor });
updateRowCallback();
};
const handlePause = async () => {
try {
setIsLoading(true);
await pauseMonitor({ monitorId: monitor.id });
pauseCallback();
} catch (error) {
createToast({ body: "Failed to pause monitor." });
} finally {
setIsLoading(false);
}
};
const openMenu = (event, id, url) => {
event.preventDefault();
event.stopPropagation();
setAnchorEl(event.currentTarget);
setActions({ id: id, url: url });
};
const openRemove = (e) => {
closeMenu(e);
setIsOpen(true);
};
const closeMenu = (e) => {
e.stopPropagation();
setAnchorEl(null);
};
const navigate = useNavigate();
return (
<>
<IconButton
aria-label="monitor actions"
onClick={(event) => {
event.stopPropagation();
openMenu(event, monitor.id, monitor.type === "ping" ? null : monitor.url);
}}
sx={{
"&:focus": {
outline: "none",
},
"& svg path": {
stroke: theme.palette.primary.contrastTextTertiary,
},
}}
>
<Icon
name="Settings"
size={20}
/>
</IconButton>
<Menu
className="actions-menu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={(e) => closeMenu(e)}
disableScrollLock
slotProps={{
paper: {
sx: {
"& ul": {
p: theme.spacing(2.5),
backgroundColor: theme.palette.primary.main,
},
"& li": { m: 0, color: theme.palette.primary.contrastTextSecondary },
/*
This should not be set automatically on the last of type
"& li:last-of-type": {
color: theme.palette.error.main,
}, */
},
},
}}
>
{actions.url !== null ? (
<MenuItem
onClick={(e) => {
closeMenu(e);
e.stopPropagation();
window.open(actions.url, "_blank", "noreferrer");
}}
>
Open site
</MenuItem>
) : (
""
)}
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/uptime/${actions.id}`);
}}
>
Details
</MenuItem>
{/* TODO - pass monitor id to Incidents page */}
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/incidents/${actions.id}`);
}}
>
Incidents
</MenuItem>
{isAdmin && (
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/uptime/configure/${actions.id}`);
}}
>
Configure
</MenuItem>
)}
{isAdmin && (
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/uptime/create/${actions.id}`);
}}
>
Clone
</MenuItem>
)}
{isAdmin && (
<MenuItem
onClick={(e) => {
closeMenu(e);
e.stopPropagation();
handlePause(e);
}}
>
{monitor?.isActive === true ? "Pause" : "Resume"}
</MenuItem>
)}
{isAdmin && (
<MenuItem
onClick={(e) => {
e.stopPropagation();
openRemove(e);
}}
sx={{ "&.MuiButtonBase-root": { color: theme.palette.error.main } }}
>
Remove
</MenuItem>
)}
</Menu>
<Dialog
open={isOpen}
theme={theme}
title="Do you really want to delete this monitor?"
description="Once deleted, this monitor cannot be retrieved."
/* Do we need stop propagation? */
onCancel={(e) => {
e.stopPropagation();
setIsOpen(false);
}}
confirmationButtonLabel="Delete"
/* Do we need stop propagation? */
onConfirm={(e) => {
console.log(e);
e.stopPropagation();
handleRemove(e);
}}
isLoading={isDeleting}
modelTitle="modal-delete-monitor"
modelDescription="delete-monitor-confirmation"
/>
</>
);
};
ActionsMenu.propTypes = {
monitor: PropTypes.shape({
id: PropTypes.string,
url: PropTypes.string,
type: PropTypes.string,
isActive: PropTypes.bool,
}).isRequired,
isAdmin: PropTypes.bool,
updateRowCallback: PropTypes.func,
pauseCallback: PropTypes.func,
setIsLoading: PropTypes.func,
};
export default ActionsMenu;
+34
View File
@@ -179,3 +179,37 @@ export const useDelete = <R = any>() => {
return { deleteFn, loading, error };
};
export const useLazyGet = <R = any>() => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { toastError } = useToast();
const getFn = async (
endpoint: string,
config?: AxiosRequestConfig
): Promise<ApiResponse<R> | null> => {
setLoading(true);
setError(null);
try {
const res = await get<ApiResponse<R>>(endpoint, {
...config,
headers: {
...config?.headers,
},
});
return res.data;
} catch (err: any) {
console.error(err);
const errMsg = err?.response?.data?.msg || err.message || "An error occurred";
toastError(errMsg);
setError(errMsg);
return null;
} finally {
setLoading(false);
}
};
return { get: getFn, loading, error };
};
-170
View File
@@ -1,170 +0,0 @@
import { useEffect, useState } from "react";
import { networkService } from "../main.jsx";
import { createToast } from "../Utils/toastUtils.jsx";
import { useTranslation } from "react-i18next";
export const useFetchMonitorsByTeamId = ({ types, filter, updateTrigger }) => {
const [isLoading, setIsLoading] = useState(false);
const [monitors, setMonitors] = useState(undefined);
const [networkError, setNetworkError] = useState(false);
useEffect(() => {
const fetchMonitors = async () => {
try {
setIsLoading(true);
const res = await networkService.getMonitorsByTeamId({
types,
filter,
});
if (res?.data?.data) {
setMonitors(res.data.data);
}
} catch (error) {
setNetworkError(true);
createToast({
body: error.message,
});
} finally {
setIsLoading(false);
}
};
fetchMonitors();
}, [types, filter, updateTrigger]);
return [monitors, isLoading, networkError];
};
export const useDeleteMonitor = () => {
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const deleteMonitor = async (monitorId, successCallback = () => {}) => {
try {
setIsLoading(true);
await networkService.deleteMonitorById({ monitorId });
successCallback();
createToast({ body: t("monitorDeleted") });
} catch (error) {
createToast({ body: t("failedDeleteMonitor") });
} finally {
setIsLoading(false);
}
};
return [deleteMonitor, isLoading];
};
export const usePauseMonitor = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const { t } = useTranslation();
const pauseMonitor = async (monitorId, successCallback = () => {}) => {
try {
setIsLoading(true);
const res = await networkService.pauseMonitorById({ monitorId });
successCallback();
if (res.data.data.isActive === false) {
createToast({ body: t("monitorPaused") });
} else {
createToast({ body: t("monitorResumed") });
}
} catch (error) {
setError(error.message);
createToast({ body: t("failedPauseMonitor") });
} finally {
setIsLoading(false);
}
};
return [pauseMonitor, isLoading, error];
};
export const useAddDemoMonitors = () => {
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const addDemoMonitors = async () => {
try {
setIsLoading(true);
await networkService.addDemoMonitors();
createToast({ body: t("monitorHooks.successAddDemoMonitors") });
} catch (error) {
createToast({ body: t("monitorHooks.failureAddDemoMonitors") });
} finally {
setIsLoading(false);
}
};
return [addDemoMonitors, isLoading];
};
export const useDeleteAllMonitors = () => {
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const deleteAllMonitors = async () => {
try {
setIsLoading(true);
await networkService.deleteAllMonitors();
createToast({ body: t("settingsMonitorsDeleted") });
} catch (error) {
createToast({ body: t("settingsFailedToDeleteMonitors") });
} finally {
setIsLoading(false);
}
};
return [deleteAllMonitors, isLoading];
};
export const useDeleteMonitorStats = () => {
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const deleteMonitorStats = async () => {
setIsLoading(true);
try {
await networkService.deleteChecksByTeamId();
createToast({ body: t("settingsStatsCleared") });
} catch (error) {
createToast({ body: t("settingsFailedToClearStats") });
} finally {
setIsLoading(false);
}
};
return [deleteMonitorStats, isLoading];
};
export const useCreateBulkMonitors = () => {
const [isLoading, setIsLoading] = useState(false);
const createBulkMonitors = async (file, user) => {
setIsLoading(true);
const formData = new FormData();
formData.append("csvFile", file);
try {
const response = await networkService.createBulkMonitors(formData);
return [true, response.data, null];
} catch (err) {
const errorMessage = err?.response?.data?.msg || err.message;
return [false, null, errorMessage];
} finally {
setIsLoading(false);
}
};
return [createBulkMonitors, isLoading];
};
export const useFetchJson = () => {
const [isLoading, setIsLoading] = useState(false);
const fetchJson = async () => {
try {
setIsLoading(true);
const res = await networkService.fetchJson();
createToast({ body: "JSON fetched successfully" });
return res?.data?.data ?? [];
} catch (error) {
createToast({ body: "Failed to create monitor." });
} finally {
setIsLoading(false);
}
};
return [fetchJson, isLoading];
};
+13 -16
View File
@@ -26,14 +26,9 @@ import {
} from "@/Features/UI/uiSlice.js";
import SettingsStats from "./SettingsStats.jsx";
import { useFetchSettings, useSaveSettings } from "@/Hooks/settingsHooks.js";
import { useFetchSettings, useSaveSettings } from "@/Hooks/settingsHooks";
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
import {
useAddDemoMonitors,
useDeleteAllMonitors,
useDeleteMonitorStats,
useFetchJson,
} from "@/Hooks/monitorHooks.js";
import { usePost, useDelete, useLazyGet } from "@/Hooks/UseApi";
// Constants
const BREADCRUMBS = [{ name: `Settings`, path: "/settings" }];
@@ -64,8 +59,6 @@ const Settings = () => {
setIsEmailPasswordSet,
});
const [addDemoMonitors, isAddingDemoMonitors] = useAddDemoMonitors();
const [isSaving, saveError, saveSettings] = useSaveSettings({
setSettingsData,
setIsApiKeySet,
@@ -73,9 +66,12 @@ const Settings = () => {
setIsEmailPasswordSet,
setEmailPasswordHasBeenReset,
});
const [deleteAllMonitors, isDeletingMonitors] = useDeleteAllMonitors();
const [deleteMonitorStats, isDeletingMonitorStats] = useDeleteMonitorStats();
const [fetchJson, isFetchingJson] = useFetchJson();
// New API hooks to replace monitorHooks
const { post: postDemoMonitors, loading: isAddingDemoMonitors } = usePost();
const { deleteFn: deleteAllMonitorsFn, loading: isDeletingMonitors } = useDelete();
const { deleteFn: deleteMonitorStatsFn, loading: isDeletingMonitorStats } = useDelete();
const { get: fetchJson, loading: isFetchingJson } = useLazyGet();
// Setup
const isAdmin = useIsAdmin();
@@ -130,22 +126,23 @@ const Settings = () => {
}
if (name === "deleteStats") {
await deleteMonitorStats();
await deleteMonitorStatsFn("/checks/team");
return;
}
if (name === "demo") {
await addDemoMonitors();
await postDemoMonitors("/monitors/demo", {});
return;
}
if (name === "deleteMonitors") {
await deleteAllMonitors();
await deleteAllMonitorsFn("/monitors/");
return;
}
if (name === "export") {
const json = await fetchJson();
const res = await fetchJson("/monitors/export/json");
const json = res?.data ?? [];
if (!json || json.length === 0) {
return;
}
@@ -1,73 +0,0 @@
import { useTheme } from "@emotion/react";
import { useState, useRef } from "react";
import { Button, Typography } from "@mui/material";
import { useTranslation } from "react-i18next";
import PropTypes from "prop-types";
const UploadFile = ({ onFileSelect }) => {
// Changed prop to onFileSelect
const theme = useTheme();
const [file, setFile] = useState();
const [error, setError] = useState("");
const inputRef = useRef();
const { t } = useTranslation();
const handleSelectFile = () => {
inputRef.current.click();
};
const handleFileChange = (e) => {
setError("");
const selectedFile = e.target.files[0];
// Basic file validation
if (!selectedFile) return;
if (!selectedFile.name.toLowerCase().endsWith(".csv")) {
setError(t("bulkImport.invalidFileType"));
return;
}
setFile(selectedFile);
onFileSelect(selectedFile); // Pass the file directly to parent
};
return (
<div>
<input
ref={inputRef}
type="file"
accept=".csv"
style={{ display: "none" }}
onChange={handleFileChange}
/>
<Typography
component="h2"
mb={theme.spacing(1.5)}
sx={{ wordBreak: "break-all" }}
>
{file?.name || t("bulkImport.noFileSelected")}
</Typography>
<Typography
component="div"
mb={theme.spacing(1.5)}
color={theme.palette.error.main}
>
{error}
</Typography>
<Button
variant="contained"
color="accent"
onClick={handleSelectFile}
>
{t("bulkImport.selectFile")}
</Button>
</div>
);
};
UploadFile.prototype = {
onFileSelect: PropTypes.func.isRequired,
};
export default UploadFile;
@@ -1,117 +0,0 @@
// React, Redux, Router
import { useTheme } from "@emotion/react";
import { useState } from "react";
// MUI
import { Box, Stack, Typography, Button, Link } from "@mui/material";
//Components
import { createToast } from "../../../Utils/toastUtils.jsx";
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
import UploadFile from "./Upload.jsx";
import { useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { Trans, useTranslation } from "react-i18next";
import { useCreateBulkMonitors } from "../../../Hooks/monitorHooks.js";
const BulkImport = () => {
const theme = useTheme();
const { user } = useSelector((state) => state.auth);
const navigate = useNavigate();
const { t } = useTranslation();
const [selectedFile, setSelectedFile] = useState(null);
const crumbs = [
{ name: t("uptime"), path: "/uptime" },
{ name: t("bulkImport.title"), path: `/uptime/bulk-import` },
];
const [createBulkMonitors, hookLoading] = useCreateBulkMonitors();
const handleSubmit = async () => {
if (!selectedFile) {
createToast({ body: t("bulkImport.noFileSelected") });
return;
}
const [success, data, error] = await createBulkMonitors(selectedFile, user);
if (success) {
// You can use `data` here if needed
createToast({ body: t("bulkImport.uploadSuccess") });
navigate("/uptime");
} else {
createToast({ body: error || t("bulkImport.uploadFailed") });
}
};
return (
<Box className="bulk-import-monitor">
<Breadcrumbs list={crumbs} />
<Stack
component="form"
gap={theme.spacing(12)}
mt={theme.spacing(6)}
>
<Typography
component="h1"
variant="h1"
>
{t("bulkImport.title")}
</Typography>
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("bulkImport.selectFileTips")}
</Typography>
<Typography component="p">
<Trans
i18nKey="bulkImport.selectFileDescription"
components={{
template: (
<Link
color="info"
download
href="/bulk_import_monitors_template.csv"
/>
),
sample: (
<Link
color="info"
download
href="/bulk_import_monitors_sample.csv"
/>
),
}}
/>
</Typography>
</Box>
<Stack gap={theme.spacing(12)}>
<Stack gap={theme.spacing(6)}>
<UploadFile onFileSelect={(file) => setSelectedFile(file)} />
</Stack>
</Stack>
</ConfigBox>
<Stack
direction="row"
justifyContent="flex-end"
>
<Button
variant="contained"
color="accent"
onClick={handleSubmit}
disabled={hookLoading}
loading={hookLoading}
>
{t("submit")}
</Button>
</Stack>
</Stack>
</Box>
);
};
export default BulkImport;
+1 -5
View File
@@ -53,7 +53,6 @@ import CreateNewMaintenanceWindow from "@/Pages/Maintenance/create";
import ProtectedRoute from "../Components/v1/ProtectedRoute";
import RoleProtectedRoute from "../Components/v1/RoleProtectedRoute";
import withAdminCheck from "@/Components/v1/HOC/withAdminCheck";
import BulkImport from "../Pages/Uptime/BulkImport/index.jsx";
import Logs from "../Pages/Logs";
import CreateMonitor from "@/Pages/CreateMonitor";
@@ -87,10 +86,7 @@ const Routes = () => {
}
/>
<Route
path="/uptime/bulk-import"
element={<BulkImport />}
/>
<Route path="/uptime/bulk-import" />
<Route
path="/uptime/create"