diff --git a/client/src/Hooks/v1/monitorHooks.js b/client/src/Hooks/v1/monitorHooks.js
index e6b4b5261..9ee98cc55 100644
--- a/client/src/Hooks/v1/monitorHooks.js
+++ b/client/src/Hooks/v1/monitorHooks.js
@@ -538,6 +538,23 @@ const useExportMonitors = () => {
return [exportMonitors, isLoading];
};
+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];
+};
+
export {
useFetchMonitorsWithSummary,
useFetchMonitorsWithChecks,
@@ -557,4 +574,5 @@ export {
useCreateBulkMonitors,
useExportMonitors,
useFetchMonitorGames,
+ useFetchJson,
};
diff --git a/client/src/Pages/v1/Settings/SettingsExport.jsx b/client/src/Pages/v1/Settings/SettingsExport.jsx
new file mode 100644
index 000000000..fe04697f4
--- /dev/null
+++ b/client/src/Pages/v1/Settings/SettingsExport.jsx
@@ -0,0 +1,65 @@
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import Button from "@mui/material/Button";
+import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
+// Utils
+import { useTheme } from "@emotion/react";
+import { PropTypes } from "prop-types";
+import { useTranslation } from "react-i18next";
+import Dialog from "@/Components/v1/Dialog/index.jsx";
+import { useState } from "react";
+
+const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+ // Local state
+ const [isOpen, setIsOpen] = useState(false);
+
+ if (!isAdmin) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+
+ Export monitors to JSON
+
+
+ Export your monitors data as a JSON file for backup or transfer.
+
+
+
+
+
+
+ >
+ );
+};
+
+SettingsDemoMonitors.propTypes = {
+ isAdmin: PropTypes.bool,
+ handleChange: PropTypes.func,
+ HEADER_SX: PropTypes.object,
+};
+
+export default SettingsDemoMonitors;
diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx
index 2732e331a..b6137121b 100644
--- a/client/src/Pages/v1/Settings/index.jsx
+++ b/client/src/Pages/v1/Settings/index.jsx
@@ -9,6 +9,7 @@ import SettingsDemoMonitors from "./SettingsDemoMonitors.jsx";
import SettingsAbout from "./SettingsAbout.jsx";
import SettingsEmail from "./SettingsEmail.jsx";
import SettingsGlobalThresholds from "./SettingsGlobalThresholds.jsx";
+import SettingsExport from "./SettingsExport.jsx";
import Button from "@mui/material/Button";
// Utils
import { settingsValidation } from "../../../Validation/validation.js";
@@ -30,6 +31,7 @@ import {
useAddDemoMonitors,
useDeleteAllMonitors,
useDeleteMonitorStats,
+ useFetchJson,
} from "../../../Hooks/v1/monitorHooks.js";
// Constants
const BREADCRUMBS = [{ name: `Settings`, path: "/settings" }];
@@ -66,6 +68,7 @@ const Settings = () => {
});
const [deleteAllMonitors, isDeletingMonitors] = useDeleteAllMonitors();
const [deleteMonitorStats, isDeletingMonitorStats] = useDeleteMonitorStats();
+ const [fetchJson, isFetchingJson] = useFetchJson();
// Setup
const isAdmin = useIsAdmin();
@@ -128,6 +131,27 @@ const Settings = () => {
return;
}
+ if (name === "export") {
+ const json = await fetchJson();
+ if (!json || json.length === 0) {
+ return;
+ }
+
+ const blob = new Blob([JSON.stringify(json, null, 2)], {
+ type: "application/json",
+ });
+ const url = URL.createObjectURL(blob);
+
+ const link = document.createElement("a");
+ link.href = url;
+ link.download = "monitors.json";
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ return;
+ }
+
// Validate
const { error } = settingsValidation.validate(newSettingsData.settings, {
abortEarly: false,
@@ -224,6 +248,12 @@ const Settings = () => {
setEmailPasswordHasBeenReset={setEmailPasswordHasBeenReset}
/>
+
{
+ const teamId = req?.user?.teamId;
+ if (!teamId) {
+ throw this.errorService.createBadRequestError("Team ID is required");
+ }
+
+ const json = await this.monitorService.exportMonitorsToJSON({ teamId });
+
+ return res.success({
+ msg: "OK",
+ data: json,
+ });
+ },
+ SERVICE_NAME,
+ "exportMonitorsToJSON"
+ );
+
getAllGames = this.asyncHandler(
async (req, res) => {
return res.success({
diff --git a/server/src/routes/v1/monitorRoute.js b/server/src/routes/v1/monitorRoute.js
index dc0dd694b..fe430d46f 100755
--- a/server/src/routes/v1/monitorRoute.js
+++ b/server/src/routes/v1/monitorRoute.js
@@ -43,6 +43,7 @@ class MonitorRoutes {
// Other static routes
this.router.post("/demo", isAllowed(["admin", "superadmin"]), this.monitorController.addDemoMonitors);
this.router.get("/export", isAllowed(["admin", "superadmin"]), this.monitorController.exportMonitorsToCSV);
+ this.router.get("/export/json", isAllowed(["admin", "superadmin"]), this.monitorController.exportMonitorsToJSON);
this.router.post("/bulk", isAllowed(["admin", "superadmin"]), upload.single("csvFile"), this.monitorController.createBulkMonitors);
this.router.post("/test-email", isAllowed(["admin", "superadmin"]), this.monitorController.sendTestEmail);
this.router.get("/games", this.monitorController.getAllGames);
diff --git a/server/src/service/v1/business/monitorService.js b/server/src/service/v1/business/monitorService.js
index 2b643ee64..2118e6281 100644
--- a/server/src/service/v1/business/monitorService.js
+++ b/server/src/service/v1/business/monitorService.js
@@ -263,6 +263,46 @@ class MonitorService {
const csv = this.papaparse.unparse(csvData);
return csv;
};
+ exportMonitorsToJSON = async ({ teamId }) => {
+ const monitors = await this.db.monitorModule.getMonitorsByTeamId({ teamId });
+
+ if (!monitors || monitors.length === 0) {
+ throw this.errorService.createNotFoundError("No monitors to export");
+ }
+
+ const json = monitors?.filteredMonitors
+ ?.map((monitor) => {
+ const initialType = monitor.type;
+ let parsedType;
+
+ if (initialType === "hardware") {
+ parsedType = "infrastructure";
+ } else if (initialType === "http") {
+ if (monitor.url.startsWith("https://")) {
+ parsedType = "https";
+ } else {
+ parsedType = "http";
+ }
+ } else if (initialType === "pagespeed") {
+ parsedType = initialType;
+ } else {
+ // Skip unsupported types
+ return;
+ }
+
+ return {
+ name: monitor.name,
+ url: monitor.url,
+ type: parsedType,
+ interval: monitor.interval,
+ n: monitor.statusWindowSize,
+ secret: monitor.secret,
+ };
+ })
+ .filter(Boolean);
+
+ return json;
+ };
getAllGames = () => {
return this.games;