Merge pull request #2693 from julian-piehl/group-monitors

Group monitors
This commit is contained in:
Louis Lam 2023-06-11 14:09:02 +08:00 committed by GitHub
commit c4c3fc81b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 516 additions and 57 deletions

View File

@ -0,0 +1,6 @@
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD parent INTEGER REFERENCES [monitor] ([id]) ON DELETE SET NULL ON UPDATE CASCADE;
COMMIT

View File

@ -69,6 +69,7 @@ class Database {
"patch-api-key-table.sql": true, "patch-api-key-table.sql": true,
"patch-monitor-tls.sql": true, "patch-monitor-tls.sql": true,
"patch-maintenance-cron.sql": true, "patch-maintenance-cron.sql": true,
"patch-add-parent-monitor.sql": true,
}; };
/** /**

View File

@ -74,13 +74,17 @@ class Monitor extends BeanModel {
id: this.id, id: this.id,
name: this.name, name: this.name,
description: this.description, description: this.description,
pathName: await this.getPathName(),
parent: this.parent,
childrenIDs: await Monitor.getAllChildrenIDs(this.id),
url: this.url, url: this.url,
method: this.method, method: this.method,
hostname: this.hostname, hostname: this.hostname,
port: this.port, port: this.port,
maxretries: this.maxretries, maxretries: this.maxretries,
weight: this.weight, weight: this.weight,
active: this.active, active: await this.isActive(),
forceInactive: !await Monitor.isParentActive(this.id),
type: this.type, type: this.type,
interval: this.interval, interval: this.interval,
retryInterval: this.retryInterval, retryInterval: this.retryInterval,
@ -144,6 +148,16 @@ class Monitor extends BeanModel {
return data; return data;
} }
/**
* Checks if the monitor is active based on itself and its parents
* @returns {Promise<Boolean>}
*/
async isActive() {
const parentActive = await Monitor.isParentActive(this.id);
return this.active && parentActive;
}
/** /**
* Get all tags applied to this monitor * Get all tags applied to this monitor
* @returns {Promise<LooseObject<any>[]>} * @returns {Promise<LooseObject<any>[]>}
@ -259,6 +273,36 @@ class Monitor extends BeanModel {
if (await Monitor.isUnderMaintenance(this.id)) { if (await Monitor.isUnderMaintenance(this.id)) {
bean.msg = "Monitor under maintenance"; bean.msg = "Monitor under maintenance";
bean.status = MAINTENANCE; bean.status = MAINTENANCE;
} else if (this.type === "group") {
const children = await Monitor.getChildren(this.id);
if (children.length > 0) {
bean.status = UP;
bean.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
if (bean.status === UP && (lastBeat.status === PENDING || lastBeat.status === DOWN)) {
bean.status = lastBeat.status;
} else if (bean.status === PENDING && lastBeat.status === DOWN) {
bean.status = lastBeat.status;
}
}
if (bean.status !== UP) {
bean.msg = "Child inaccessible";
}
} else {
// Set status pending if group is empty
bean.status = PENDING;
bean.msg = "Group empty";
}
} else if (this.type === "http" || this.type === "keyword") { } else if (this.type === "http" || this.type === "keyword") {
// Do not do any queries/high loading things before the "bean.ping" // Do not do any queries/high loading things before the "bean.ping"
let startTime = dayjs().valueOf(); let startTime = dayjs().valueOf();
@ -1329,6 +1373,11 @@ class Monitor extends BeanModel {
} }
} }
const parent = await Monitor.getParent(monitorID);
if (parent != null) {
return await Monitor.isUnderMaintenance(parent.id);
}
return false; return false;
} }
@ -1341,6 +1390,94 @@ class Monitor extends BeanModel {
throw new Error(`Interval cannot be less than ${MIN_INTERVAL_SECOND} seconds`); throw new Error(`Interval cannot be less than ${MIN_INTERVAL_SECOND} seconds`);
} }
} }
/**
* Gets Parent of the monitor
* @param {number} monitorID ID of monitor to get
* @returns {Promise<LooseObject<any>>}
*/
static async getParent(monitorID) {
return await R.getRow(`
SELECT parent.* FROM monitor parent
LEFT JOIN monitor child
ON child.parent = parent.id
WHERE child.id = ?
`, [
monitorID,
]);
}
/**
* Gets all Children of the monitor
* @param {number} monitorID ID of monitor to get
* @returns {Promise<LooseObject<any>>}
*/
static async getChildren(monitorID) {
return await R.getAll(`
SELECT * FROM monitor
WHERE parent = ?
`, [
monitorID,
]);
}
/**
* Gets Full Path-Name (Groups and Name)
* @returns {Promise<String>}
*/
async getPathName() {
let path = this.name;
if (this.parent === null) {
return path;
}
let parent = await Monitor.getParent(this.id);
while (parent !== null) {
path = `${parent.name} / ${path}`;
parent = await Monitor.getParent(parent.id);
}
return path;
}
/**
* Gets recursive all child ids
* @param {number} monitorID ID of the monitor to get
* @returns {Promise<Array>}
*/
static async getAllChildrenIDs(monitorID) {
const childs = await Monitor.getChildren(monitorID);
if (childs === null) {
return [];
}
let childrenIDs = [];
for (const child of childs) {
childrenIDs.push(child.id);
childrenIDs = childrenIDs.concat(await Monitor.getAllChildrenIDs(child.id));
}
return childrenIDs;
}
/**
* Checks recursive if parent (ancestors) are active
* @param {number} monitorID ID of the monitor to get
* @returns {Promise<Boolean>}
*/
static async isParentActive(monitorID) {
const parent = await Monitor.getParent(monitorID);
if (parent === null) {
return true;
}
const parentActive = await Monitor.isParentActive(parent.id);
return parent.active && parentActive;
}
} }
module.exports = Monitor; module.exports = Monitor;

View File

@ -684,8 +684,17 @@ let needSetup = false;
throw new Error("Permission denied."); throw new Error("Permission denied.");
} }
// Check if Parent is Decendant (would cause endless loop)
if (monitor.parent !== null) {
const childIDs = await Monitor.getAllChildrenIDs(monitor.id);
if (childIDs.includes(monitor.parent)) {
throw new Error("Invalid Monitor Group");
}
}
bean.name = monitor.name; bean.name = monitor.name;
bean.description = monitor.description; bean.description = monitor.description;
bean.parent = monitor.parent;
bean.type = monitor.type; bean.type = monitor.type;
bean.url = monitor.url; bean.url = monitor.url;
bean.method = monitor.method; bean.method = monitor.method;
@ -745,7 +754,7 @@ let needSetup = false;
await updateMonitorNotification(bean.id, monitor.notificationIDList); await updateMonitorNotification(bean.id, monitor.notificationIDList);
if (bean.active) { if (bean.isActive()) {
await restartMonitor(socket.userID, bean.id); await restartMonitor(socket.userID, bean.id);
} }

View File

@ -186,7 +186,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
log.debug("maintenance", `Get Monitors for Maintenance: ${maintenanceID} User ID: ${socket.userID}`); log.debug("maintenance", `Get Monitors for Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
let monitors = await R.getAll("SELECT monitor.id, monitor.name FROM monitor_maintenance mm JOIN monitor ON mm.monitor_id = monitor.id WHERE mm.maintenance_id = ? ", [ let monitors = await R.getAll("SELECT monitor.id FROM monitor_maintenance mm JOIN monitor ON mm.monitor_id = monitor.id WHERE mm.maintenance_id = ? ", [
maintenanceID, maintenanceID,
]); ]);

View File

@ -19,43 +19,18 @@
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link> {{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
</div> </div>
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }" :title="item.description"> <MonitorListItem v-for="(item, index) in sortedMonitorList" :key="index" :monitor="item" :isSearch="searchText !== ''" />
<div class="row">
<div class="col-9 col-md-8 small-padding" :class="{ 'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
<div class="info">
<Uptime :monitor="item" type="24" :pill="true" />
{{ item.name }}
</div>
<div class="tags">
<Tag v-for="tag in item.tags" :key="tag" :item="tag" :size="'sm'" />
</div>
</div>
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-3 col-md-4">
<HeartbeatBar size="small" :monitor-id="item.id" />
</div>
</div>
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
<div class="col-12 bottom-style">
<HeartbeatBar size="small" :monitor-id="item.id" />
</div>
</div>
</router-link>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import HeartbeatBar from "../components/HeartbeatBar.vue"; import MonitorListItem from "../components/MonitorListItem.vue";
import Tag from "../components/Tag.vue";
import Uptime from "../components/Uptime.vue";
import { getMonitorRelativeURL } from "../util.ts"; import { getMonitorRelativeURL } from "../util.ts";
export default { export default {
components: { components: {
Uptime, MonitorListItem,
HeartbeatBar,
Tag,
}, },
props: { props: {
/** Should the scrollbar be shown */ /** Should the scrollbar be shown */
@ -91,6 +66,20 @@ export default {
sortedMonitorList() { sortedMonitorList() {
let result = Object.values(this.$root.monitorList); let result = Object.values(this.$root.monitorList);
// Simple filter by search text
// finds monitor name, tag name or tag value
if (this.searchText !== "") {
const loweredSearchText = this.searchText.toLowerCase();
result = result.filter(monitor => {
return monitor.name.toLowerCase().includes(loweredSearchText)
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|| tag.value?.toLowerCase().includes(loweredSearchText));
});
} else {
result = result.filter(monitor => monitor.parent === null);
}
// Filter result by active state, weight and alphabetical
result.sort((m1, m2) => { result.sort((m1, m2) => {
if (m1.active !== m2.active) { if (m1.active !== m2.active) {
@ -116,17 +105,6 @@ export default {
return m1.name.localeCompare(m2.name); return m1.name.localeCompare(m2.name);
}); });
// Simple filter by search text
// finds monitor name, tag name or tag value
if (this.searchText !== "") {
const loweredSearchText = this.searchText.toLowerCase();
result = result.filter(monitor => {
return monitor.name.toLowerCase().includes(loweredSearchText)
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|| tag.value?.toLowerCase().includes(loweredSearchText));
});
}
return result; return result;
}, },
}, },

View File

@ -0,0 +1,204 @@
<template>
<div>
<router-link :to="monitorURL(monitor.id)" class="item" :class="{ 'disabled': ! monitor.active }">
<div class="row">
<div class="col-9 col-md-8 small-padding" :class="{ 'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
<div class="info" :style="depthMargin">
<Uptime :monitor="monitor" type="24" :pill="true" />
<span v-if="hasChildren" class="collapse-padding" @click.prevent="changeCollapsed">
<font-awesome-icon icon="chevron-down" class="animated" :class="{ collapsed: isCollapsed}" />
</span>
{{ monitorName }}
</div>
<div class="tags">
<Tag v-for="tag in monitor.tags" :key="tag" :item="tag" :size="'sm'" />
</div>
</div>
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-3 col-md-4">
<HeartbeatBar size="small" :monitor-id="monitor.id" />
</div>
</div>
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
<div class="col-12 bottom-style">
<HeartbeatBar size="small" :monitor-id="monitor.id" />
</div>
</div>
</router-link>
<transition name="slide-fade-up">
<div v-if="!isCollapsed" class="childs">
<MonitorListItem v-for="(item, index) in sortedChildMonitorList" :key="index" :monitor="item" :isSearch="isSearch" :depth="depth + 1" />
</div>
</transition>
</div>
</template>
<script>
import HeartbeatBar from "../components/HeartbeatBar.vue";
import Tag from "../components/Tag.vue";
import Uptime from "../components/Uptime.vue";
import { getMonitorRelativeURL } from "../util.ts";
export default {
name: "MonitorListItem",
components: {
Uptime,
HeartbeatBar,
Tag,
},
props: {
/** Monitor this represents */
monitor: {
type: Object,
default: null,
},
/** If the user is currently searching */
isSearch: {
type: Boolean,
default: false,
},
/** How many ancestors are above this monitor */
depth: {
type: Number,
default: 0,
},
},
data() {
return {
isCollapsed: true,
};
},
computed: {
sortedChildMonitorList() {
let result = Object.values(this.$root.monitorList);
result = result.filter(childMonitor => childMonitor.parent === this.monitor.id);
result.sort((m1, m2) => {
if (m1.active !== m2.active) {
if (m1.active === 0) {
return 1;
}
if (m2.active === 0) {
return -1;
}
}
if (m1.weight !== m2.weight) {
if (m1.weight > m2.weight) {
return -1;
}
if (m1.weight < m2.weight) {
return 1;
}
}
return m1.name.localeCompare(m2.name);
});
return result;
},
hasChildren() {
return this.sortedChildMonitorList.length > 0;
},
depthMargin() {
return {
marginLeft: `${31 * this.depth}px`,
};
},
monitorName() {
if (this.isSearch) {
return this.monitor.pathName;
} else {
return this.monitor.name;
}
}
},
beforeMount() {
// Always unfold if monitor is accessed directly
if (this.monitor.childrenIDs.includes(parseInt(this.$route.params.id))) {
this.isCollapsed = false;
return;
}
// Set collapsed value based on local storage
let storage = window.localStorage.getItem("monitorCollapsed");
if (storage === null) {
return;
}
let storageObject = JSON.parse(storage);
if (storageObject[`monitor_${this.monitor.id}`] == null) {
return;
}
this.isCollapsed = storageObject[`monitor_${this.monitor.id}`];
},
methods: {
/**
* Changes the collapsed value of the current monitor and saves it to local storage
*/
changeCollapsed() {
this.isCollapsed = !this.isCollapsed;
// Save collapsed value into local storage
let storage = window.localStorage.getItem("monitorCollapsed");
let storageObject = {};
if (storage !== null) {
storageObject = JSON.parse(storage);
}
storageObject[`monitor_${this.monitor.id}`] = this.isCollapsed;
window.localStorage.setItem("monitorCollapsed", JSON.stringify(storageObject));
},
/**
* Get URL of monitor
* @param {number} id ID of monitor
* @returns {string} Relative URL of monitor
*/
monitorURL(id) {
return getMonitorRelativeURL(id);
},
},
};
</script>
<style lang="scss" scoped>
@import "../assets/vars.scss";
.small-padding {
padding-left: 5px !important;
padding-right: 5px !important;
}
.collapse-padding {
padding-left: 8px !important;
padding-right: 2px !important;
}
// .monitor-item {
// width: 100%;
// }
.tags {
margin-top: 4px;
padding-left: 67px;
display: flex;
flex-wrap: wrap;
gap: 0;
}
.collapsed {
transform: rotate(-90deg);
}
.animated {
transition: all 0.2s $easing-in;
}
</style>

View File

@ -752,5 +752,7 @@
"endDateTime": "Ende Datum/Uhrzeit", "endDateTime": "Ende Datum/Uhrzeit",
"cronExpression": "Cron-Ausdruck", "cronExpression": "Cron-Ausdruck",
"cronSchedule": "Zeitplan: ", "cronSchedule": "Zeitplan: ",
"invalidCronExpression": "Ungültiger Cron-Ausdruck: {0}" "invalidCronExpression": "Ungültiger Cron-Ausdruck: {0}",
"Group": "Gruppe",
"Monitor Group": "Monitor Gruppe"
} }

View File

@ -744,5 +744,7 @@
"Badge Down Days": "Badge Down Days", "Badge Down Days": "Badge Down Days",
"Badge Style": "Badge Style", "Badge Style": "Badge Style",
"Badge value (For Testing only.)": "Badge value (For Testing only.)", "Badge value (For Testing only.)": "Badge value (For Testing only.)",
"Badge URL": "Badge URL" "Badge URL": "Badge URL",
"Group": "Group",
"Monitor Group": "Monitor Group"
} }

View File

@ -1,6 +1,7 @@
<template> <template>
<transition name="slide-fade" appear> <transition name="slide-fade" appear>
<div v-if="monitor"> <div v-if="monitor">
<router-link v-if="group !== ''" :to="monitorURL(monitor.parent)"> {{ group }}</router-link>
<h1> {{ monitor.name }}</h1> <h1> {{ monitor.name }}</h1>
<p v-if="monitor.description">{{ monitor.description }}</p> <p v-if="monitor.description">{{ monitor.description }}</p>
<div class="tags"> <div class="tags">
@ -40,7 +41,7 @@
<button v-if="monitor.active" class="btn btn-normal" @click="pauseDialog"> <button v-if="monitor.active" class="btn btn-normal" @click="pauseDialog">
<font-awesome-icon icon="pause" /> {{ $t("Pause") }} <font-awesome-icon icon="pause" /> {{ $t("Pause") }}
</button> </button>
<button v-if="! monitor.active" class="btn btn-primary" @click="resumeMonitor"> <button v-if="! monitor.active" class="btn btn-primary" :disabled="monitor.forceInactive" @click="resumeMonitor">
<font-awesome-icon icon="play" /> {{ $t("Resume") }} <font-awesome-icon icon="play" /> {{ $t("Resume") }}
</button> </button>
<router-link :to=" '/edit/' + monitor.id " class="btn btn-normal"> <router-link :to=" '/edit/' + monitor.id " class="btn btn-normal">
@ -69,7 +70,7 @@
<div class="shadow-box big-padding text-center stats"> <div class="shadow-box big-padding text-center stats">
<div class="row"> <div class="row">
<div class="col-12 col-sm col row d-flex align-items-center d-sm-block"> <div v-if="monitor.type !== 'group'" class="col-12 col-sm col row d-flex align-items-center d-sm-block">
<h4 class="col-4 col-sm-12">{{ pingTitle() }}</h4> <h4 class="col-4 col-sm-12">{{ pingTitle() }}</h4>
<p class="col-4 col-sm-12 mb-0 mb-sm-2">({{ $t("Current") }})</p> <p class="col-4 col-sm-12 mb-0 mb-sm-2">({{ $t("Current") }})</p>
<span class="col-4 col-sm-12 num"> <span class="col-4 col-sm-12 num">
@ -78,7 +79,7 @@
</a> </a>
</span> </span>
</div> </div>
<div class="col-12 col-sm col row d-flex align-items-center d-sm-block"> <div v-if="monitor.type !== 'group'" class="col-12 col-sm col row d-flex align-items-center d-sm-block">
<h4 class="col-4 col-sm-12">{{ pingTitle(true) }}</h4> <h4 class="col-4 col-sm-12">{{ pingTitle(true) }}</h4>
<p class="col-4 col-sm-12 mb-0 mb-sm-2">(24{{ $t("-hour") }})</p> <p class="col-4 col-sm-12 mb-0 mb-sm-2">(24{{ $t("-hour") }})</p>
<span class="col-4 col-sm-12 num"> <span class="col-4 col-sm-12 num">
@ -214,6 +215,7 @@ import Pagination from "v-pagination-3";
const PingChart = defineAsyncComponent(() => import("../components/PingChart.vue")); const PingChart = defineAsyncComponent(() => import("../components/PingChart.vue"));
import Tag from "../components/Tag.vue"; import Tag from "../components/Tag.vue";
import CertificateInfo from "../components/CertificateInfo.vue"; import CertificateInfo from "../components/CertificateInfo.vue";
import { getMonitorRelativeURL } from "../util.ts";
import { URL } from "whatwg-url"; import { URL } from "whatwg-url";
export default { export default {
@ -313,6 +315,13 @@ export default {
return this.heartBeatList.slice(startIndex, endIndex); return this.heartBeatList.slice(startIndex, endIndex);
}, },
group() {
if (!this.monitor.pathName.includes("/")) {
return "";
}
return this.monitor.pathName.substr(0, this.monitor.pathName.lastIndexOf("/"));
},
pushURL() { pushURL() {
return this.$root.baseURL + "/api/push/" + this.monitor.pushToken + "?status=up&msg=OK&ping="; return this.$root.baseURL + "/api/push/" + this.monitor.pushToken + "?status=up&msg=OK&ping=";
}, },
@ -409,6 +418,15 @@ export default {
return this.$t(translationPrefix + "Ping"); return this.$t(translationPrefix + "Ping");
}, },
/**
* Get URL of monitor
* @param {number} id ID of monitor
* @returns {string} Relative URL of monitor
*/
monitorURL(id) {
return getMonitorRelativeURL(id);
},
/** Filter and hide password in URL for display */ /** Filter and hide password in URL for display */
filterPassword(urlString) { filterPassword(urlString) {
try { try {

View File

@ -36,7 +36,7 @@
v-model="affectedMonitors" v-model="affectedMonitors"
:options="affectedMonitorsOptions" :options="affectedMonitorsOptions"
track-by="id" track-by="id"
label="name" label="pathName"
:multiple="true" :multiple="true"
:close-on-select="false" :close-on-select="false"
:clear-on-select="false" :clear-on-select="false"
@ -381,17 +381,39 @@ export default {
}, },
}, },
mounted() { mounted() {
this.init();
this.$root.getMonitorList((res) => { this.$root.getMonitorList((res) => {
if (res.ok) { if (res.ok) {
Object.values(this.$root.monitorList).map(monitor => { Object.values(this.$root.monitorList).sort((m1, m2) => {
if (m1.active !== m2.active) {
if (m1.active === 0) {
return 1;
}
if (m2.active === 0) {
return -1;
}
}
if (m1.weight !== m2.weight) {
if (m1.weight > m2.weight) {
return -1;
}
if (m1.weight < m2.weight) {
return 1;
}
}
return m1.pathName.localeCompare(m2.pathName);
}).map(monitor => {
this.affectedMonitorsOptions.push({ this.affectedMonitorsOptions.push({
id: monitor.id, id: monitor.id,
name: monitor.name, pathName: monitor.pathName,
}); });
}); });
} }
this.init();
}); });
}, },
methods: { methods: {
@ -429,7 +451,7 @@ export default {
this.$root.getSocket().emit("getMonitorMaintenance", this.$route.params.id, (res) => { this.$root.getSocket().emit("getMonitorMaintenance", this.$route.params.id, (res) => {
if (res.ok) { if (res.ok) {
Object.values(res.monitors).map(monitor => { Object.values(res.monitors).map(monitor => {
this.affectedMonitors.push(monitor); this.affectedMonitors.push(this.affectedMonitorsOptions.find(item => item.id === monitor.id));
}); });
} else { } else {
toast.error(res.msg); toast.error(res.msg);

View File

@ -12,6 +12,9 @@
<label for="type" class="form-label">{{ $t("Monitor Type") }}</label> <label for="type" class="form-label">{{ $t("Monitor Type") }}</label>
<select id="type" v-model="monitor.type" class="form-select"> <select id="type" v-model="monitor.type" class="form-select">
<optgroup :label="$t('General Monitor Type')"> <optgroup :label="$t('General Monitor Type')">
<option value="group">
{{ $t("Group") }}
</option>
<option value="http"> <option value="http">
HTTP(s) HTTP(s)
</option> </option>
@ -89,6 +92,15 @@
<input id="name" v-model="monitor.name" type="text" class="form-control" required> <input id="name" v-model="monitor.name" type="text" class="form-control" required>
</div> </div>
<!-- Parent Monitor -->
<div class="my-3">
<label for="parent" class="form-label">{{ $t("Monitor Group") }}</label>
<select v-model="monitor.parent" class="form-select" :disabled="sortedMonitorList.length === 0">
<option :value="null" selected>{{ $t("None") }}</option>
<option v-for="parentMonitor in sortedMonitorList" :key="parentMonitor.id" :value="parentMonitor.id">{{ parentMonitor.pathName }}</option>
</select>
</div>
<!-- URL --> <!-- URL -->
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'browser' " class="my-3"> <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'browser' " class="my-3">
<label for="url" class="form-label">{{ $t("URL") }}</label> <label for="url" class="form-label">{{ $t("URL") }}</label>
@ -807,6 +819,48 @@ message HealthCheckResponse {
return null; return null;
}, },
// Filter result by active state, weight and alphabetical
// Only return groups which arent't itself and one of its decendants
sortedMonitorList() {
let result = Object.values(this.$root.monitorList);
console.log(this.monitor.childrenIDs);
// Only groups, not itself, not a decendant
result = result.filter(
monitor => monitor.type === "group" &&
monitor.id !== this.monitor.id &&
!this.monitor.childrenIDs?.includes(monitor.id)
);
// Filter result by active state, weight and alphabetical
result.sort((m1, m2) => {
if (m1.active !== m2.active) {
if (m1.active === 0) {
return 1;
}
if (m2.active === 0) {
return -1;
}
}
if (m1.weight !== m2.weight) {
if (m1.weight > m2.weight) {
return -1;
}
if (m1.weight < m2.weight) {
return 1;
}
}
return m1.pathName.localeCompare(m2.pathName);
});
return result;
},
}, },
watch: { watch: {
"$root.proxyList"() { "$root.proxyList"() {
@ -926,6 +980,7 @@ message HealthCheckResponse {
this.monitor = { this.monitor = {
type: "http", type: "http",
name: "", name: "",
parent: null,
url: "https://", url: "https://",
method: "GET", method: "GET",
interval: 60, interval: 60,

View File

@ -278,11 +278,11 @@
</div> </div>
<div class="mt-3"> <div class="mt-3">
<div v-if="allMonitorList.length > 0 && loadedData"> <div v-if="sortedMonitorList.length > 0 && loadedData">
<label>{{ $t("Add a monitor") }}:</label> <label>{{ $t("Add a monitor") }}:</label>
<VueMultiselect <VueMultiselect
v-model="selectedMonitor" v-model="selectedMonitor"
:options="allMonitorList" :options="sortedMonitorList"
:multiple="false" :multiple="false"
:searchable="true" :searchable="true"
:placeholder="$t('Add a monitor')" :placeholder="$t('Add a monitor')"
@ -292,7 +292,7 @@
> >
<template #option="{ option }"> <template #option="{ option }">
<div class="d-inline-flex"> <div class="d-inline-flex">
<span>{{ option.name }} <Tag v-for="tag in option.tags" :key="tag" :item="tag" :size="'sm'" /></span> <span>{{ option.pathName }} <Tag v-for="tag in option.tags" :key="tag" :item="tag" :size="'sm'" /></span>
</div> </div>
</template> </template>
</VueMultiselect> </VueMultiselect>
@ -449,7 +449,7 @@ export default {
/** /**
* If the monitor is added to public list, which will not be in this list. * If the monitor is added to public list, which will not be in this list.
*/ */
allMonitorList() { sortedMonitorList() {
let result = []; let result = [];
for (let id in this.$root.monitorList) { for (let id in this.$root.monitorList) {
@ -459,6 +459,31 @@ export default {
} }
} }
result.sort((m1, m2) => {
if (m1.active !== m2.active) {
if (m1.active === 0) {
return 1;
}
if (m2.active === 0) {
return -1;
}
}
if (m1.weight !== m2.weight) {
if (m1.weight > m2.weight) {
return -1;
}
if (m1.weight < m2.weight) {
return 1;
}
}
return m1.pathName.localeCompare(m2.pathName);
});
return result; return result;
}, },