Compare commits

...

2 Commits

Author SHA1 Message Date
Dorian Grasset
2d8918a1b8 feat: enhance monitor deletion functionality by adding child deletion… (#6314)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2025-11-11 03:52:13 +01:00
Mercury233
20c6cfcfad Fix(i18n): refactor secondsToHumanReadableFormat (#6281)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Frank Elsinga <frank@elsinga.de>
2025-11-11 03:34:07 +01:00
8 changed files with 181 additions and 42 deletions

View File

@@ -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<void>}
*/
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<void>}
*/
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

View File

@@ -1047,30 +1047,49 @@ 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
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
@@ -1080,18 +1099,26 @@ let needSetup = false;
}
}
}
}
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,

View File

@@ -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.",

View File

@@ -1,5 +1,5 @@
import { currentLocale } from "../i18n";
import { setPageLocale, relativeTimeFormatter } from "../util-frontend";
import { setPageLocale, timeDurationFormatter } from "../util-frontend";
const langModules = import.meta.glob("../lang/*.json");
export default {
@@ -34,7 +34,7 @@ export default {
this.$i18n.locale = lang;
localStorage.locale = lang;
setPageLocale();
relativeTimeFormatter.updateLocale(lang);
timeDurationFormatter.updateLocale(lang);
},
},
};

View File

@@ -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);
},
/**

View File

@@ -441,7 +441,23 @@
:no-text="$t('No')"
@yes="deleteMonitor"
>
<div v-if="monitor && monitor.type === 'group'">
<div>{{ $t("deleteGroupMsg") }}</div>
<div v-if="hasChildren" class="form-check">
<input
id="delete-children-checkbox"
v-model="deleteChildrenMonitors"
class="form-check-input"
type="checkbox"
>
<label class="form-check-label" for="delete-children-checkbox">
{{ $t("deleteChildrenMonitors", childrenCount, { count: childrenCount }) }}
</label>
</div>
</div>
<div v-else>
{{ $t("deleteMonitorMsg") }}
</div>
</Confirm>
<Confirm
@@ -487,7 +503,7 @@ import { getMonitorRelativeURL } from "../util.ts";
import { URL } from "whatwg-url";
import DOMPurify from "dompurify";
import { marked } from "marked";
import { getResBaseURL, relativeTimeFormatter } from "../util-frontend";
import { getResBaseURL, timeDurationFormatter } from "../util-frontend";
import { highlight, languages } from "prismjs/components/prism-core";
import "prismjs/components/prism-clike";
import "prismjs/components/prism-javascript";
@@ -530,6 +546,7 @@ export default {
currentExample: "javascript-fetch",
code: "",
},
deleteChildrenMonitors: false,
};
},
computed: {
@@ -538,6 +555,26 @@ export default {
return this.$root.monitorList[id];
},
/**
* Get the count of children monitors for this group
* @returns {number} Number of children monitors
*/
childrenCount() {
if (!this.monitor || this.monitor.type !== "group") {
return 0;
}
const children = Object.values(this.$root.monitorList).filter(m => 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");
@@ -928,7 +965,7 @@ export default {
},
secondsToHumanReadableFormat(seconds) {
return relativeTimeFormatter.secondsToHumanReadableFormat(seconds);
return timeDurationFormatter.secondsToHumanReadableFormat(seconds);
},
},
};
@@ -937,6 +974,10 @@ export default {
<style lang="scss" scoped>
@import "../assets/vars.scss";
.form-check {
margin-top: 16px;
}
@media (max-width: 767px) {
.badge {
margin-top: 14px;

View File

@@ -1174,7 +1174,7 @@ import {
MIN_INTERVAL_SECOND,
sleep,
} from "../util.ts";
import { hostNameRegexPattern, relativeTimeFormatter } from "../util-frontend";
import { hostNameRegexPattern, timeDurationFormatter } from "../util-frontend";
import HiddenInput from "../components/HiddenInput.vue";
import EditMonitorConditions from "../components/EditMonitorConditions.vue";
@@ -1190,7 +1190,7 @@ const monitorDefaults = {
method: "GET",
ipFamily: null,
interval: 60,
humanReadableInterval: relativeTimeFormatter.secondsToHumanReadableFormat(60),
humanReadableInterval: timeDurationFormatter.secondsToHumanReadableFormat(60),
retryInterval: 60,
resendInterval: 0,
maxretries: 0,
@@ -1574,7 +1574,7 @@ message HealthCheckResponse {
this.monitor.retryInterval = value;
}
// Converting monitor.interval to human readable format.
this.monitor.humanReadableInterval = relativeTimeFormatter.secondsToHumanReadableFormat(value);
this.monitor.humanReadableInterval = timeDurationFormatter.secondsToHumanReadableFormat(value);
},
"monitor.timeout"(value, oldValue) {

View File

@@ -214,13 +214,18 @@ export function getToastErrorTimeout() {
return errorTimeout;
}
class RelativeTimeFormatter {
class TimeDurationFormatter {
/**
* Default locale and options for Relative Time Formatter
* Default locale and options for Time Duration Formatter (supports both DurationFormat and RelativeTimeFormat)
*/
constructor() {
this.options = { numeric: "always" };
this.instance = new Intl.RelativeTimeFormat(currentLocale(), this.options);
this.durationFormatOptions = { style: "long" };
this.relativeTimeFormatOptions = { numeric: "always" };
if (Intl.DurationFormat !== undefined) {
this.durationFormatInstance = new Intl.DurationFormat(currentLocale(), this.durationFormatOptions);
} else {
this.relativeTimeFormatInstance = new Intl.RelativeTimeFormat(currentLocale(), this.relativeTimeFormatOptions);
}
}
/**
@@ -229,7 +234,11 @@ class RelativeTimeFormatter {
* @returns {void} No return value.
*/
updateLocale(locale) {
this.instance = new Intl.RelativeTimeFormat(locale, this.options);
if (Intl.DurationFormat !== undefined) {
this.durationFormatInstance = new Intl.DurationFormat(locale, this.durationFormatOptions);
} else {
this.relativeTimeFormatInstance = new Intl.RelativeTimeFormat(locale, this.relativeTimeFormatOptions);
}
}
/**
@@ -242,6 +251,17 @@ class RelativeTimeFormatter {
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor(((seconds % 86400) % 3600) / 60);
const secs = ((seconds % 86400) % 3600) % 60;
if (this.durationFormatInstance !== undefined) {
// use Intl.DurationFormat if available
return this.durationFormatInstance.format({
days,
hours,
minutes,
seconds: secs
});
}
const parts = [];
/**
* Build the formatted string from parts
@@ -253,12 +273,11 @@ class RelativeTimeFormatter {
* @returns {void}
*/
const toFormattedPart = (value, unitOfTime) => {
const partsArray = this.instance.formatToParts(value, unitOfTime);
const partsArray = this.relativeTimeFormatInstance.formatToParts(value, unitOfTime);
const filteredParts = partsArray
.filter(
(part, index) =>
(part.type === "literal" || part.type === "integer") &&
index > 0
part.type === "integer" || (part.type === "literal" && index > 0)
)
.map((part) => part.value);
@@ -282,9 +301,9 @@ class RelativeTimeFormatter {
if (parts.length > 0) {
return `${parts.join(" ")}`;
}
return this.instance.format(0, "second"); // Handle case for 0 seconds
return this.relativeTimeFormatInstance.format(0, "second"); // Handle case for 0 seconds
}
}
export const relativeTimeFormatter = new RelativeTimeFormatter();
export const timeDurationFormatter = new TimeDurationFormatter();