mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-05-18 23:48:43 -05:00
summary
This commit is contained in:
@@ -15,6 +15,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { Button, Typography, useTheme } from "@mui/material";
|
||||
|
||||
const IncidentTable = ({
|
||||
monitors = [],
|
||||
incidents = [],
|
||||
incidentsCount = 0,
|
||||
isLoading = false,
|
||||
@@ -44,7 +45,11 @@ const IncidentTable = ({
|
||||
{
|
||||
id: "monitorName",
|
||||
content: t("incidentsTableMonitorName"),
|
||||
render: (row) => row.monitorName ?? "N/A",
|
||||
render: (row) => {
|
||||
console.log(monitors, row);
|
||||
const monitor = monitors.find((monitor) => monitor.id === row.monitorId);
|
||||
return monitor ? monitor.name : "N/A";
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
@@ -130,7 +135,7 @@ const IncidentTable = ({
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
onClick={() => {
|
||||
handleResolveIncident(row._id);
|
||||
handleResolveIncident(row.id);
|
||||
}}
|
||||
>
|
||||
{t("incidentsPage.incidentsTableActionResolveManually")}
|
||||
|
||||
@@ -45,9 +45,8 @@ const useFetchIncidents = () => {
|
||||
setNetworkError(false);
|
||||
|
||||
const res = await networkService.getIncidentsByTeam(config);
|
||||
|
||||
setIncidents(res.data?.data?.incidents || []);
|
||||
setIncidentsCount(res.data?.data?.incidentsCount || 0);
|
||||
setIncidentsCount(res.data?.data?.count || 0);
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
console.error(t("incidentsPage.errorFetchingIncidents"), error);
|
||||
|
||||
@@ -88,7 +88,7 @@ const Incidents2 = () => {
|
||||
useEffect(() => {
|
||||
const lookup = monitors?.reduce((acc, monitor) => {
|
||||
acc[monitor.id] = {
|
||||
_id: monitor.id,
|
||||
id: monitor.id,
|
||||
name: monitor.name,
|
||||
type: monitor.type,
|
||||
};
|
||||
@@ -132,6 +132,7 @@ const Incidents2 = () => {
|
||||
/>
|
||||
|
||||
<IncidentTable
|
||||
monitors={monitors || []}
|
||||
incidents={incidents || []}
|
||||
incidentsCount={incidentsCount || 0}
|
||||
isLoading={isLoadingIncidents}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
const SERVICE_NAME = "incidentController";
|
||||
@@ -18,7 +19,7 @@ class IncidentController {
|
||||
try {
|
||||
const result = await this.incidentService.getIncidentsByTeam({
|
||||
teamId: req?.user?.teamId,
|
||||
query: req?.query,
|
||||
query: req?.query?.limit,
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
@@ -33,10 +34,15 @@ class IncidentController {
|
||||
|
||||
getIncidentSummary = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const summary = await this.incidentService.getIncidentSummary({
|
||||
teamId: req?.user?.teamId,
|
||||
query: req?.query,
|
||||
});
|
||||
const teamId = req.user?.teamId;
|
||||
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", service: SERVICE_NAME, status: 400 });
|
||||
}
|
||||
|
||||
const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : 10;
|
||||
|
||||
const summary = await this.incidentService.getIncidentSummary(req?.user?.teamId, limit);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Incident } from "@/types/index.js";
|
||||
import type { IncidentSummary } from "@/types/index.js";
|
||||
export interface IIncidentsRepository {
|
||||
// create
|
||||
create(incident: Partial<Incident>): Promise<Incident>;
|
||||
@@ -7,17 +8,19 @@ export interface IIncidentsRepository {
|
||||
findActiveByMonitorId(monitorId: string, teamId: string): Promise<Incident | null>;
|
||||
findByTeamId(
|
||||
teamId: string,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
startDate: Date | undefined,
|
||||
page: number,
|
||||
rowsPerPage: number,
|
||||
sortOrder?: string,
|
||||
status?: string,
|
||||
status?: boolean,
|
||||
monitorId?: string,
|
||||
resolutionType?: string
|
||||
): Promise<Incident[]>;
|
||||
findSummaryByTeamId(teamId: string, limit?: number): Promise<IncidentSummary>;
|
||||
countByTeamId(teamId: string, startDate: Date | undefined, status?: boolean, monitorId?: string, resolutionType?: string): Promise<number>;
|
||||
|
||||
// update
|
||||
updateById(incidentId: string, teamId: string, updateData: Partial<Incident>): Promise<Incident>;
|
||||
// delete
|
||||
// other
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IncidentModel } from "@/db/models/index.js";
|
||||
import type { IncidentDocument } from "@/db/models/Incident.js";
|
||||
import type { Incident } from "@/types/index.js";
|
||||
import type { Incident, IncidentSummary } from "@/types/index.js";
|
||||
import type { IIncidentsRepository } from "@/repositories/index.js";
|
||||
import mongoose from "mongoose";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
@@ -20,6 +20,32 @@ class MongoIncidentRepository implements IIncidentsRepository {
|
||||
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
||||
};
|
||||
|
||||
private buildMatchStage({
|
||||
teamId,
|
||||
startDate,
|
||||
status,
|
||||
monitorId,
|
||||
resolutionType,
|
||||
}: {
|
||||
teamId: string;
|
||||
startDate: Date | undefined;
|
||||
status?: boolean;
|
||||
monitorId?: string;
|
||||
resolutionType?: string;
|
||||
}): Record<string, any> {
|
||||
const matchStage: Record<string, any> = {
|
||||
teamId: new mongoose.Types.ObjectId(teamId),
|
||||
...(status !== undefined && { status }),
|
||||
...(monitorId && { monitorId: new mongoose.Types.ObjectId(monitorId) }),
|
||||
...(resolutionType && { resolutionType }),
|
||||
};
|
||||
|
||||
if (startDate) {
|
||||
matchStage.createdAt = { $gte: startDate };
|
||||
}
|
||||
return matchStage;
|
||||
}
|
||||
|
||||
protected toEntity = (doc: IncidentDocument): Incident => {
|
||||
return {
|
||||
id: this.toStringId(doc._id),
|
||||
@@ -79,22 +105,15 @@ class MongoIncidentRepository implements IIncidentsRepository {
|
||||
|
||||
findByTeamId = async (
|
||||
teamId: string,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
startDate: Date | undefined,
|
||||
page: number,
|
||||
rowsPerPage: number,
|
||||
sortOrder?: string,
|
||||
status?: string,
|
||||
status?: boolean,
|
||||
monitorId?: string,
|
||||
resolutionType?: string
|
||||
): Promise<Incident[]> => {
|
||||
const matchStage: Record<string, any> = {
|
||||
teamId: new mongoose.Types.ObjectId(teamId),
|
||||
...(status !== undefined && { status: status }),
|
||||
...(monitorId && { monitorId: new mongoose.Types.ObjectId(monitorId) }),
|
||||
...(resolutionType && { resolutionType }),
|
||||
createdAt: { $gte: startDate, $lte: endDate },
|
||||
};
|
||||
const matchStage = this.buildMatchStage({ teamId, startDate, status, monitorId, resolutionType });
|
||||
const incidents = await IncidentModel.find(matchStage)
|
||||
.sort({ createdAt: sortOrder === "asc" ? 1 : -1 })
|
||||
.skip(page * rowsPerPage)
|
||||
@@ -117,5 +136,130 @@ class MongoIncidentRepository implements IIncidentsRepository {
|
||||
}
|
||||
return this.toEntity(updatedIncident);
|
||||
};
|
||||
|
||||
countByTeamId = async (
|
||||
teamId: string,
|
||||
startDate: Date | undefined,
|
||||
status?: boolean,
|
||||
monitorId?: string,
|
||||
resolutionType?: string
|
||||
): Promise<number> => {
|
||||
const matchStage = this.buildMatchStage({ teamId, startDate, status, monitorId, resolutionType });
|
||||
return IncidentModel.countDocuments(matchStage);
|
||||
};
|
||||
|
||||
findSummaryByTeamId = async (teamId: string, limit: number): Promise<IncidentSummary> => {
|
||||
const matchStage = { teamId: new mongoose.Types.ObjectId(teamId) };
|
||||
|
||||
const counts = await IncidentModel.aggregate([
|
||||
{ $match: matchStage },
|
||||
{
|
||||
$group: {
|
||||
_id: "$status",
|
||||
count: { $sum: 1 },
|
||||
manualResolutions: {
|
||||
$sum: { $cond: [{ $eq: ["$resolutionType", "manual"] }, 1, 0] },
|
||||
},
|
||||
automaticResolutions: {
|
||||
$sum: { $cond: [{ $eq: ["$resolutionType", "automatic"] }, 1, 0] },
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
let total = 0;
|
||||
let active = 0;
|
||||
let manual = 0;
|
||||
let automatic = 0;
|
||||
|
||||
counts.forEach((item) => {
|
||||
total += item.count;
|
||||
if (item._id === true) {
|
||||
active = item.count;
|
||||
}
|
||||
manual += item.manualResolutions;
|
||||
automatic += item.automaticResolutions;
|
||||
});
|
||||
|
||||
const resolutionTimeResult = await IncidentModel.aggregate([
|
||||
{ $match: { ...matchStage, status: false, endTime: { $exists: true, $ne: null } } },
|
||||
{ $project: { resolutionTime: { $subtract: ["$endTime", "$startTime"] } } },
|
||||
{ $group: { _id: null, avgResolutionTime: { $avg: "$resolutionTime" } } },
|
||||
]);
|
||||
const avgResolutionTimeMs = resolutionTimeResult[0]?.avgResolutionTime || 0;
|
||||
const avgResolutionTimeHours = Math.round((avgResolutionTimeMs / (1000 * 60 * 60) || 0) * 100) / 100;
|
||||
|
||||
const monitorResult = await IncidentModel.aggregate([
|
||||
{ $match: matchStage },
|
||||
{ $group: { _id: "$monitorId", count: { $sum: 1 } } },
|
||||
{ $sort: { count: -1 } },
|
||||
{ $limit: 1 },
|
||||
{
|
||||
$lookup: {
|
||||
from: "monitors",
|
||||
localField: "_id",
|
||||
foreignField: "_id",
|
||||
as: "monitor",
|
||||
},
|
||||
},
|
||||
{ $project: { monitorId: "$_id", count: 1, monitorName: { $arrayElemAt: ["$monitor.name", 0] } } },
|
||||
]);
|
||||
|
||||
const latestLimit = Math.max(1, Number.isFinite(Number(limit)) ? Number(limit) : 10);
|
||||
const latestIncidents = await IncidentModel.aggregate([
|
||||
{ $match: matchStage },
|
||||
{ $sort: { createdAt: -1 } },
|
||||
{ $limit: latestLimit },
|
||||
{
|
||||
$lookup: {
|
||||
from: "monitors",
|
||||
localField: "monitorId",
|
||||
foreignField: "_id",
|
||||
as: "monitor",
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 1,
|
||||
monitorId: 1,
|
||||
monitorName: { $arrayElemAt: ["$monitor.name", 0] },
|
||||
status: 1,
|
||||
startTime: 1,
|
||||
endTime: 1,
|
||||
resolutionType: 1,
|
||||
message: 1,
|
||||
statusCode: 1,
|
||||
createdAt: 1,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
return {
|
||||
total,
|
||||
totalActive: active,
|
||||
totalManualResolutions: manual,
|
||||
totalAutomaticResolutions: automatic,
|
||||
avgResolutionTimeHours,
|
||||
topMonitor: monitorResult[0]
|
||||
? {
|
||||
monitorId: this.toStringId(monitorResult[0].monitorId),
|
||||
monitorName: monitorResult[0].monitorName ?? null,
|
||||
incidentCount: monitorResult[0].count,
|
||||
}
|
||||
: null,
|
||||
latestIncidents: latestIncidents.map((incident) => ({
|
||||
id: this.toStringId(incident._id),
|
||||
monitorId: this.toStringId(incident.monitorId),
|
||||
monitorName: incident.monitorName ?? null,
|
||||
status: incident.status,
|
||||
startTime: this.toDateString(incident.startTime),
|
||||
endTime: incident.endTime ? this.toDateString(incident.endTime) : null,
|
||||
resolutionType: incident.resolutionType ?? null,
|
||||
message: incident.message ?? null,
|
||||
statusCode: incident.statusCode ?? null,
|
||||
createdAt: this.toDateString(incident.createdAt),
|
||||
})),
|
||||
};
|
||||
};
|
||||
}
|
||||
export default MongoIncidentRepository;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const SERVICE_NAME = "incidentService";
|
||||
import type { Monitor } from "@/types/monitor.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { ParseBoolean } from "@/utils/utils.js";
|
||||
import type { IIncidentsRepository } from "@/repositories/index.js";
|
||||
import type { Incident } from "@/types/index.js";
|
||||
|
||||
@@ -136,16 +137,13 @@ class IncidentService {
|
||||
|
||||
const startDate = dateRangeLookup[dateRange];
|
||||
|
||||
const endDate = new Date();
|
||||
|
||||
const parsedPage = Number.isFinite(parseInt(page)) ? parseInt(page) : 0;
|
||||
const parsedRowsPerPage = Number.isFinite(parseInt(rowsPerPage)) ? parseInt(rowsPerPage) : 20;
|
||||
const parsedStatus = status === "true" ? "true" : status === "false" ? "false" : undefined;
|
||||
const parsedStatus = typeof status === "undefined" ? undefined : ParseBoolean(status);
|
||||
|
||||
const res = await this.incidentsRepository.findByTeamId(
|
||||
const incidents = await this.incidentsRepository.findByTeamId(
|
||||
teamId,
|
||||
startDate,
|
||||
endDate,
|
||||
parsedPage,
|
||||
parsedRowsPerPage,
|
||||
sortOrder,
|
||||
@@ -154,18 +152,9 @@ class IncidentService {
|
||||
resolutionType
|
||||
);
|
||||
|
||||
// const result = await this.db.incidentModule.getIncidentsByTeam({
|
||||
// teamId,
|
||||
// sortOrder,
|
||||
// dateRange,
|
||||
// page,
|
||||
// rowsPerPage,
|
||||
// status,
|
||||
// monitorId,
|
||||
// resolutionType,
|
||||
// });
|
||||
const count = await this.incidentsRepository.countByTeamId(teamId, startDate, parsedStatus, monitorId, resolutionType);
|
||||
|
||||
return res;
|
||||
return { incidents, count };
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
service: SERVICE_NAME,
|
||||
@@ -178,18 +167,14 @@ class IncidentService {
|
||||
}
|
||||
};
|
||||
|
||||
getIncidentSummary = async ({ teamId, query }: { teamId: string; query?: any }) => {
|
||||
getIncidentSummary = async (teamId: string, limit?: string) => {
|
||||
try {
|
||||
if (!teamId) {
|
||||
throw this.errorService.createBadRequestError("No team ID in request");
|
||||
throw this.errorService.createBadRequestError(" team ID in request");
|
||||
}
|
||||
|
||||
const { limit } = query || {};
|
||||
|
||||
const summary = await this.db.incidentModule.getIncidentSummary({
|
||||
teamId,
|
||||
limit,
|
||||
});
|
||||
const parsedLimit = limit && Number.isFinite(parseInt(limit, 10)) ? parseInt(limit, 10) : 10;
|
||||
const summary = await this.incidentsRepository.findSummaryByTeamId(teamId, parsedLimit);
|
||||
|
||||
return summary;
|
||||
} catch (error: any) {
|
||||
@@ -256,7 +241,7 @@ class IncidentService {
|
||||
|
||||
for (const item of resolveItems as any[]) {
|
||||
try {
|
||||
await this.resolveIncident(item.monitor);
|
||||
await this.resolveIncidentForMonitor(item.monitor);
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
service: SERVICE_NAME,
|
||||
@@ -378,6 +363,20 @@ class IncidentService {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
private resolveIncidentForMonitor = async (monitor: Monitor) => {
|
||||
if (!monitor?.id) {
|
||||
return;
|
||||
}
|
||||
const incident = await this.incidentsRepository.findActiveByMonitorId(monitor.id, monitor.teamId);
|
||||
if (!incident) {
|
||||
return;
|
||||
}
|
||||
incident.status = false;
|
||||
incident.endTime = new Date().toISOString();
|
||||
incident.resolutionType = "automatic";
|
||||
await this.incidentsRepository.updateById(incident.id, incident.teamId, incident);
|
||||
};
|
||||
}
|
||||
|
||||
export default IncidentService;
|
||||
|
||||
@@ -18,3 +18,32 @@ export interface Incident {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface IncidentSummaryTopMonitor {
|
||||
monitorId: string;
|
||||
monitorName: string | null;
|
||||
incidentCount: number;
|
||||
}
|
||||
|
||||
export interface IncidentSummaryItem {
|
||||
id: string;
|
||||
monitorId: string;
|
||||
monitorName: string | null;
|
||||
status: boolean;
|
||||
startTime: string;
|
||||
endTime: string | null;
|
||||
resolutionType: IncidentResolutionType;
|
||||
message: string | null;
|
||||
statusCode: number | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface IncidentSummary {
|
||||
total: number;
|
||||
totalActive: number;
|
||||
totalManualResolutions: number;
|
||||
totalAutomaticResolutions: number;
|
||||
avgResolutionTimeHours: number;
|
||||
topMonitor: IncidentSummaryTopMonitor | null;
|
||||
latestIncidents: IncidentSummaryItem[];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const ParseBoolean = (value) => {
|
||||
export const ParseBoolean = (value: boolean | string | null | undefined) => {
|
||||
if (value === true || value === "true") {
|
||||
return true;
|
||||
} else if (value === false || value === "false" || value === null || value === undefined) {
|
||||
@@ -6,7 +6,7 @@ const ParseBoolean = (value) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getTokenFromHeaders = (headers) => {
|
||||
export const getTokenFromHeaders = (headers: Record<string, string>) => {
|
||||
const authorizationHeader = headers.authorization;
|
||||
if (!authorizationHeader) throw new Error("No auth headers");
|
||||
|
||||
@@ -15,5 +15,3 @@ const getTokenFromHeaders = (headers) => {
|
||||
|
||||
return parts[1];
|
||||
};
|
||||
|
||||
export { ParseBoolean, getTokenFromHeaders };
|
||||
Reference in New Issue
Block a user