diff --git a/client/src/Hooks/checkHooks.js b/client/src/Hooks/checkHooks.js
index 683b89b20..98fb0cfd5 100644
--- a/client/src/Hooks/checkHooks.js
+++ b/client/src/Hooks/checkHooks.js
@@ -8,6 +8,7 @@ const useFetchChecksTeam = ({
limit,
dateRange,
filter,
+ ack,
page,
rowsPerPage,
enabled = true,
@@ -29,6 +30,7 @@ const useFetchChecksTeam = ({
limit,
dateRange,
filter,
+ ack,
page,
rowsPerPage,
};
@@ -47,7 +49,7 @@ const useFetchChecksTeam = ({
};
fetchChecks();
- }, [status, sortOrder, limit, dateRange, filter, page, rowsPerPage, enabled]);
+ }, [status, sortOrder, limit, dateRange, filter, ack, page, rowsPerPage, enabled]);
return [checks, checksCount, isLoading, networkError];
};
@@ -60,6 +62,7 @@ const useFetchChecksByMonitor = ({
limit,
dateRange,
filter,
+ ack,
page,
rowsPerPage,
enabled = true,
@@ -83,6 +86,7 @@ const useFetchChecksByMonitor = ({
limit,
dateRange,
filter,
+ ack,
page,
rowsPerPage,
};
@@ -109,6 +113,7 @@ const useFetchChecksByMonitor = ({
limit,
dateRange,
filter,
+ ack,
page,
rowsPerPage,
enabled,
diff --git a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx b/client/src/Pages/Incidents/Components/IncidentTable/index.jsx
index 3b2e07238..bc5eaa5ca 100644
--- a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx
+++ b/client/src/Pages/Incidents/Components/IncidentTable/index.jsx
@@ -40,7 +40,8 @@ const IncidentTable = ({
sortOrder: "desc",
limit: null,
dateRange,
- filter: filter,
+ filter: filter === "resolved" ? "all" : filter,
+ ack: filter === "resolved" ? true : false,
page: page,
rowsPerPage: rowsPerPage,
enabled: selectedMonitor !== "0",
@@ -52,7 +53,8 @@ const IncidentTable = ({
sortOrder: "desc",
limit: null,
dateRange,
- filter: filter,
+ filter: filter === "resolved" ? "all" : filter,
+ ack: filter === "resolved" ? true : false,
page: page,
rowsPerPage: rowsPerPage,
enabled: selectedMonitor === "0",
diff --git a/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx b/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx
index 20140e8c4..457c6fd96 100644
--- a/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx
+++ b/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx
@@ -87,6 +87,13 @@ const OptionsHeader = ({
>
{t("incidentsOptionsHeaderFilterCannotResolve")}
+ {/* */}
diff --git a/client/src/Utils/NetworkService.js b/client/src/Utils/NetworkService.js
index 714268b0b..414c45a22 100644
--- a/client/src/Utils/NetworkService.js
+++ b/client/src/Utils/NetworkService.js
@@ -549,6 +549,7 @@ class NetworkService {
* @param {number} config.limit - The maximum number of checks to retrieve.
* @param {string} config.dateRange - The range of dates for which to retrieve checks.
* @param {string} config.filter - The filter to apply to the checks.
+ * @param {boolean} config.ack - The acknowledgment status to apply to the checks.
* @param {number} config.page - The page number to retrieve in a paginated list.
* @param {number} config.rowsPerPage - The number of rows per page in a paginated list.
* @returns {Promise} The response from the axios GET request.
@@ -562,6 +563,7 @@ class NetworkService {
if (config.limit) params.append("limit", config.limit);
if (config.dateRange) params.append("dateRange", config.dateRange);
if (config.filter) params.append("filter", config.filter);
+ if (config.ack !== undefined) params.append("ack", config.ack);
if (config.page) params.append("page", config.page);
if (config.rowsPerPage) params.append("rowsPerPage", config.rowsPerPage);
if (config.status !== undefined) params.append("status", config.status);
@@ -581,6 +583,7 @@ class NetworkService {
* @param {number} config.limit - The maximum number of checks to retrieve.
* @param {string} config.dateRange - The range of dates for which to retrieve checks.
* @param {string} config.filter - The filter to apply to the checks.
+ * @param {boolean} config.ack - The acknowledgment status to apply to the checks.
* @param {number} config.page - The page number to retrieve in a paginated list.
* @param {number} config.rowsPerPage - The number of rows per page in a paginated list.
* @returns {Promise} The response from the axios GET request.
@@ -592,12 +595,30 @@ class NetworkService {
if (config.limit) params.append("limit", config.limit);
if (config.dateRange) params.append("dateRange", config.dateRange);
if (config.filter) params.append("filter", config.filter);
+ if (config.ack !== undefined) params.append("ack", config.ack);
if (config.page) params.append("page", config.page);
if (config.rowsPerPage) params.append("rowsPerPage", config.rowsPerPage);
- if (config.status !== undefined) params.append("status", config.status);
return this.axiosInstance.get(`/checks/team?${params.toString()}`);
};
+ /**
+ * ************************************
+ * Update the status of a check
+ * ************************************
+ *
+ * @async
+ * @param {Object} config - The configuration object.
+ * @param {string} config.checkId - The ID of the check to update.
+ * @param {boolean} config.ack - The acknowledgment to update the check to.
+ * @returns {Promise} The response from the axios PUT request.
+ *
+ */
+ async updateCheckStatus(config) {
+ return this.axiosInstance.put(`/checks/check/${config.checkId}`, {
+ ack: config.ack,
+ });
+ }
+
/**
* ************************************
* Get all checks for a given user
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index bf025667c..117188852 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -637,6 +637,7 @@
}
}
},
+ "incidentsOptionsHeaderFilterResolved": "Resolved",
"createNotifications": {
"title": "Create notification channel",
"nameSettings": {
@@ -792,8 +793,8 @@
},
"pageSpeedSettings": {
"description": "Enter your Google PageSpeed API key to enable Google PageSpeed monitoring. Click Reset to update the key.",
- "labelApiKeySet": "API key is set. Click Reset to change it.",
"labelApiKey": "PageSpeed API key",
+ "labelApiKeySet": "API key is set. Click Reset to change it.",
"title": "Google PageSpeed API key"
},
"saveButtonLabel": "Save",
diff --git a/server/controllers/checkController.js b/server/controllers/checkController.js
index 387839beb..8f9d0fbb5 100755
--- a/server/controllers/checkController.js
+++ b/server/controllers/checkController.js
@@ -8,6 +8,9 @@ import {
deleteChecksParamValidation,
deleteChecksByTeamIdParamValidation,
updateChecksTTLBodyValidation,
+ ackCheckBodyValidation,
+ ackAllChecksParamValidation,
+ ackAllChecksBodyValidation,
} from "../validation/joi.js";
import jwt from "jsonwebtoken";
import { getTokenFromHeaders } from "../utils/utils.js";
@@ -55,13 +58,15 @@ class CheckController {
try {
const { monitorId } = req.params;
- let { type, sortOrder, dateRange, filter, page, rowsPerPage, status } = req.query;
+ let { type, sortOrder, dateRange, filter, ack, page, rowsPerPage, status } =
+ req.query;
const result = await this.db.getChecksByMonitor({
monitorId,
type,
sortOrder,
dateRange,
filter,
+ ack,
page,
rowsPerPage,
status,
@@ -85,13 +90,14 @@ class CheckController {
return;
}
try {
- let { sortOrder, dateRange, filter, page, rowsPerPage } = req.query;
+ let { sortOrder, dateRange, filter, ack, page, rowsPerPage } = req.query;
const { teamId } = req.user;
const checkData = await this.db.getChecksByTeam({
sortOrder,
dateRange,
filter,
+ ack,
page,
rowsPerPage,
teamId,
@@ -105,6 +111,55 @@ class CheckController {
}
};
+ ackCheck = async (req, res, next) => {
+ try {
+ await ackCheckBodyValidation.validateAsync(req.body);
+ } catch (error) {
+ next(handleValidationError(error, SERVICE_NAME));
+ return;
+ }
+
+ try {
+ const { checkId } = req.params;
+ const { ack } = req.body;
+ const { teamId } = req.user;
+
+ const updatedCheck = await this.db.ackCheck(checkId, teamId, ack);
+
+ return res.success({
+ msg: this.stringService.checkUpdateStatus,
+ data: updatedCheck,
+ });
+ } catch (error) {
+ next(handleError(error, SERVICE_NAME, "ackCheck"));
+ }
+ };
+
+ ackAllChecks = async (req, res, next) => {
+ try {
+ await ackAllChecksParamValidation.validateAsync(req.params);
+ await ackAllChecksBodyValidation.validateAsync(req.body);
+ } catch (error) {
+ next(handleValidationError(error, SERVICE_NAME));
+ return;
+ }
+
+ try {
+ const { monitorId, path } = req.params;
+ const { ack } = req.body;
+ const { teamId } = req.user;
+
+ const updatedChecks = await this.db.ackAllChecks(monitorId, teamId, ack, path);
+
+ return res.success({
+ msg: this.stringService.checkUpdateStatus,
+ data: updatedChecks,
+ });
+ } catch (error) {
+ next(handleError(error, SERVICE_NAME, "ackAllChecks"));
+ }
+ };
+
deleteChecks = async (req, res, next) => {
try {
await deleteChecksParamValidation.validateAsync(req.params);
diff --git a/server/db/models/Check.js b/server/db/models/Check.js
index 39c5281d2..8bb6cd028 100755
--- a/server/db/models/Check.js
+++ b/server/db/models/Check.js
@@ -64,6 +64,23 @@ const BaseCheckSchema = mongoose.Schema({
default: Date.now,
expires: 60 * 60 * 24 * 30, // 30 days
},
+ /**
+ * Acknowledgment of the check.
+ *
+ * @type {Boolean}
+ */
+ ack: {
+ type: Boolean,
+ default: false,
+ },
+ /**
+ * Resolution date of the check (when the check was resolved).
+ *
+ * @type {Date}
+ */
+ ackAt: {
+ type: Date,
+ },
});
/**
diff --git a/server/db/mongo/modules/checkModule.js b/server/db/mongo/modules/checkModule.js
index a53384d1a..b7d9f4fb2 100755
--- a/server/db/mongo/modules/checkModule.js
+++ b/server/db/mongo/modules/checkModule.js
@@ -63,18 +63,26 @@ const getChecksByMonitor = async ({
sortOrder,
dateRange,
filter,
+ ack,
page,
rowsPerPage,
status,
}) => {
try {
- status = typeof status !== "undefined" ? false : undefined;
+ status = status === "true" ? true : status === "false" ? false : undefined;
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
+
+ const ackStage =
+ ack === "true"
+ ? { ack: true }
+ : { $or: [{ ack: false }, { ack: { $exists: false } }] };
+
// Match
const matchStage = {
monitorId: new ObjectId(monitorId),
...(typeof status !== "undefined" && { status }),
+ ...(typeof ack !== "undefined" && ackStage),
...(dateRangeLookup[dateRange] && {
createdAt: {
$gte: dateRangeLookup[dateRange],
@@ -153,6 +161,7 @@ const getChecksByTeam = async ({
sortOrder,
dateRange,
filter,
+ ack,
page,
rowsPerPage,
teamId,
@@ -160,9 +169,16 @@ const getChecksByTeam = async ({
try {
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
+
+ const ackStage =
+ ack === "true"
+ ? { ack: true }
+ : { $or: [{ ack: false }, { ack: { $exists: false } }] };
+
const matchStage = {
teamId: new ObjectId(teamId),
status: false,
+ ...(typeof ack !== "undefined" && ackStage),
...(dateRangeLookup[dateRange] && {
createdAt: {
$gte: dateRangeLookup[dateRange],
@@ -236,6 +252,58 @@ const getChecksByTeam = async ({
}
};
+/**
+ * Update the acknowledgment status of a check
+ * @async
+ * @param {string} checkId - The ID of the check to update
+ * @param {string} teamId - The ID of the team
+ * @param {boolean} ack - The acknowledgment status to set
+ * @returns {Promise}
+ * @throws {Error}
+ */
+const ackCheck = async (checkId, teamId, ack) => {
+ try {
+ const updatedCheck = await Check.findOneAndUpdate(
+ { _id: checkId, teamId: teamId },
+ { $set: { ack, ackAt: new Date() } },
+ { new: true }
+ );
+
+ if (!updatedCheck) {
+ throw new Error("Check not found");
+ }
+
+ return updatedCheck;
+ } catch (error) {
+ error.service = SERVICE_NAME;
+ error.method = "ackCheck";
+ throw error;
+ }
+};
+
+/**
+ * Update the acknowledgment status of all checks for a monitor or team
+ * @async
+ * @param {string} id - The monitor ID or team ID
+ * @param {boolean} ack - The acknowledgment status to set
+ * @param {string} path - The path type ('monitor' or 'team')
+ * @returns {Promise}
+ * @throws {Error}
+ */
+const ackAllChecks = async (monitorId, teamId, ack, path) => {
+ try {
+ const updatedChecks = await Check.updateMany(
+ path === "monitor" ? { monitorId } : { teamId },
+ { $set: { ack, ackAt: new Date() } }
+ );
+ return updatedChecks.modifiedCount;
+ } catch (error) {
+ error.service = SERVICE_NAME;
+ error.method = "ackAllChecks";
+ throw error;
+ }
+};
+
/**
* Delete all checks for a monitor
* @async
@@ -317,6 +385,8 @@ export {
createChecks,
getChecksByMonitor,
getChecksByTeam,
+ ackCheck,
+ ackAllChecks,
deleteChecks,
deleteChecksByTeamId,
updateChecksTTL,
diff --git a/server/routes/checkRoute.js b/server/routes/checkRoute.js
index aedca991a..7f91dbe31 100755
--- a/server/routes/checkRoute.js
+++ b/server/routes/checkRoute.js
@@ -1,7 +1,9 @@
import { Router } from "express";
import { verifyOwnership } from "../middleware/verifyOwnership.js";
+import { verifyTeamAccess } from "../middleware/verifyTeamAccess.js";
import { isAllowed } from "../middleware/isAllowed.js";
import Monitor from "../db/models/Monitor.js";
+import Check from "../db/models/Check.js";
class CheckRoutes {
constructor(checkController) {
@@ -20,6 +22,14 @@ class CheckRoutes {
this.router.get("/:monitorId", this.checkController.getChecksByMonitor);
+ this.router.put(
+ "/check/:checkId",
+ verifyTeamAccess(Check, "checkId"),
+ this.checkController.ackCheck
+ );
+
+ this.router.put("/:path/:monitorId?", this.checkController.ackAllChecks);
+
this.router.post(
"/:monitorId",
verifyOwnership(Monitor, "monitorId"),
diff --git a/server/validation/joi.js b/server/validation/joi.js
index e5c1d6e43..d44bb14eb 100755
--- a/server/validation/joi.js
+++ b/server/validation/joi.js
@@ -289,6 +289,19 @@ const createCheckBodyValidation = joi.object({
message: joi.string().required(),
});
+const ackCheckBodyValidation = joi.object({
+ ack: joi.boolean(),
+});
+
+const ackAllChecksParamValidation = joi.object({
+ monitorId: joi.string().optional(),
+ path: joi.string().valid("monitor", "team").required(),
+});
+
+const ackAllChecksBodyValidation = joi.object({
+ ack: joi.boolean(),
+});
+
const getChecksParamValidation = joi.object({
monitorId: joi.string().required(),
});
@@ -299,6 +312,7 @@ const getChecksQueryValidation = joi.object({
limit: joi.number(),
dateRange: joi.string().valid("recent", "hour", "day", "week", "month", "all"),
filter: joi.string().valid("all", "down", "resolve"),
+ ack: joi.boolean(),
page: joi.number(),
rowsPerPage: joi.number(),
status: joi.boolean(),
@@ -311,9 +325,9 @@ const getTeamChecksQueryValidation = joi.object({
limit: joi.number(),
dateRange: joi.string().valid("hour", "day", "week", "month", "all"),
filter: joi.string().valid("all", "down", "resolve"),
+ ack: joi.boolean(),
page: joi.number(),
rowsPerPage: joi.number(),
- status: joi.boolean(),
});
const deleteChecksParamValidation = joi.object({
@@ -654,6 +668,9 @@ export {
getChecksQueryValidation,
getTeamChecksParamValidation,
getTeamChecksQueryValidation,
+ ackCheckBodyValidation,
+ ackAllChecksParamValidation,
+ ackAllChecksBodyValidation,
deleteChecksParamValidation,
deleteChecksByTeamIdParamValidation,
updateChecksTTLBodyValidation,