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