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;