7 files changed
+134
-27
No files matched your search
@@ -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>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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> => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user