diff --git a/server/model/monitor.js b/server/model/monitor.js index 6e2b3c033..398378292 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -1727,6 +1727,55 @@ class Monitor extends BeanModel { ]); } + /** + * Delete a monitor from the system + * @param {number} monitorID ID of the monitor to delete + * @param {number} userID ID of the user who owns the monitor + * @returns {Promise} + */ + static async deleteMonitor(monitorID, userID) { + const server = UptimeKumaServer.getInstance(); + + // Stop the monitor if it's running + if (monitorID in server.monitorList) { + await server.monitorList[monitorID].stop(); + delete server.monitorList[monitorID]; + } + + // Delete from database + await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [ + monitorID, + userID, + ]); + } + + /** + * Recursively delete a monitor and all its descendants + * @param {number} monitorID ID of the monitor to delete + * @param {number} userID ID of the user who owns the monitor + * @returns {Promise} + */ + static async deleteMonitorRecursively(monitorID, userID) { + // Check if this monitor is a group + const monitor = await R.findOne("monitor", " id = ? AND user_id = ? ", [ + monitorID, + userID, + ]); + + if (monitor && monitor.type === "group") { + // Get all children and delete them recursively + const children = await Monitor.getChildren(monitorID); + if (children && children.length > 0) { + for (const child of children) { + await Monitor.deleteMonitorRecursively(child.id, userID); + } + } + } + + // Delete the monitor itself + await Monitor.deleteMonitor(monitorID, userID); + } + /** * Checks recursive if parent (ancestors) are active * @param {number} monitorID ID of the monitor to get diff --git a/server/server.js b/server/server.js index 207710a98..e6f1c246a 100644 --- a/server/server.js +++ b/server/server.js @@ -1047,51 +1047,78 @@ let needSetup = false; } }); - socket.on("deleteMonitor", async (monitorID, callback) => { + socket.on("deleteMonitor", async (monitorID, deleteChildren, callback) => { try { - checkLogin(socket); - - log.info("manage", `Delete Monitor: ${monitorID} User ID: ${socket.userID}`); - - if (monitorID in server.monitorList) { - await server.monitorList[monitorID].stop(); - delete server.monitorList[monitorID]; + // Backward compatibility: if deleteChildren is omitted, the second parameter is the callback + if (typeof deleteChildren === "function") { + callback = deleteChildren; + deleteChildren = false; } + checkLogin(socket); + const startTime = Date.now(); - // Check if this is a group monitor and unlink children before deletion + // Check if this is a group monitor const monitor = await R.findOne("monitor", " id = ? AND user_id = ? ", [ monitorID, socket.userID, ]); + // Log with context about deletion type if (monitor && monitor.type === "group") { - // Get all children before unlinking them + if (deleteChildren) { + log.info("manage", `Delete Group and Children: ${monitorID} User ID: ${socket.userID}`); + } else { + log.info("manage", `Delete Group (unlink children): ${monitorID} User ID: ${socket.userID}`); + } + } else { + log.info("manage", `Delete Monitor: ${monitorID} User ID: ${socket.userID}`); + } + + if (monitor && monitor.type === "group") { + // Get all children before processing const children = await Monitor.getChildren(monitorID); - // Unlink all children from the group - await Monitor.unlinkAllChildren(monitorID); + if (deleteChildren) { + // Delete all child monitors recursively + if (children && children.length > 0) { + for (const child of children) { + await Monitor.deleteMonitorRecursively(child.id, socket.userID); + await server.sendDeleteMonitorFromList(socket, child.id); + } + } + } else { + // Unlink all children from the group (set parent to null) + await Monitor.unlinkAllChildren(monitorID); - // Notify frontend to update each child monitor's parent to null - if (children && children.length > 0) { - for (const child of children) { - await server.sendUpdateMonitorIntoList(socket, child.id); + // Notify frontend to update each child monitor's parent to null + if (children && children.length > 0) { + for (const child of children) { + await server.sendUpdateMonitorIntoList(socket, child.id); + } } } } - await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [ - monitorID, - socket.userID, - ]); + // Delete the monitor itself + await Monitor.deleteMonitor(monitorID, socket.userID); // Fix #2880 apicache.clear(); const endTime = Date.now(); - log.info("DB", `Delete Monitor completed in : ${endTime - startTime} ms`); + // Log completion with context about children handling + if (monitor && monitor.type === "group") { + if (deleteChildren) { + log.info("DB", `Delete Monitor completed (group and children deleted) in: ${endTime - startTime} ms`); + } else { + log.info("DB", `Delete Monitor completed (group deleted, children unlinked) in: ${endTime - startTime} ms`); + } + } else { + log.info("DB", `Delete Monitor completed in: ${endTime - startTime} ms`); + } callback({ ok: true, diff --git a/src/lang/en.json b/src/lang/en.json index 84ff347a3..b559d8ab3 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -596,6 +596,8 @@ "grpcMethodDescription": "Method name is convert to camelCase format such as sayHello, check, etc.", "acceptedStatusCodesDescription": "Select status codes which are considered as a successful response.", "deleteMonitorMsg": "Are you sure want to delete this monitor?", + "deleteGroupMsg": "Are you sure you want to delete this group?", + "deleteChildrenMonitors": "Also delete the direct child monitors and its children if it has any | Also delete all {count} direct child monitors and their children if they have any", "deleteMaintenanceMsg": "Are you sure want to delete this maintenance?", "deleteNotificationMsg": "Are you sure want to delete this notification for all monitors?", "dnsPortDescription": "DNS server port. Defaults to 53. You can change the port at any time.", diff --git a/src/mixins/socket.js b/src/mixins/socket.js index 3272e042c..6c4391eff 100644 --- a/src/mixins/socket.js +++ b/src/mixins/socket.js @@ -602,11 +602,12 @@ export default { /** * Delete monitor by ID * @param {number} monitorID ID of monitor to delete + * @param {boolean} deleteChildren Whether to delete child monitors (for groups) * @param {socketCB} callback Callback for socket response * @returns {void} */ - deleteMonitor(monitorID, callback) { - socket.emit("deleteMonitor", monitorID, callback); + deleteMonitor(monitorID, deleteChildren, callback) { + socket.emit("deleteMonitor", monitorID, deleteChildren, callback); }, /** diff --git a/src/pages/Details.vue b/src/pages/Details.vue index 3538e746a..db0a890d0 100644 --- a/src/pages/Details.vue +++ b/src/pages/Details.vue @@ -441,7 +441,23 @@ :no-text="$t('No')" @yes="deleteMonitor" > - {{ $t("deleteMonitorMsg") }} +
+
{{ $t("deleteGroupMsg") }}
+
+ + +
+
+
+ {{ $t("deleteMonitorMsg") }} +
m.parent === this.monitor.id); + return children.length; + }, + + /** + * Check if the monitor is a group and has children + * @returns {boolean} True if monitor is a group with children + */ + hasChildren() { + return this.childrenCount > 0; + }, + lastHeartBeat() { // Also trigger screenshot refresh here // eslint-disable-next-line vue/no-side-effects-in-computed-properties @@ -752,7 +789,7 @@ export default { * @returns {void} */ deleteMonitor() { - this.$root.deleteMonitor(this.monitor.id, (res) => { + this.$root.deleteMonitor(this.monitor.id, this.deleteChildrenMonitors, (res) => { this.$root.toastRes(res); if (res.ok) { this.$router.push("/dashboard"); @@ -937,6 +974,10 @@ export default {