Merge pull request #3324 from bluewave-labs/fix/import

fix: import
This commit is contained in:
Alexander Holliday authored and GitHub committed 2026-02-21 08:20:57 -08:00
commit 06be65ec52
7 files changed
+134 -27

No files matched your search

+55 -4
View File
@@ -8,6 +8,7 @@ import { useTranslation } from "react-i18next";
import { useSelector, useDispatch } from "react-redux";
import DummyChart from "@/Pages/Settings/DummyChart";
import { useGet, usePatch, usePost, useLazyGet } from "@/Hooks/UseApi";
import { useToast } from "@/Hooks/UseToast";
import { useSettingsForm } from "@/Hooks/useSettingsForm";
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
import type { SettingsFormData } from "@/Validation/settings";
@@ -44,10 +45,13 @@ export const SettingsPage = () => {
const { t, i18n } = useTranslation();
const dispatch = useDispatch();
const isAdmin = useIsAdmin();
const { toastError } = useToast();
// Local state for demo monitors dialog
const [isDemoMonitorsDialogOpen, setIsDemoMonitorsDialogOpen] = useState(false);
const { post: postDemoMonitors, loading: isPostingDemoMonitors } = usePost();
const { deleteFn: deleteAllMonitors, loading: isDeletingAllMonitors } = useDelete();
// Import monitors functionality
const { post: importMonitors, loading: isImportingMonitors } = usePost();
// Fetch settings data from API
const { data: fetchedSettings } = useGet<SettingsResponse>("/settings");
@@ -202,6 +206,36 @@ export const SettingsPage = () => {
URL.revokeObjectURL(url);
};
const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) {
return;
}
if (file.type !== "application/json") {
toastError("Please select a valid JSON file");
event.target.value = "";
}
try {
const text = await file.text();
const monitors = JSON.parse(text);
if (!Array.isArray(monitors)) {
toastError("Invalid file format: expected an array of monitors");
event.target.value = "";
return;
}
await importMonitors("/monitors/import/json", { monitors });
event.target.value = "";
} catch (error) {
toastError("Error parsing JSON file. Please check the file format.");
event.target.value = "";
}
};
const onSubmit = async (data: SettingsFormData) => {
// Don't send pagespeedApiKey if it's already set and user hasn't clicked reset
const dataToSend = { ...data };
@@ -866,17 +900,34 @@ export const SettingsPage = () => {
{/* Export Monitors - Admin Only */}
{isAdmin && (
<ConfigBox
title={t("pages.settings.form.exportMonitors.title")}
subtitle={t("pages.settings.form.exportMonitors.description")}
title={t("pages.settings.form.importExportMonitors.title")}
subtitle={t("pages.settings.form.importExportMonitors.description")}
rightContent={
<Box>
<Stack
gap={theme.spacing(4)}
direction={"row"}
>
<input
id="monitor-import-input"
type="file"
accept=".json"
style={{ display: "none" }}
onChange={handleFileSelect}
/>
<Button
variant="contained"
onClick={() => document.getElementById("monitor-import-input")?.click()}
disabled={isImportingMonitors}
>
{t("common.buttons.importFromJSON")}
</Button>
<Button
variant="contained"
onClick={handleExportMonitors}
>
{t("common.buttons.exportToJSON")}
</Button>
</Box>
</Stack>
}
/>
)}
+7 -3
View File
@@ -44,7 +44,8 @@
"addDemo": "Add demo monitors",
"removeMonitors": "Remove monitors",
"sendTestEmail": "Send test email",
"exportToJSON": "Export to JSON"
"exportToJSON": "Export to JSON",
"importFromJSON": "Import from JSON"
},
"charts": {
"labels": {
@@ -1086,8 +1087,11 @@
}
},
"exportMonitors": {
"title": "Export monitors",
"description": "Export your monitors data as a JSON file for backup or transfer."
"title": "Export monitors"
},
"importExportMonitors": {
"title": "Import / Export monitors",
"description": "Import or export your monitors data as a JSON file for backup or transfer."
},
"about": {
"title": "About",
+12 -10
View File
@@ -23,6 +23,7 @@ import {
parseMonitorTypeFilter,
parseSortOrder,
requireTeamId,
requireUserId,
} from "./controllerUtils.js";
import { AppError } from "@/utils/AppError.js";
import { IMonitorService } from "@/service/index.js";
@@ -172,22 +173,23 @@ class MonitorController {
}
};
createBulkMonitors = async (req: Request, res: Response, next: NextFunction) => {
importMonitorsFromJSON = async (req: Request, res: Response, next: NextFunction) => {
try {
const userId = req.user?.id;
if (!userId) {
throw new AppError({ message: "User ID is required", status: 400 });
const teamId = requireTeamId(req?.user?.teamId);
const userId = requireUserId(req?.user?.id);
const { monitors } = req.body;
if (!monitors || !Array.isArray(monitors)) {
throw new AppError({ message: "Invalid request: monitors array is required", status: 400 });
}
const teamId = requireTeamId(req?.user?.teamId);
const data = req.body;
const monitors = await this.monitorService.createBulkMonitors(data, userId, teamId);
const result = await this.monitorService.importMonitorsFromJSON({ teamId, userId, monitors });
return res.status(200).json({
success: true,
msg: "Bulk monitors created successfully",
data: monitors,
msg: `Successfully imported ${result.imported} monitor(s)`,
data: result,
});
} catch (error) {
next(error);
@@ -17,7 +17,7 @@ export interface SummaryConfig {
export interface IMonitorsRepository {
// create
create(monitor: Monitor, teamId: string, userId: string): Promise<Monitor | null>;
createBulkMonitors(monitors: Monitor[]): Promise<Monitor[]>;
createMonitors(monitors: Monitor[]): Promise<Monitor[]>;
// single fetch
findById(monitorId: string, teamId: string): Promise<Monitor>;
@@ -12,13 +12,23 @@ class MongoMonitorsRepository implements IMonitorsRepository {
return this.toEntity(saved);
};
createBulkMonitors = async (monitors: Monitor[]): Promise<Monitor[]> => {
createMonitors = async (monitors: Monitor[]): Promise<Monitor[]> => {
if (!monitors.length) {
return [];
}
const payload = monitors.map((monitor) => ({ ...monitor, notifications: undefined }));
const inserted = await MonitorModel.insertMany(payload, { ordered: false });
return this.mapDocuments(inserted);
try {
const inserted = await MonitorModel.insertMany(payload, { ordered: false });
return this.mapDocuments(inserted);
} catch (error: any) {
if (error.name === "MongoBulkWriteError" || error.name === "BulkWriteError") {
const insertedDocs = error.insertedDocs || [];
if (insertedDocs.length > 0) {
return this.mapDocuments(insertedDocs);
}
}
throw error;
}
};
findById = async (monitorId: string, teamId: string): Promise<Monitor> => {
+3 -1
View File
@@ -26,6 +26,7 @@ class MonitorRoutes {
// Hardware routes
this.router.get("/hardware/details/:monitorId", this.monitorController.getHardwareDetailsById);
// PageSpeed routes
this.router.get("/pagespeed/details/:monitorId", this.monitorController.getPageSpeedDetailsById);
@@ -44,7 +45,8 @@ class MonitorRoutes {
// Other static routes
this.router.post("/demo", isAllowed(["admin", "superadmin"]), this.monitorController.addDemoMonitors);
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("/import/json", isAllowed(["admin", "superadmin"]), this.monitorController.importMonitorsFromJSON);
this.router.post("/test-email", isAllowed(["admin", "superadmin"]), this.monitorController.sendTestEmail);
this.router.get("/games", this.monitorController.getAllGames);
+43 -5
View File
@@ -29,7 +29,7 @@ export interface IMonitorService {
// create
createMonitor(teamId: string, userId: string, body: Monitor): Promise<void>;
createBulkMonitors(monitors: Array<Monitor>, userId: string, teamId: string): Promise<Monitor[] | null>;
createMonitors(monitors: Array<Monitor>, userId: string, teamId: string): Promise<Monitor[] | null>;
addDemoMonitors(args: { userId: string; teamId: string }): Promise<Monitor[]>;
// read
@@ -72,6 +72,7 @@ export interface IMonitorService {
// other
sendTestEmail(args: { to: string }): Promise<string>;
exportMonitorsToJSON(args: { teamId: string }): Promise<Monitor[]>;
importMonitorsFromJSON(args: { teamId: string; userId: string; monitors: any[] }): Promise<{ imported: number; errors: string[] }>;
}
export class MonitorService implements IMonitorService {
@@ -157,10 +158,10 @@ export class MonitorService implements IMonitorService {
this.jobQueue.addJob(monitor.id, monitor);
};
createBulkMonitors = async (monitors: Array<Monitor>, userId: string, teamId: string): Promise<Monitor[] | null> => {
const createdMonitors = await this.monitorsRepository.createBulkMonitors(monitors);
createMonitors = async (monitors: Array<Monitor>, userId: string, teamId: string): Promise<Monitor[] | null> => {
const createdMonitors = await this.monitorsRepository.createMonitors(monitors);
if (!monitors || monitors.length === 0) {
throw new AppError({ message: "Failed to create monitors", status: 500, service: SERVICE_NAME, method: "createBulkMonitors" });
throw new AppError({ message: "Failed to create monitors", status: 500, service: SERVICE_NAME, method: "createMonitors" });
}
await Promise.all(createdMonitors.map((monitor) => this.jobQueue.addJob(monitor.id, monitor)));
@@ -184,7 +185,7 @@ export class MonitorService implements IMonitorService {
interval: 60000,
};
});
const demoMonitors = await this.monitorsRepository.createBulkMonitors(monitors);
const demoMonitors = await this.monitorsRepository.createMonitors(monitors);
await Promise.all(demoMonitors.map((monitor) => this.jobQueue.addJob(monitor.id, monitor)));
return demoMonitors;
@@ -486,4 +487,41 @@ export class MonitorService implements IMonitorService {
return monitors;
};
importMonitorsFromJSON = async ({
teamId,
userId,
monitors,
}: {
teamId: string;
userId: string;
monitors: any[];
}): Promise<{ imported: number; errors: string[] }> => {
const errors: string[] = [];
const cleanedMonitors = monitors.map((monitor) => {
const cleanData = { ...monitor };
delete cleanData.id;
delete cleanData._id;
delete cleanData.createdAt;
delete cleanData.updatedAt;
delete cleanData.recentChecks;
// Monitors must belong to current team
cleanData.teamId = teamId;
return cleanData;
});
const createdMonitors = await this.createMonitors(cleanedMonitors, userId, teamId);
if (!createdMonitors || createdMonitors.length === 0) {
throw new AppError({
message: "Failed to import any monitors. Please check the file format and try again.",
service: SERVICE_NAME,
method: "importMonitorsFromJSON",
status: 400,
});
}
return { imported: createdMonitors.length, errors };
};
}