Merge pull request #2693 from julian-piehl/group-monitors
Group monitors
This commit is contained in:
commit
c4c3fc81b2
|
@ -0,0 +1,6 @@
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
ALTER TABLE monitor
|
||||||
|
ADD parent INTEGER REFERENCES [monitor] ([id]) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
COMMIT
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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>
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue