diff --git a/server/model/monitor.js b/server/model/monitor.js index 9a30a6689..560738a88 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -383,7 +383,11 @@ class Monitor extends BeanModel { } else if (this.type === "group") { const children = await Monitor.getChildren(this.id); - if (children.length > 0) { + if (children.length > 0 && children.filter(child => child.active).length === 0) { + // Set status pending if all children are paused + bean.status = PENDING; + bean.msg = "All Children are paused."; + } else if (children.length > 0) { bean.status = UP; bean.msg = "All children up and running"; for (const child of children) { @@ -1468,10 +1472,12 @@ class Monitor extends BeanModel { * @returns {Promise} Is the monitor under maintenance */ static async isUnderMaintenance(monitorID) { + const ancestorIDs = await Monitor.getAllAncestorIDs(monitorID); + const allIDs = [ monitorID, ...ancestorIDs ]; const maintenanceIDList = await R.getCol(` SELECT maintenance_id FROM monitor_maintenance - WHERE monitor_id = ? - `, [ monitorID ]); + WHERE monitor_id IN (${allIDs.map((_) => "?").join(",")}) + `, allIDs); for (const maintenanceID of maintenanceIDList) { const maintenance = await UptimeKumaServer.getInstance().getMaintenance(maintenanceID); @@ -1480,11 +1486,6 @@ class Monitor extends BeanModel { } } - const parent = await Monitor.getParent(monitorID); - if (parent != null) { - return await Monitor.isUnderMaintenance(parent.id); - } - return false; } @@ -1604,13 +1605,32 @@ class Monitor extends BeanModel { }; } + /** + * Gets all active monitors + * @returns {Promise} Active Monitors + */ + static async getAllActiveMonitors() { + // Gets all monitors but only if they and all their ancestors are active + return R.convertToBeans("monitor", await R.getAll(` + WITH RECURSIVE MonitorHierarchy AS ( + SELECT * FROM monitor + WHERE parent IS NULL AND active = 1 + UNION ALL + SELECT m.* FROM monitor m + INNER JOIN MonitorHierarchy mh ON m.parent = mh.id + WHERE m.active = 1 + ) + SELECT * FROM MonitorHierarchy; + `)); + } + /** * Gets Parent of the monitor * @param {number} monitorID ID of monitor to get - * @returns {Promise>} Parent + * @returns {Promise} Parent */ static async getParent(monitorID) { - return await R.getRow(` + const result = await R.getRow(` SELECT parent.* FROM monitor parent LEFT JOIN monitor child ON child.parent = parent.id @@ -1618,20 +1638,25 @@ class Monitor extends BeanModel { `, [ monitorID, ]); + + if (!result) { + return null; + } + return R.convertToBean("monitor", result); } /** * Gets all Children of the monitor * @param {number} monitorID ID of monitor to get - * @returns {Promise>} Children + * @returns {Promise} Children */ static async getChildren(monitorID) { - return await R.getAll(` + return R.convertToBeans("monitor", await R.getAll(` SELECT * FROM monitor WHERE parent = ? `, [ monitorID, - ]); + ])); } /** @@ -1657,25 +1682,64 @@ class Monitor extends BeanModel { } /** - * Gets recursive all child ids + * Gets recursive all ancestor ids * @param {number} monitorID ID of the monitor to get - * @returns {Promise} IDs of all children + * @returns {Promise} IDs of all ancestors */ - static async getAllChildrenIDs(monitorID) { - const childs = await Monitor.getChildren(monitorID); + static async getAllAncestorIDs(monitorID) { + // Gets all ancestor monitor ids recursive + return await R.getCol(` + WITH RECURSIVE Ancestors AS ( + SELECT parent FROM monitor + WHERE id = ? + UNION ALL + SELECT m.parent FROM monitor m + JOIN Ancestors a ON m.id = a.parent + ) + SELECT parent AS ancestor_id FROM Ancestors + WHERE parent IS NOT NULL; + `, [ + monitorID, + ]); + } - if (childs === null) { - return []; + /** + * Gets recursive all children ids + * @param {number} monitorID ID of the monitor to get + * @param {boolean} onlyActive Return only monitors which are active (including all ancestors) + * @returns {Promise} IDs of all children + */ + static async getAllChildrenIDs(monitorID, onlyActive = false) { + if (onlyActive) { + // Gets all children monitor ids recursive but only if they and all their ancestors are active + return await R.getCol(` + WITH RECURSIVE MonitorHierarchy(id, active_chain) AS ( + SELECT id, active FROM monitor + WHERE id = ? + UNION ALL + SELECT m.id, m.active * mh.active_chain FROM monitor m + INNER JOIN MonitorHierarchy mh ON m.parent = mh.id + WHERE mh.active_chain = 1 + ) + SELECT id FROM MonitorHierarchy + WHERE id != ? AND active_chain = 1; + `, [ + monitorID, + monitorID + ]); } - - let childrenIDs = []; - - for (const child of childs) { - childrenIDs.push(child.id); - childrenIDs = childrenIDs.concat(await Monitor.getAllChildrenIDs(child.id)); - } - - return childrenIDs; + // Gets all children monitor ids recursive + return await R.getCol(` + WITH RECURSIVE MonitorHierarchy(id) AS ( + SELECT id FROM monitor WHERE id = ? + UNION ALL + SELECT m.id FROM monitor m INNER JOIN MonitorHierarchy mh ON m.parent = mh.id + ) + SELECT id FROM MonitorHierarchy WHERE id != ?; + `, [ + monitorID, + monitorID + ]); } /** @@ -1695,14 +1759,32 @@ class Monitor extends BeanModel { * @returns {Promise} Is the parent monitor active? */ static async isParentActive(monitorID) { - const parent = await Monitor.getParent(monitorID); + // Checks recursive if the parent and all its ancestors are active + const result = await R.getRow(` + WITH RECURSIVE MonitorHierarchy AS ( + SELECT parent FROM monitor + WHERE id = ? + UNION ALL + SELECT m.parent FROM monitor m + JOIN MonitorHierarchy mh ON m.id = mh.parent + ) + SELECT + CASE + WHEN (SELECT parent FROM monitor WHERE id = ?) IS NULL THEN 1 + ELSE + CASE + WHEN COUNT(m.id) = SUM(m.active) THEN 1 + ELSE 0 + END + END AS all_active + FROM MonitorHierarchy mh + LEFT JOIN monitor m ON mh.parent = m.id; + `, [ + monitorID, + monitorID + ]); - if (parent === null) { - return true; - } - - const parentActive = await Monitor.isParentActive(parent.id); - return parent.active && parentActive; + return result.all_active === 1; } /** diff --git a/server/server.js b/server/server.js index c88daca88..c035b3d3e 100644 --- a/server/server.js +++ b/server/server.js @@ -729,7 +729,7 @@ let needSetup = false; await updateMonitorNotification(bean.id, notificationIDList); - await server.sendUpdateMonitorIntoList(socket, bean.id); + await server.sendUpdateMonitorsIntoList(socket, bean.id); if (monitor.active !== false) { await startMonitor(socket.userID, bean.id); @@ -889,7 +889,7 @@ let needSetup = false; await restartMonitor(socket.userID, bean.id); } - await server.sendUpdateMonitorIntoList(socket, bean.id); + await server.sendUpdateMonitorsIntoList(socket, bean.id); callback({ ok: true, @@ -990,7 +990,9 @@ let needSetup = false; try { checkLogin(socket); await startMonitor(socket.userID, monitorID); - await server.sendUpdateMonitorIntoList(socket, monitorID); + + const childrenIDs = await Monitor.getAllChildrenIDs(monitorID); + await server.sendUpdateMonitorsIntoList(socket, [ monitorID, ...childrenIDs ]); callback({ ok: true, @@ -1010,7 +1012,9 @@ let needSetup = false; try { checkLogin(socket); await pauseMonitor(socket.userID, monitorID); - await server.sendUpdateMonitorIntoList(socket, monitorID); + + const childrenIDs = await Monitor.getAllChildrenIDs(monitorID); + await server.sendUpdateMonitorsIntoList(socket, [ monitorID, ...childrenIDs ]); callback({ ok: true, @@ -1753,16 +1757,19 @@ async function startMonitor(userID, monitorID) { userID, ]); - let monitor = await R.findOne("monitor", " id = ? ", [ - monitorID, - ]); + const childrenIDs = await Monitor.getAllChildrenIDs(monitorID, true); + const monitorIDs = [ monitorID, ...childrenIDs ]; - if (monitor.id in server.monitorList) { - await server.monitorList[monitor.id].stop(); - } + let monitors = await R.find("monitor", ` id IN (${monitorIDs.map((_) => "?").join(",")})`, monitorIDs); - server.monitorList[monitor.id] = monitor; - await monitor.start(io); + await Promise.all(monitors.map(async (monitor) => { + if (monitor.id in server.monitorList) { + await server.monitorList[monitor.id].stop(); + } + + server.monitorList[monitor.id] = monitor; + await monitor.start(io); + })); } /** @@ -1791,10 +1798,16 @@ async function pauseMonitor(userID, monitorID) { userID, ]); - if (monitorID in server.monitorList) { - await server.monitorList[monitorID].stop(); - server.monitorList[monitorID].active = 0; - } + const childrenIDs = await Monitor.getAllChildrenIDs(monitorID); + const monitorIDs = [ monitorID, ...childrenIDs ]; + + await Promise.all(monitorIDs.map(async (currentMonitorID) => { + if (currentMonitorID in server.monitorList) { + await server.monitorList[currentMonitorID].stop(); + } + })); + + server.monitorList[monitorID].active = 0; } /** @@ -1802,7 +1815,7 @@ async function pauseMonitor(userID, monitorID) { * @returns {Promise} */ async function startMonitors() { - let list = await R.find("monitor", " active = 1 "); + let list = await Monitor.getAllActiveMonitors(); for (let monitor of list) { server.monitorList[monitor.id] = monitor; diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js index 062f098d7..80db98b87 100644 --- a/server/uptime-kuma-server.js +++ b/server/uptime-kuma-server.js @@ -209,12 +209,16 @@ class UptimeKumaServer { /** * Update Monitor into list * @param {Socket} socket Socket to send list on - * @param {number} monitorID update or deleted monitor id + * @param {number | number[]} monitorIDs update or deleted monitor ids * @returns {Promise} */ - async sendUpdateMonitorIntoList(socket, monitorID) { - let list = await this.getMonitorJSONList(socket.userID, monitorID); - this.io.to(socket.userID).emit("updateMonitorIntoList", list); + async sendUpdateMonitorsIntoList(socket, monitorIDs) { + if (!Array.isArray(monitorIDs)) { + monitorIDs = [ monitorIDs ]; + } + + let list = await this.getMonitorJSONList(socket.userID, monitorIDs); + this.io.to(socket.userID).emit("updateMonitorsIntoList", list); } /** @@ -230,19 +234,19 @@ class UptimeKumaServer { /** * Get a list of monitors for the given user. * @param {string} userID - The ID of the user to get monitors for. - * @param {number} monitorID - The ID of monitor for. + * @param {number[]} monitorIDs - The IDs of monitors for. * @returns {Promise} A promise that resolves to an object with monitor IDs as keys and monitor objects as values. * * Generated by Trelent */ - async getMonitorJSONList(userID, monitorID = null) { + async getMonitorJSONList(userID, monitorIDs = null) { let query = " user_id = ? "; let queryParams = [ userID ]; - if (monitorID) { - query += "AND id = ? "; - queryParams.push(monitorID); + if (monitorIDs) { + query += `AND id IN (${monitorIDs.map((_) => "?").join(",")}) `; + queryParams.push(...monitorIDs); } let monitorList = await R.find("monitor", query + "ORDER BY weight DESC, name", queryParams); diff --git a/src/mixins/socket.js b/src/mixins/socket.js index 3272e042c..31b7d630d 100644 --- a/src/mixins/socket.js +++ b/src/mixins/socket.js @@ -145,7 +145,7 @@ export default { this.monitorList = data; }); - socket.on("updateMonitorIntoList", (data) => { + socket.on("updateMonitorsIntoList", (data) => { this.assignMonitorUrlParser(data); Object.entries(data).forEach(([ monitorID, updatedMonitor ]) => { this.monitorList[monitorID] = updatedMonitor;