Merge pull request #3069 from bluewave-labs/feat/JSON-export

implement JSON export
This commit is contained in:
Alexander Holliday
2025-11-21 10:45:32 -08:00
committed by GitHub
7 changed files with 176 additions and 0 deletions

View File

@@ -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,
};

View File

@@ -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 (
<>
<ConfigBox>
<Box>
<Typography
component="h1"
variant="h2"
>
Export monitors to JSON
</Typography>
<Typography sx={HEADER_SX}>
Export your monitors data as a JSON file for backup or transfer.
</Typography>
</Box>
<Box>
<Button
variant="contained"
color="accent"
loading={isLoading}
onClick={() => {
const syntheticEvent = {
target: {
name: "export",
},
};
handleChange(syntheticEvent);
}}
sx={{ mt: theme.spacing(4) }}
>
Export Monitors to JSON
</Button>
</Box>
</ConfigBox>
</>
);
};
SettingsDemoMonitors.propTypes = {
isAdmin: PropTypes.bool,
handleChange: PropTypes.func,
HEADER_SX: PropTypes.object,
};
export default SettingsDemoMonitors;

View File

@@ -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}
/>
<SettingsExport
isAdmin={isAdmin}
HEADER_SX={HEADING_SX}
handleChange={handleChange}
isLoading={isSettingsLoading || isSaving || isFetchingJson}
/>
<SettingsAbout />
<Stack
direction="row"

View File

@@ -1145,6 +1145,10 @@ class NetworkService {
},
});
}
async fetchJson() {
return this.axiosInstance.get("/monitors/export/json");
}
}
export default NetworkService;

View File

@@ -442,6 +442,24 @@ class MonitorController extends BaseController {
"exportMonitorsToCSV"
);
exportMonitorsToJSON = this.asyncHandler(
async (req, res) => {
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({

View File

@@ -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);

View File

@@ -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;