mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-12-06 05:24:48 -08:00
Compare commits
6 Commits
3.0.X
...
2d8918a1b8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d8918a1b8 | ||
|
|
20c6cfcfad | ||
|
|
8d48ed7850 | ||
|
|
751ffd8e72 | ||
|
|
81544c8a39 | ||
|
|
36ac5dd56d |
@@ -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
|
||||
|
||||
@@ -11,35 +11,67 @@ class GroupMonitorType extends MonitorType {
|
||||
async check(monitor, heartbeat, _server) {
|
||||
const children = await Monitor.getChildren(monitor.id);
|
||||
|
||||
if (children.length > 0) {
|
||||
heartbeat.status = UP;
|
||||
heartbeat.msg = "All children up and running";
|
||||
for (const child of children) {
|
||||
if (!child.active) {
|
||||
// Ignore inactive childs
|
||||
continue;
|
||||
}
|
||||
const lastBeat = await Monitor.getPreviousHeartbeat(child.id);
|
||||
|
||||
// Only change state if the monitor is in worse conditions then the ones before
|
||||
// lastBeat.status could be null
|
||||
if (!lastBeat) {
|
||||
heartbeat.status = PENDING;
|
||||
} else if (heartbeat.status === UP && (lastBeat.status === PENDING || lastBeat.status === DOWN)) {
|
||||
heartbeat.status = lastBeat.status;
|
||||
} else if (heartbeat.status === PENDING && lastBeat.status === DOWN) {
|
||||
heartbeat.status = lastBeat.status;
|
||||
}
|
||||
}
|
||||
|
||||
if (heartbeat.status !== UP) {
|
||||
heartbeat.msg = "Child inaccessible";
|
||||
}
|
||||
} else {
|
||||
if (children.length === 0) {
|
||||
// Set status pending if group is empty
|
||||
heartbeat.status = PENDING;
|
||||
heartbeat.msg = "Group empty";
|
||||
return;
|
||||
}
|
||||
|
||||
let worstStatus = UP;
|
||||
const downChildren = [];
|
||||
const pendingChildren = [];
|
||||
|
||||
for (const child of children) {
|
||||
if (!child.active) {
|
||||
// Ignore inactive (=paused) children
|
||||
continue;
|
||||
}
|
||||
|
||||
const label = child.name || `#${child.id}`;
|
||||
const lastBeat = await Monitor.getPreviousHeartbeat(child.id);
|
||||
|
||||
if (!lastBeat) {
|
||||
if (worstStatus === UP) {
|
||||
worstStatus = PENDING;
|
||||
}
|
||||
pendingChildren.push(label);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lastBeat.status === DOWN) {
|
||||
worstStatus = DOWN;
|
||||
downChildren.push(label);
|
||||
} else if (lastBeat.status === PENDING) {
|
||||
if (worstStatus !== DOWN) {
|
||||
worstStatus = PENDING;
|
||||
}
|
||||
pendingChildren.push(label);
|
||||
}
|
||||
}
|
||||
|
||||
if (worstStatus === UP) {
|
||||
heartbeat.status = UP;
|
||||
heartbeat.msg = "All children up and running";
|
||||
return;
|
||||
}
|
||||
|
||||
if (worstStatus === PENDING) {
|
||||
heartbeat.status = PENDING;
|
||||
heartbeat.msg = `Pending child monitors: ${pendingChildren.join(", ")}`;
|
||||
return;
|
||||
}
|
||||
|
||||
heartbeat.status = DOWN;
|
||||
|
||||
let message = `Child monitors down: ${downChildren.join(", ")}`;
|
||||
|
||||
if (pendingChildren.length > 0) {
|
||||
message += `; pending: ${pendingChildren.join(", ")}`;
|
||||
}
|
||||
|
||||
// Throw to leverage the generic retry handling and notification flow
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ class Telegram extends NotificationProvider {
|
||||
text: msg,
|
||||
disable_notification: notification.telegramSendSilently ?? false,
|
||||
protect_content: notification.telegramProtectContent ?? false,
|
||||
link_preview_options: { is_disabled: true },
|
||||
};
|
||||
if (notification.telegramMessageThreadID) {
|
||||
params.message_thread_id = notification.telegramMessageThreadID;
|
||||
@@ -30,9 +31,9 @@ class Telegram extends NotificationProvider {
|
||||
}
|
||||
}
|
||||
|
||||
let config = this.getAxiosConfigWithProxy({ params });
|
||||
let config = this.getAxiosConfigWithProxy();
|
||||
|
||||
await axios.get(`${url}/bot${notification.telegramBotToken}/sendMessage`, config);
|
||||
await axios.post(`${url}/bot${notification.telegramBotToken}/sendMessage`, params, config);
|
||||
return okMsg;
|
||||
|
||||
} catch (error) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<div :style="depthMargin">
|
||||
<div
|
||||
class="draggable-item"
|
||||
:style="depthMargin"
|
||||
:class="{ 'drag-over': dragOverCount > 0 }"
|
||||
@dragstart="onDragStart"
|
||||
@dragenter.prevent="onDragEnter"
|
||||
@dragleave.prevent="onDragLeave"
|
||||
@dragover.prevent
|
||||
@drop.prevent="onDrop"
|
||||
>
|
||||
<!-- Checkbox -->
|
||||
<div v-if="isSelectMode" class="select-input-wrapper">
|
||||
<input
|
||||
@@ -116,6 +125,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
isCollapsed: true,
|
||||
dragOverCount: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -187,6 +197,91 @@ export default {
|
||||
|
||||
window.localStorage.setItem("monitorCollapsed", JSON.stringify(storageObject));
|
||||
},
|
||||
/**
|
||||
* Initializes the drag operation if the monitor is draggable.
|
||||
* @param {DragEvent} event - The dragstart event triggered by the browser.
|
||||
* @returns {void} This method does not return anything.
|
||||
*/
|
||||
onDragStart(event) {
|
||||
try {
|
||||
event.dataTransfer.setData("text/monitor-id", String(this.monitor.id));
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
|
||||
onDragEnter(event) {
|
||||
if (this.monitor.type !== "group") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dragOverCount++;
|
||||
},
|
||||
|
||||
onDragLeave(event) {
|
||||
if (this.monitor.type !== "group") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dragOverCount = Math.max(0, this.dragOverCount - 1);
|
||||
},
|
||||
|
||||
async onDrop(event) {
|
||||
this.dragOverCount = 0;
|
||||
|
||||
// Only groups accept drops
|
||||
if (this.monitor.type !== "group") {
|
||||
return;
|
||||
}
|
||||
|
||||
const draggedId = event.dataTransfer.getData("text/monitor-id");
|
||||
if (!draggedId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const draggedMonitorId = parseInt(draggedId);
|
||||
if (isNaN(draggedMonitorId) || draggedMonitorId === this.monitor.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const draggedMonitor = this.$root.monitorList[draggedMonitorId];
|
||||
if (!draggedMonitor) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save original parent so we can revert locally if server returns error
|
||||
const originalParent = draggedMonitor.parent;
|
||||
|
||||
// Prepare a full monitor object (clone) and set new parent
|
||||
const monitorToSave = JSON.parse(JSON.stringify(draggedMonitor));
|
||||
monitorToSave.parent = this.monitor.id;
|
||||
|
||||
// Optimistically update local state so UI updates immediately
|
||||
this.$root.monitorList[draggedMonitorId].parent = this.monitor.id;
|
||||
|
||||
// Send updated monitor state via socket
|
||||
try {
|
||||
this.$root.getSocket().emit("editMonitor", monitorToSave, (res) => {
|
||||
if (!res || !res.ok) {
|
||||
// Revert local change on error
|
||||
if (this.$root.monitorList[draggedMonitorId]) {
|
||||
this.$root.monitorList[draggedMonitorId].parent = originalParent;
|
||||
}
|
||||
if (res && res.msg) {
|
||||
this.$root.toastError(res.msg);
|
||||
}
|
||||
} else {
|
||||
this.$root.toastRes(res);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
// revert on exception
|
||||
if (this.$root.monitorList[draggedMonitorId]) {
|
||||
this.$root.monitorList[draggedMonitorId].parent = originalParent;
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Get URL of monitor
|
||||
* @param {number} id ID of monitor
|
||||
@@ -253,4 +348,35 @@ export default {
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
.drag-over {
|
||||
border: 4px dashed $primary;
|
||||
border-radius: 0.5rem;
|
||||
background-color: $highlight-white;
|
||||
}
|
||||
|
||||
.dark {
|
||||
.drag-over {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
}
|
||||
|
||||
/* -4px on all due to border-width */
|
||||
.monitor-list .drag-over .item {
|
||||
padding: 9px 11px 6px 11px;
|
||||
}
|
||||
|
||||
.draggable-item {
|
||||
cursor: grab;
|
||||
position: relative;
|
||||
|
||||
/* We don't want the padding change due to the border animated */
|
||||
.item {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
&.dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -518,6 +518,12 @@
|
||||
"Effective Date Range": "Effective Date Range (Optional)",
|
||||
"Schedule Maintenance": "Schedule Maintenance",
|
||||
"Edit Maintenance": "Edit Maintenance",
|
||||
"Clone Maintenance": "Clone Maintenance",
|
||||
"ariaPauseMaintenance": "Pause this maintenance schedule",
|
||||
"ariaResumeMaintenance": "Resume this maintenance schedule",
|
||||
"ariaCloneMaintenance": "Create a copy of this maintenance schedule",
|
||||
"ariaEditMaintenance": "Edit this maintenance schedule",
|
||||
"ariaDeleteMaintenance": "Delete this maintenance schedule",
|
||||
"Date and Time": "Date and Time",
|
||||
"DateTime Range": "DateTime Range",
|
||||
"loadingError": "Cannot fetch the data, please try again later.",
|
||||
@@ -590,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.",
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -441,7 +441,23 @@
|
||||
:no-text="$t('No')"
|
||||
@yes="deleteMonitor"
|
||||
>
|
||||
{{ $t("deleteMonitorMsg") }}
|
||||
<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;
|
||||
|
||||
@@ -354,7 +354,14 @@ export default {
|
||||
},
|
||||
|
||||
pageName() {
|
||||
return this.$t((this.isAdd) ? "Schedule Maintenance" : "Edit Maintenance");
|
||||
let name = "Schedule Maintenance";
|
||||
|
||||
if (this.isEdit) {
|
||||
name = "Edit Maintenance";
|
||||
} else if (this.isClone) {
|
||||
name = "Clone Maintenance";
|
||||
}
|
||||
return this.$t(name);
|
||||
},
|
||||
|
||||
isAdd() {
|
||||
@@ -365,6 +372,9 @@ export default {
|
||||
return this.$route.path.startsWith("/maintenance/edit");
|
||||
},
|
||||
|
||||
isClone() {
|
||||
return this.$route.path.startsWith("/maintenance/clone");
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"$route.fullPath"() {
|
||||
@@ -443,11 +453,16 @@ export default {
|
||||
daysOfMonth: [],
|
||||
timezoneOption: null,
|
||||
};
|
||||
} else if (this.isEdit) {
|
||||
} else if (this.isEdit || this.isClone) {
|
||||
this.$root.getSocket().emit("getMaintenance", this.$route.params.id, (res) => {
|
||||
if (res.ok) {
|
||||
this.maintenance = res.maintenance;
|
||||
|
||||
if (this.isClone) {
|
||||
this.maintenance.id = undefined; // Remove id when cloning as we want a new id
|
||||
this.maintenance.title = this.$t("cloneOf", [ this.maintenance.title ]);
|
||||
}
|
||||
|
||||
this.$root.getSocket().emit("getMonitorMaintenance", this.$route.params.id, (res) => {
|
||||
if (res.ok) {
|
||||
Object.values(res.monitors).map(monitor => {
|
||||
@@ -491,7 +506,7 @@ export default {
|
||||
return this.processing = false;
|
||||
}
|
||||
|
||||
if (this.isAdd) {
|
||||
if (this.isAdd || this.isClone) {
|
||||
this.$root.addMaintenance(this.maintenance, async (res) => {
|
||||
if (res.ok) {
|
||||
await this.addMonitorMaintenance(res.maintenanceID, async () => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -41,19 +41,23 @@
|
||||
<router-link v-if="false" :to="maintenanceURL(item.id)" class="btn btn-light">{{ $t("Details") }}</router-link>
|
||||
|
||||
<div class="btn-group" role="group">
|
||||
<button v-if="item.active" class="btn btn-normal" @click="pauseDialog(item.id)">
|
||||
<button v-if="item.active" class="btn btn-normal" :aria-label="$t('ariaPauseMaintenance')" @click="pauseDialog(item.id)">
|
||||
<font-awesome-icon icon="pause" /> {{ $t("Pause") }}
|
||||
</button>
|
||||
|
||||
<button v-if="!item.active" class="btn btn-primary" @click="resumeMaintenance(item.id)">
|
||||
<button v-if="!item.active" class="btn btn-primary" :aria-label="$t('ariaResumeMaintenance')" @click="resumeMaintenance(item.id)">
|
||||
<font-awesome-icon icon="play" /> {{ $t("Resume") }}
|
||||
</button>
|
||||
|
||||
<router-link :to="'/maintenance/edit/' + item.id" class="btn btn-normal">
|
||||
<router-link :to="'/maintenance/clone/' + item.id" class="btn btn-normal" :aria-label="$t('ariaCloneMaintenance')">
|
||||
<font-awesome-icon icon="clone" /> {{ $t("Clone") }}
|
||||
</router-link>
|
||||
|
||||
<router-link :to="'/maintenance/edit/' + item.id" class="btn btn-normal" :aria-label="$t('ariaEditMaintenance')">
|
||||
<font-awesome-icon icon="edit" /> {{ $t("Edit") }}
|
||||
</router-link>
|
||||
|
||||
<button class="btn btn-normal text-danger" @click="deleteDialog(item.id)">
|
||||
<button class="btn btn-normal text-danger" :aria-label="$t('ariaDeleteMaintenance')" @click="deleteDialog(item.id)">
|
||||
<font-awesome-icon icon="trash" /> {{ $t("Delete") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -162,6 +162,10 @@ const routes = [
|
||||
path: "/maintenance/edit/:id",
|
||||
component: EditMaintenance,
|
||||
},
|
||||
{
|
||||
path: "/maintenance/clone/:id",
|
||||
component: EditMaintenance,
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user