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,