diff --git a/client/src/pages/export/Export.tsx b/client/src/pages/export/Export.tsx index 96bfb753b..5de55fe64 100644 --- a/client/src/pages/export/Export.tsx +++ b/client/src/pages/export/Export.tsx @@ -14,20 +14,19 @@ const ExportPage = () => { const { get, loading } = useGetOnDemand(); const { post, loading: isPosting } = usePost(); - const handleExport = async () => { - const res = await get("/monitors/export"); - if (!res?.data) { - return; - } + const exportJson = async (endpoint: string, filename: string) => { + const res = await get(endpoint); + const data = (res as any)?.data ?? (res as any); + if (!data) return; - const blob = new Blob([JSON.stringify(res.data, null, 2)], { + const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json", }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; - link.download = "monitors.json"; + link.download = `${filename}-${Date.now()}.json`; document.body.appendChild(link); link.click(); document.body.removeChild(link); @@ -63,6 +62,8 @@ const ExportPage = () => { input.click(); }; + // Removed wrapper handlers; call exportJson directly from onClick + return ( { loading={loading} variant="contained" color="primary" - onClick={handleExport} + onClick={() => exportJson("/monitors/export", "monitors")} > Export to JSON @@ -145,6 +146,26 @@ const ExportPage = () => { } /> + + + Click here to export your incidents to JSON + + + + + } + /> ); }; diff --git a/server/src/controllers/IncidentsController.ts b/server/src/controllers/IncidentsController.ts index b91a3d7eb..ced2fa944 100644 --- a/server/src/controllers/IncidentsController.ts +++ b/server/src/controllers/IncidentsController.ts @@ -12,6 +12,7 @@ export interface IIncidentsController { getIncidents: (req: Request, res: Response, next: NextFunction) => void; getIncidentById: (req: Request, res: Response, next: NextFunction) => void; resolveIncident: (req: Request, res: Response, next: NextFunction) => void; + exportIncidents: (req: Request, res: Response, next: NextFunction) => void; } class IncidentsController implements IIncidentsController { @@ -136,6 +137,26 @@ class IncidentsController implements IIncidentsController { next(error); } }; + + exportIncidents = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + if (!userContext) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const teamId = userContext.currentTeamId; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + const incidents = await this.incidentService.export(teamId); + + return res.status(200).json({ message: "OK", data: incidents }); + } catch (error) { + next(error); + } + }; } export default IncidentsController; diff --git a/server/src/routes/incidents.ts b/server/src/routes/incidents.ts index e65aa4e44..19612e7dd 100644 --- a/server/src/routes/incidents.ts +++ b/server/src/routes/incidents.ts @@ -4,7 +4,10 @@ import { IncidentsController } from "@/controllers/index.js"; import { verifyToken } from "@/middleware/VerifyToken.js"; import { addUserContext } from "@/middleware/AddUserContext.js"; import { validateQuery, validateBody } from "@/middleware/validation.js"; -import { verifyOrgPermission } from "@/middleware/VerifyPermission.js"; +import { + verifyOrgPermission, + verifyTeamPermission, +} from "@/middleware/VerifyPermission.js"; import { PERMISSIONS } from "@/types/permissions.js"; import { getIncidentsQuerySchema, @@ -30,6 +33,14 @@ class IncidentsRoutes { validateQuery(getIncidentsQuerySchema), this.controller.getIncidents ); + this.router.get( + "/export", + verifyToken, + addUserContext, + verifyTeamPermission([PERMISSIONS.incidents.read]), + this.controller.exportIncidents + ); + this.router.get( "/:id", verifyToken, diff --git a/server/src/services/business/IncidentService.ts b/server/src/services/business/IncidentService.ts index 788d7018d..c4bf07980 100644 --- a/server/src/services/business/IncidentService.ts +++ b/server/src/services/business/IncidentService.ts @@ -4,6 +4,7 @@ import { IMonitor, Monitor, ICheck, + IUser, } from "@/db/models/index.js"; import type { ResolutionType } from "@/db/models/index.js"; import mongoose from "mongoose"; @@ -11,6 +12,11 @@ import { getChildLogger } from "@/logger/Logger.js"; import ApiError from "@/utils/ApiError.js"; import { ThresholdEvaluationResult } from "@/services/infrastructure/StatusService.js"; +type IncidentPopulated = Omit & { + monitorId: IMonitor; + resolvedBy?: IUser | null; +}; + export interface IIncidentService { handleStatusChange: ( updatedMonitor: IMonitor, @@ -48,6 +54,7 @@ export interface IIncidentService { teamId: mongoose.Types.ObjectId, incidentId: mongoose.Types.ObjectId ) => Promise; + export: (teamId: string) => Promise>>; cleanupOrphanedIncidents: () => Promise; } @@ -267,6 +274,25 @@ class IncidentService implements IIncidentService { return result.deletedCount === 1; }; + export = async (teamId: string) => { + const incidents = await Incident.find({ + teamId, + }) + .populate("monitorId") + .populate("resolvedBy") + .lean(); + + return incidents.map((incident) => ({ + monitor: incident.monitorId?.name ?? "", + monitorId: incident.monitorId?._id?.toString?.() ?? "", + resolved: incident.resolved, + resolutionType: incident.resolutionType ?? "Unresolved", + startedAt: incident.startedAt, + endedAt: incident.endedAt ?? "Ongoing", + resolvedBy: incident.resolvedBy ? incident.resolvedBy.email ?? "" : "", + })); + }; + async cleanupOrphanedIncidents() { try { const monitorIds = await Monitor.find().distinct("_id");