mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-08-19 03:27:52 -04:00
Merge branch 'master' into 1.23.14-to-2.0.0
# Conflicts: # src/lang/en.json # src/util.js # src/util.ts
This commit is contained in:
commit
cc52ee3feb
440 changed files with 26491 additions and 16428 deletions
|
@ -92,11 +92,9 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { Modal } from "bootstrap";
|
||||
import { useToast } from "vue-toastification";
|
||||
import dayjs from "dayjs";
|
||||
import Datepicker from "@vuepic/vue-datepicker";
|
||||
import CopyableInput from "./CopyableInput.vue";
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -126,6 +124,7 @@ export default {
|
|||
methods: {
|
||||
/**
|
||||
* Show modal
|
||||
* @returns {void}
|
||||
*/
|
||||
show() {
|
||||
this.id = null;
|
||||
|
@ -138,7 +137,10 @@ export default {
|
|||
this.keyaddmodal.show();
|
||||
},
|
||||
|
||||
/** Submit data to server */
|
||||
/**
|
||||
* Submit data to server
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async submit() {
|
||||
this.processing = true;
|
||||
|
||||
|
@ -154,12 +156,15 @@ export default {
|
|||
this.keymodal.show();
|
||||
this.clearForm();
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
this.$root.toastError(res.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/** Clear Form inputs */
|
||||
/**
|
||||
* Clear Form inputs
|
||||
* @returns {void}
|
||||
*/
|
||||
clearForm() {
|
||||
this.key = {
|
||||
name: "",
|
||||
|
|
|
@ -279,8 +279,9 @@ export default {
|
|||
methods: {
|
||||
/**
|
||||
* Setting monitor
|
||||
* @param {number} monitorId ID of monitor
|
||||
* @param {string} monitorName Name of monitor
|
||||
* @param {number} monitorId ID of monitor
|
||||
* @param {string} monitorName Name of monitor
|
||||
* @returns {void}
|
||||
*/
|
||||
show(monitorId, monitorName) {
|
||||
this.monitor = {
|
||||
|
|
|
@ -65,9 +65,9 @@ export default {
|
|||
methods: {
|
||||
/**
|
||||
* Format the subject of the certificate
|
||||
* @param {Object} subject Object representing the certificates
|
||||
* @param {object} subject Object representing the certificates
|
||||
* subject
|
||||
* @returns {string}
|
||||
* @returns {string} Certificate subject
|
||||
*/
|
||||
formatSubject(subject) {
|
||||
if (subject.O && subject.CN && subject.C) {
|
||||
|
|
|
@ -58,18 +58,23 @@ export default {
|
|||
this.modal = new Modal(this.$refs.modal);
|
||||
},
|
||||
methods: {
|
||||
/** Show the confirm dialog */
|
||||
/**
|
||||
* Show the confirm dialog
|
||||
* @returns {void}
|
||||
*/
|
||||
show() {
|
||||
this.modal.show();
|
||||
},
|
||||
/**
|
||||
* @emits string "yes" Notify the parent when Yes is pressed
|
||||
* @fires string "yes" Notify the parent when Yes is pressed
|
||||
* @returns {void}
|
||||
*/
|
||||
yes() {
|
||||
this.$emit("yes");
|
||||
},
|
||||
/**
|
||||
* @emits string "no" Notify the parent when No is pressed
|
||||
* @fires string "no" Notify the parent when No is pressed
|
||||
* @returns {void}
|
||||
*/
|
||||
no() {
|
||||
this.$emit("no");
|
||||
|
|
|
@ -90,19 +90,25 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
|
||||
/** Show the input */
|
||||
/**
|
||||
* Show the input
|
||||
* @returns {void}
|
||||
*/
|
||||
showInput() {
|
||||
this.visibility = "text";
|
||||
},
|
||||
|
||||
/** Hide the input */
|
||||
/**
|
||||
* Hide the input
|
||||
* @returns {void}
|
||||
*/
|
||||
hideInput() {
|
||||
this.visibility = "password";
|
||||
},
|
||||
|
||||
/**
|
||||
* Copy the provided text to the users clipboard
|
||||
* @param {string} textToCopy
|
||||
* @param {string} textToCopy Text to copy to clipboard
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
copyToClipboard(textToCopy) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<span v-if="isNum" ref="output">{{ output }}</span> <span v-if="isNum">{{ unit }}</span>
|
||||
<span v-if="isNum" ref="output">{{ outputFixed }}</span> <span v-if="isNum">{{ unit }}</span>
|
||||
<span v-else>{{ value }}</span>
|
||||
</template>
|
||||
|
||||
|
@ -37,6 +37,19 @@ export default {
|
|||
isNum() {
|
||||
return typeof this.value === "number";
|
||||
},
|
||||
outputFixed() {
|
||||
if (typeof this.output === "number") {
|
||||
if (this.output < 1) {
|
||||
return "<1";
|
||||
} else if (Number.isInteger(this.output)) {
|
||||
return this.output;
|
||||
} else {
|
||||
return this.output.toFixed(2);
|
||||
}
|
||||
} else {
|
||||
return this.output;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
|
|
@ -43,10 +43,17 @@ export default {
|
|||
this.modal = new Modal(this.$refs.modal);
|
||||
},
|
||||
methods: {
|
||||
/** Show the confirm dialog */
|
||||
/**
|
||||
* Show the confirm dialog
|
||||
* @returns {void}
|
||||
*/
|
||||
show() {
|
||||
this.modal.show();
|
||||
},
|
||||
/**
|
||||
* Dialog confirmed
|
||||
* @returns {void}
|
||||
*/
|
||||
confirm() {
|
||||
this.$emit("added", this.groupName);
|
||||
this.modal.hide();
|
||||
|
|
|
@ -62,8 +62,6 @@
|
|||
<script lang="ts">
|
||||
import { Modal } from "bootstrap";
|
||||
import Confirm from "./Confirm.vue";
|
||||
import { useToast } from "vue-toastification";
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -91,7 +89,10 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
|
||||
/** Confirm deletion of docker host */
|
||||
/**
|
||||
* Confirm deletion of docker host
|
||||
* @returns {void}
|
||||
*/
|
||||
deleteConfirm() {
|
||||
this.modal.hide();
|
||||
this.$refs.confirmDelete.show();
|
||||
|
@ -99,7 +100,8 @@ export default {
|
|||
|
||||
/**
|
||||
* Show specified docker host
|
||||
* @param {number} dockerHostID
|
||||
* @param {number} dockerHostID ID of host to show
|
||||
* @returns {void}
|
||||
*/
|
||||
show(dockerHostID) {
|
||||
if (dockerHostID) {
|
||||
|
@ -116,7 +118,7 @@ export default {
|
|||
}
|
||||
|
||||
if (!found) {
|
||||
toast.error("Docker Host not found!");
|
||||
this.$root.toastError("Docker Host not found!");
|
||||
}
|
||||
|
||||
} else {
|
||||
|
@ -131,7 +133,10 @@ export default {
|
|||
this.modal.show();
|
||||
},
|
||||
|
||||
/** Add docker host */
|
||||
/**
|
||||
* Add docker host
|
||||
* @returns {void}
|
||||
*/
|
||||
submit() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("addDockerHost", this.dockerHost, this.id, (res) => {
|
||||
|
@ -150,7 +155,10 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
/** Test the docker host */
|
||||
/**
|
||||
* Test the docker host
|
||||
* @returns {void}
|
||||
*/
|
||||
test() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("testDockerHost", this.dockerHost, (res) => {
|
||||
|
@ -159,7 +167,10 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
/** Delete this docker host */
|
||||
/**
|
||||
* Delete this docker host
|
||||
* @returns {void}
|
||||
*/
|
||||
deleteDockerHost() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("deleteDockerHost", this.id, (res) => {
|
||||
|
|
|
@ -56,6 +56,7 @@ export default {
|
|||
|
||||
/**
|
||||
* If heartbeatList is null, get it from $root.heartbeatList
|
||||
* @returns {object} Heartbeat list
|
||||
*/
|
||||
beatList() {
|
||||
if (this.heartbeatList === null) {
|
||||
|
@ -67,8 +68,7 @@ export default {
|
|||
|
||||
/**
|
||||
* Calculates the amount of beats of padding needed to fill the length of shortBeatList.
|
||||
*
|
||||
* @return {number} The amount of beats of padding needed to fill the length of shortBeatList.
|
||||
* @returns {number} The amount of beats of padding needed to fill the length of shortBeatList.
|
||||
*/
|
||||
numPadding() {
|
||||
if (!this.beatList) {
|
||||
|
@ -148,7 +148,7 @@ export default {
|
|||
|
||||
/**
|
||||
* Returns the style object for positioning the time element.
|
||||
* @return {Object} The style object containing the CSS properties for positioning the time element.
|
||||
* @returns {object} The style object containing the CSS properties for positioning the time element.
|
||||
*/
|
||||
timeStyle() {
|
||||
return {
|
||||
|
@ -158,8 +158,7 @@ export default {
|
|||
|
||||
/**
|
||||
* Calculates the time elapsed since the first valid beat.
|
||||
*
|
||||
* @return {string} The time elapsed in minutes or hours.
|
||||
* @returns {string} The time elapsed in minutes or hours.
|
||||
*/
|
||||
timeSinceFirstBeat() {
|
||||
const firstValidBeat = this.shortBeatList.at(this.numPadding);
|
||||
|
@ -173,8 +172,7 @@ export default {
|
|||
|
||||
/**
|
||||
* Calculates the elapsed time since the last valid beat was registered.
|
||||
*
|
||||
* @return {string} The elapsed time in a minutes, hours or "now".
|
||||
* @returns {string} The elapsed time in a minutes, hours or "now".
|
||||
*/
|
||||
timeSinceLastBeat() {
|
||||
const lastValidBeat = this.shortBeatList.at(-1);
|
||||
|
@ -241,7 +239,10 @@ export default {
|
|||
this.resize();
|
||||
},
|
||||
methods: {
|
||||
/** Resize the heartbeat bar */
|
||||
/**
|
||||
* Resize the heartbeat bar
|
||||
* @returns {void}
|
||||
*/
|
||||
resize() {
|
||||
if (this.$refs.wrap) {
|
||||
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2));
|
||||
|
@ -251,8 +252,8 @@ export default {
|
|||
/**
|
||||
* Get the title of the beat.
|
||||
* Used as the hover tooltip on the heartbeat bar.
|
||||
* @param {Object} beat Beat to get title from
|
||||
* @returns {string}
|
||||
* @param {object} beat Beat to get title from
|
||||
* @returns {string} Beat title
|
||||
*/
|
||||
getBeatTitle(beat) {
|
||||
return `${this.$root.datetime(beat.time)}` + ((beat.msg) ? ` - ${beat.msg}` : "");
|
||||
|
@ -308,7 +309,7 @@ export default {
|
|||
}
|
||||
|
||||
.word {
|
||||
color: #aaa;
|
||||
color: $secondary-text;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
|
|
|
@ -74,11 +74,17 @@ export default {
|
|||
|
||||
},
|
||||
methods: {
|
||||
/** Show users input in plain text */
|
||||
/**
|
||||
* Show users input in plain text
|
||||
* @returns {void}
|
||||
*/
|
||||
showInput() {
|
||||
this.visibility = "text";
|
||||
},
|
||||
/** Censor users input */
|
||||
/**
|
||||
* Censor users input
|
||||
* @returns {void}
|
||||
*/
|
||||
hideInput() {
|
||||
this.visibility = "password";
|
||||
},
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
</button>
|
||||
|
||||
<div v-if="res && !res.ok" class="alert alert-danger mt-3" role="alert">
|
||||
{{ res.msg }}
|
||||
{{ $t(res.msg) }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -64,7 +64,10 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
/** Submit the user details and attempt to log in */
|
||||
/**
|
||||
* Submit the user details and attempt to log in
|
||||
* @returns {void}
|
||||
*/
|
||||
submit() {
|
||||
this.processing = true;
|
||||
|
||||
|
|
|
@ -54,11 +54,12 @@
|
|||
v-for="(item, index) in sortedMonitorList"
|
||||
:key="index"
|
||||
:monitor="item"
|
||||
:showPathName="filtersActive"
|
||||
:isSelectMode="selectMode"
|
||||
:isSelected="isSelected"
|
||||
:select="select"
|
||||
:deselect="deselect"
|
||||
:filter-func="filterFunc"
|
||||
:sort-func="sortFunc"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -106,6 +107,7 @@ export default {
|
|||
* Improve the sticky appearance of the list by increasing its
|
||||
* height as user scrolls down.
|
||||
* Not used on mobile.
|
||||
* @returns {object} Style for monitor list
|
||||
*/
|
||||
boxStyle() {
|
||||
if (window.innerWidth > 550) {
|
||||
|
@ -122,82 +124,22 @@ export default {
|
|||
|
||||
/**
|
||||
* Returns a sorted list of monitors based on the applied filters and search text.
|
||||
*
|
||||
* @return {Array} The sorted list of monitors.
|
||||
* @returns {Array} The sorted list of monitors.
|
||||
*/
|
||||
sortedMonitorList() {
|
||||
let result = Object.values(this.$root.monitorList);
|
||||
|
||||
result = result.filter(monitor => {
|
||||
// filter by search text
|
||||
// finds monitor name, tag name or tag value
|
||||
let searchTextMatch = true;
|
||||
if (this.searchText !== "") {
|
||||
const loweredSearchText = this.searchText.toLowerCase();
|
||||
searchTextMatch =
|
||||
monitor.name.toLowerCase().includes(loweredSearchText)
|
||||
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|
||||
|| tag.value?.toLowerCase().includes(loweredSearchText));
|
||||
// The root list does not show children
|
||||
if (monitor.parent !== null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// filter by status
|
||||
let statusMatch = true;
|
||||
if (this.filterState.status != null && this.filterState.status.length > 0) {
|
||||
if (monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[monitor.id]) {
|
||||
monitor.status = this.$root.lastHeartbeatList[monitor.id].status;
|
||||
}
|
||||
statusMatch = this.filterState.status.includes(monitor.status);
|
||||
}
|
||||
|
||||
// filter by active
|
||||
let activeMatch = true;
|
||||
if (this.filterState.active != null && this.filterState.active.length > 0) {
|
||||
activeMatch = this.filterState.active.includes(monitor.active);
|
||||
}
|
||||
|
||||
// filter by tags
|
||||
let tagsMatch = true;
|
||||
if (this.filterState.tags != null && this.filterState.tags.length > 0) {
|
||||
tagsMatch = monitor.tags.map(tag => tag.tag_id) // convert to array of tag IDs
|
||||
.filter(monitorTagId => this.filterState.tags.includes(monitorTagId)) // perform Array Intersaction between filter and monitor's tags
|
||||
.length > 0;
|
||||
}
|
||||
|
||||
// Hide children if not filtering
|
||||
let showChild = true;
|
||||
if (this.filterState.status == null && this.filterState.active == null && this.filterState.tags == null && this.searchText === "") {
|
||||
if (monitor.parent !== null) {
|
||||
showChild = false;
|
||||
}
|
||||
}
|
||||
|
||||
return searchTextMatch && statusMatch && activeMatch && tagsMatch && showChild;
|
||||
return true;
|
||||
});
|
||||
|
||||
// Filter result by active state, weight and alphabetical
|
||||
result.sort((m1, m2) => {
|
||||
if (m1.active !== m2.active) {
|
||||
if (m1.active === false) {
|
||||
return 1;
|
||||
}
|
||||
result = result.filter(this.filterFunc);
|
||||
|
||||
if (m2.active === false) {
|
||||
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);
|
||||
});
|
||||
result.sort(this.sortFunc);
|
||||
|
||||
return result;
|
||||
},
|
||||
|
@ -224,8 +166,7 @@ export default {
|
|||
|
||||
/**
|
||||
* Determines if any filters are active.
|
||||
*
|
||||
* @return {boolean} True if any filter is active, false otherwise.
|
||||
* @returns {boolean} True if any filter is active, false otherwise.
|
||||
*/
|
||||
filtersActive() {
|
||||
return this.filterState.status != null || this.filterState.active != null || this.filterState.tags != null || this.searchText !== "";
|
||||
|
@ -270,7 +211,10 @@ export default {
|
|||
window.removeEventListener("scroll", this.onScroll);
|
||||
},
|
||||
methods: {
|
||||
/** Handle user scroll */
|
||||
/**
|
||||
* Handle user scroll
|
||||
* @returns {void}
|
||||
*/
|
||||
onScroll() {
|
||||
if (window.top.scrollY <= 133) {
|
||||
this.windowTop = window.top.scrollY;
|
||||
|
@ -286,13 +230,17 @@ export default {
|
|||
monitorURL(id) {
|
||||
return getMonitorRelativeURL(id);
|
||||
},
|
||||
/** Clear the search bar */
|
||||
/**
|
||||
* Clear the search bar
|
||||
* @returns {void}
|
||||
*/
|
||||
clearSearchText() {
|
||||
this.searchText = "";
|
||||
},
|
||||
/**
|
||||
* Update the MonitorList Filter
|
||||
* @param {object} newFilter Object with new filter
|
||||
* @returns {void}
|
||||
*/
|
||||
updateFilter(newFilter) {
|
||||
this.filterState = newFilter;
|
||||
|
@ -300,6 +248,7 @@ export default {
|
|||
/**
|
||||
* Deselect a monitor
|
||||
* @param {number} id ID of monitor
|
||||
* @returns {void}
|
||||
*/
|
||||
deselect(id) {
|
||||
delete this.selectedMonitors[id];
|
||||
|
@ -307,6 +256,7 @@ export default {
|
|||
/**
|
||||
* Select a monitor
|
||||
* @param {number} id ID of monitor
|
||||
* @returns {void}
|
||||
*/
|
||||
select(id) {
|
||||
this.selectedMonitors[id] = true;
|
||||
|
@ -314,36 +264,127 @@ export default {
|
|||
/**
|
||||
* Determine if monitor is selected
|
||||
* @param {number} id ID of monitor
|
||||
* @returns {bool}
|
||||
* @returns {bool} Is the monitor selected?
|
||||
*/
|
||||
isSelected(id) {
|
||||
return id in this.selectedMonitors;
|
||||
},
|
||||
/** Disable select mode and reset selection */
|
||||
/**
|
||||
* Disable select mode and reset selection
|
||||
* @returns {void}
|
||||
*/
|
||||
cancelSelectMode() {
|
||||
this.selectMode = false;
|
||||
this.selectedMonitors = {};
|
||||
},
|
||||
/** Show dialog to confirm pause */
|
||||
/**
|
||||
* Show dialog to confirm pause
|
||||
* @returns {void}
|
||||
*/
|
||||
pauseDialog() {
|
||||
this.$refs.confirmPause.show();
|
||||
},
|
||||
/** Pause each selected monitor */
|
||||
/**
|
||||
* Pause each selected monitor
|
||||
* @returns {void}
|
||||
*/
|
||||
pauseSelected() {
|
||||
Object.keys(this.selectedMonitors)
|
||||
.filter(id => this.$root.monitorList[id].active)
|
||||
.forEach(id => this.$root.getSocket().emit("pauseMonitor", id));
|
||||
.forEach(id => this.$root.getSocket().emit("pauseMonitor", id, () => {}));
|
||||
|
||||
this.cancelSelectMode();
|
||||
},
|
||||
/** Resume each selected monitor */
|
||||
/**
|
||||
* Resume each selected monitor
|
||||
* @returns {void}
|
||||
*/
|
||||
resumeSelected() {
|
||||
Object.keys(this.selectedMonitors)
|
||||
.filter(id => !this.$root.monitorList[id].active)
|
||||
.forEach(id => this.$root.getSocket().emit("resumeMonitor", id));
|
||||
.forEach(id => this.$root.getSocket().emit("resumeMonitor", id, () => {}));
|
||||
|
||||
this.cancelSelectMode();
|
||||
},
|
||||
/**
|
||||
* Whether a monitor should be displayed based on the filters
|
||||
* @param {object} monitor Monitor to check
|
||||
* @returns {boolean} Should the monitor be displayed
|
||||
*/
|
||||
filterFunc(monitor) {
|
||||
// Group monitors bypass filter if at least 1 of children matched
|
||||
if (monitor.type === "group") {
|
||||
const children = Object.values(this.$root.monitorList).filter(m => m.parent === monitor.id);
|
||||
if (children.some((child, index, children) => this.filterFunc(child))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// filter by search text
|
||||
// finds monitor name, tag name or tag value
|
||||
let searchTextMatch = true;
|
||||
if (this.searchText !== "") {
|
||||
const loweredSearchText = this.searchText.toLowerCase();
|
||||
searchTextMatch =
|
||||
monitor.name.toLowerCase().includes(loweredSearchText)
|
||||
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|
||||
|| tag.value?.toLowerCase().includes(loweredSearchText));
|
||||
}
|
||||
|
||||
// filter by status
|
||||
let statusMatch = true;
|
||||
if (this.filterState.status != null && this.filterState.status.length > 0) {
|
||||
if (monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[monitor.id]) {
|
||||
monitor.status = this.$root.lastHeartbeatList[monitor.id].status;
|
||||
}
|
||||
statusMatch = this.filterState.status.includes(monitor.status);
|
||||
}
|
||||
|
||||
// filter by active
|
||||
let activeMatch = true;
|
||||
if (this.filterState.active != null && this.filterState.active.length > 0) {
|
||||
activeMatch = this.filterState.active.includes(monitor.active);
|
||||
}
|
||||
|
||||
// filter by tags
|
||||
let tagsMatch = true;
|
||||
if (this.filterState.tags != null && this.filterState.tags.length > 0) {
|
||||
tagsMatch = monitor.tags.map(tag => tag.tag_id) // convert to array of tag IDs
|
||||
.filter(monitorTagId => this.filterState.tags.includes(monitorTagId)) // perform Array Intersaction between filter and monitor's tags
|
||||
.length > 0;
|
||||
}
|
||||
|
||||
return searchTextMatch && statusMatch && activeMatch && tagsMatch;
|
||||
},
|
||||
/**
|
||||
* Function used in Array.sort to order monitors in a list.
|
||||
* @param {*} m1 monitor 1
|
||||
* @param {*} m2 monitor 2
|
||||
* @returns {number} -1, 0 or 1
|
||||
*/
|
||||
sortFunc(m1, m2) {
|
||||
if (m1.active !== m2.active) {
|
||||
if (m1.active === false) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (m2.active === false) {
|
||||
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);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -441,5 +482,4 @@ export default {
|
|||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -141,6 +141,11 @@
|
|||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="tagsList.length === 0">
|
||||
<div class="dropdown-item disabled px-3">
|
||||
{{ $t('No tags found.') }}
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</MonitorListFilterDropdown>
|
||||
</div>
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
<span v-if="hasChildren" class="collapse-padding" @click.prevent="changeCollapsed">
|
||||
<font-awesome-icon icon="chevron-down" class="animated" :class="{ collapsed: isCollapsed}" />
|
||||
</span>
|
||||
{{ monitorName }}
|
||||
{{ monitor.name }}
|
||||
</div>
|
||||
<div v-if="monitor.tags.length > 0" class="tags">
|
||||
<Tag v-for="tag in monitor.tags" :key="tag" :item="tag" :size="'sm'" />
|
||||
|
@ -44,7 +44,6 @@
|
|||
<MonitorListItem
|
||||
v-for="(item, index) in sortedChildMonitorList"
|
||||
:key="index" :monitor="item"
|
||||
:showPathName="showPathName"
|
||||
:isSelectMode="isSelectMode"
|
||||
:isSelected="isSelected"
|
||||
:select="select"
|
||||
|
@ -75,11 +74,6 @@ export default {
|
|||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
/** Should the monitor name show it's parent */
|
||||
showPathName: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** If the user is in select mode */
|
||||
isSelectMode: {
|
||||
type: Boolean,
|
||||
|
@ -105,6 +99,16 @@ export default {
|
|||
type: Function,
|
||||
default: () => {}
|
||||
},
|
||||
/** Function to filter child monitors */
|
||||
filterFunc: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
},
|
||||
/** Function to sort child monitors */
|
||||
sortFunc: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -115,32 +119,13 @@ export default {
|
|||
sortedChildMonitorList() {
|
||||
let result = Object.values(this.$root.monitorList);
|
||||
|
||||
// Get children
|
||||
result = result.filter(childMonitor => childMonitor.parent === this.monitor.id);
|
||||
|
||||
result.sort((m1, m2) => {
|
||||
// Run filter on children
|
||||
result = result.filter(this.filterFunc);
|
||||
|
||||
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);
|
||||
});
|
||||
result.sort(this.sortFunc);
|
||||
|
||||
return result;
|
||||
},
|
||||
|
@ -152,13 +137,6 @@ export default {
|
|||
marginLeft: `${31 * this.depth}px`,
|
||||
};
|
||||
},
|
||||
monitorName() {
|
||||
if (this.showPathName) {
|
||||
return this.monitor.pathName;
|
||||
} else {
|
||||
return this.monitor.name;
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isSelectMode() {
|
||||
|
@ -189,7 +167,9 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
/**
|
||||
* Changes the collapsed value of the current monitor and saves it to local storage
|
||||
* Changes the collapsed value of the current monitor and saves
|
||||
* it to local storage
|
||||
* @returns {void}
|
||||
*/
|
||||
changeCollapsed() {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
|
@ -214,6 +194,7 @@ export default {
|
|||
},
|
||||
/**
|
||||
* Toggle selection of monitor
|
||||
* @returns {void}
|
||||
*/
|
||||
toggleSelection() {
|
||||
if (this.isSelected(this.monitor.id)) {
|
||||
|
|
|
@ -67,8 +67,9 @@ export default {
|
|||
methods: {
|
||||
/**
|
||||
* Setting monitor
|
||||
* @param {Object} group Data of monitor
|
||||
* @param {Object} monitor Data of monitor
|
||||
* @param {object} group Data of monitor
|
||||
* @param {object} monitor Data of monitor
|
||||
* @returns {void}
|
||||
*/
|
||||
show(group, monitor) {
|
||||
this.monitor = {
|
||||
|
@ -86,6 +87,7 @@ export default {
|
|||
* Toggle the value of sendUrl
|
||||
* @param {number} groupIndex Index of group monitor is member of
|
||||
* @param {number} index Index of monitor within group
|
||||
* @returns {void}
|
||||
*/
|
||||
toggleLink(groupIndex, index) {
|
||||
this.$root.publicGroupList[groupIndex].monitorList[index].sendUrl = !this.$root.publicGroupList[groupIndex].monitorList[index].sendUrl;
|
||||
|
@ -95,10 +97,10 @@ export default {
|
|||
* Should a link to the monitor be shown?
|
||||
* Attempts to guess if a link should be shown based upon if
|
||||
* sendUrl is set and if the URL is default or not.
|
||||
* @param {Object} monitor Monitor to check
|
||||
* @param {boolean} [ignoreSendUrl=false] Should the presence of the sendUrl
|
||||
* @param {object} monitor Monitor to check
|
||||
* @param {boolean} ignoreSendUrl Should the presence of the sendUrl
|
||||
* property be ignored. This will only work in edit mode.
|
||||
* @returns {boolean}
|
||||
* @returns {boolean} Should the link be shown?
|
||||
*/
|
||||
showLink(monitor, ignoreSendUrl = false) {
|
||||
// We must check if there are any elements in monitorList to
|
||||
|
|
|
@ -114,12 +114,17 @@ export default {
|
|||
"AlertNow": "AlertNow",
|
||||
"apprise": this.$t("apprise"),
|
||||
"Bark": "Bark",
|
||||
"Bitrix24": "Bitrix24",
|
||||
"clicksendsms": "ClickSend SMS",
|
||||
"CallMeBot": "CallMeBot (WhatsApp, Telegram Call, Facebook Messanger)",
|
||||
"discord": "Discord",
|
||||
"GoogleChat": "Google Chat (Google Workspace)",
|
||||
"gorush": "Gorush",
|
||||
"gotify": "Gotify",
|
||||
"GrafanaOncall": "Grafana Oncall",
|
||||
"HeiiOnCall": "Heii On-Call",
|
||||
"HomeAssistant": "Home Assistant",
|
||||
"Keep": "Keep",
|
||||
"Kook": "Kook",
|
||||
"line": "LINE Messenger",
|
||||
"LineNotify": "LINE Notify",
|
||||
|
@ -142,15 +147,21 @@ export default {
|
|||
"slack": "Slack",
|
||||
"squadcast": "SquadCast",
|
||||
"SMSEagle": "SMSEagle",
|
||||
"SMSPartner": "SMS Partner",
|
||||
"smtp": this.$t("smtp"),
|
||||
"stackfield": "Stackfield",
|
||||
"teams": "Microsoft Teams",
|
||||
"telegram": "Telegram",
|
||||
"threema": "Threema",
|
||||
"twilio": "Twilio",
|
||||
"Splunk": "Splunk",
|
||||
"webhook": "Webhook",
|
||||
"GoAlert": "GoAlert",
|
||||
"ZohoCliq": "ZohoCliq"
|
||||
"ZohoCliq": "ZohoCliq",
|
||||
"SevenIO": "SevenIO",
|
||||
"whapi": "WhatsApp (Whapi)",
|
||||
"gtxmessaging": "GtxMessaging",
|
||||
"Cellsynt": "Cellsynt",
|
||||
};
|
||||
|
||||
// Put notifications here if it's not supported in most regions or its documentation is not in English
|
||||
|
@ -218,7 +229,10 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
|
||||
/** Show dialog to confirm deletion */
|
||||
/**
|
||||
* Show dialog to confirm deletion
|
||||
* @returns {void}
|
||||
*/
|
||||
deleteConfirm() {
|
||||
this.modal.hide();
|
||||
this.$refs.confirmDelete.show();
|
||||
|
@ -227,6 +241,7 @@ export default {
|
|||
/**
|
||||
* Show settings for specified notification
|
||||
* @param {number} notificationID ID of notification to show
|
||||
* @returns {void}
|
||||
*/
|
||||
show(notificationID) {
|
||||
if (notificationID) {
|
||||
|
@ -250,7 +265,10 @@ export default {
|
|||
this.modal.show();
|
||||
},
|
||||
|
||||
/** Submit the form to the server */
|
||||
/**
|
||||
* Submit the form to the server
|
||||
* @returns {void}
|
||||
*/
|
||||
submit() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("addNotification", this.notification, this.id, (res) => {
|
||||
|
@ -269,7 +287,10 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
/** Test the notification endpoint */
|
||||
/**
|
||||
* Test the notification endpoint
|
||||
* @returns {void}
|
||||
*/
|
||||
test() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("testNotification", this.notification, (res) => {
|
||||
|
@ -278,7 +299,10 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
/** Delete the notification endpoint */
|
||||
/**
|
||||
* Delete the notification endpoint
|
||||
* @returns {void}
|
||||
*/
|
||||
deleteNotification() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("deleteNotification", this.id, (res) => {
|
||||
|
@ -293,7 +317,8 @@ export default {
|
|||
/**
|
||||
* Get a unique default name for the notification
|
||||
* @param {keyof NotificationFormList} notificationKey
|
||||
* @return {string}
|
||||
* Notification to retrieve
|
||||
* @returns {string} Default name
|
||||
*/
|
||||
getUniqueDefaultName(notificationKey) {
|
||||
|
||||
|
|
|
@ -1,16 +1,24 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="period-options">
|
||||
<button type="button" class="btn btn-light dropdown-toggle btn-period-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<button
|
||||
type="button" class="btn btn-light dropdown-toggle btn-period-toggle" data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
{{ chartPeriodOptions[chartPeriodHrs] }}
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li v-for="(item, key) in chartPeriodOptions" :key="key">
|
||||
<a class="dropdown-item" :class="{ active: chartPeriodHrs == key }" href="#" @click="chartPeriodHrs = key">{{ item }}</a>
|
||||
<button
|
||||
type="button" class="dropdown-item" :class="{ active: chartPeriodHrs == key }"
|
||||
@click="chartPeriodHrs = key"
|
||||
>
|
||||
{{ item }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="chart-wrapper" :class="{ loading : loading}">
|
||||
<div class="chart-wrapper" :class="{ loading: loading }">
|
||||
<Line :data="chartData" :options="chartOptions" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -19,12 +27,8 @@
|
|||
<script lang="js">
|
||||
import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js";
|
||||
import "chartjs-adapter-dayjs-4";
|
||||
import dayjs from "dayjs";
|
||||
import { Line } from "vue-chartjs";
|
||||
import { useToast } from "vue-toastification";
|
||||
import { DOWN, PENDING, MAINTENANCE, log } from "../util.ts";
|
||||
|
||||
const toast = useToast();
|
||||
import { UP, DOWN, PENDING, MAINTENANCE } from "../util.ts";
|
||||
|
||||
Chart.register(LineController, BarController, LineElement, PointElement, TimeScale, BarElement, LinearScale, Tooltip, Filler);
|
||||
|
||||
|
@ -42,8 +46,9 @@ export default {
|
|||
|
||||
loading: false,
|
||||
|
||||
// Configurable filtering on top of the returned data
|
||||
chartPeriodHrs: 0,
|
||||
// Time period for the chart to display, in hours
|
||||
// Initial value is 0 as a workaround for triggering a data fetch on created()
|
||||
chartPeriodHrs: "0",
|
||||
|
||||
chartPeriodOptions: {
|
||||
0: this.$t("recent"),
|
||||
|
@ -53,9 +58,8 @@ export default {
|
|||
168: "1w",
|
||||
},
|
||||
|
||||
// A heartbeatList for 3h, 6h, 24h, 1w
|
||||
// Uses the $root.heartbeatList when value is null
|
||||
heartbeatList: null
|
||||
chartRawData: null,
|
||||
chartDataFetchInterval: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -160,34 +164,197 @@ export default {
|
|||
};
|
||||
},
|
||||
chartData() {
|
||||
if (this.chartPeriodHrs === "0") {
|
||||
return this.getChartDatapointsFromHeartbeatList();
|
||||
} else {
|
||||
return this.getChartDatapointsFromStats();
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
// Update chart data when the selected chart period changes
|
||||
chartPeriodHrs: function (newPeriod) {
|
||||
if (this.chartDataFetchInterval) {
|
||||
clearInterval(this.chartDataFetchInterval);
|
||||
this.chartDataFetchInterval = null;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (newPeriod == "0") {
|
||||
this.heartbeatList = null;
|
||||
this.$root.storage().removeItem(`chart-period-${this.monitorId}`);
|
||||
} else {
|
||||
this.loading = true;
|
||||
|
||||
let period;
|
||||
try {
|
||||
period = parseInt(newPeriod);
|
||||
} catch (e) {
|
||||
// Invalid period
|
||||
period = 24;
|
||||
}
|
||||
|
||||
this.$root.getMonitorChartData(this.monitorId, period, (res) => {
|
||||
if (!res.ok) {
|
||||
this.$root.toastError(res.msg);
|
||||
} else {
|
||||
this.chartRawData = res.data;
|
||||
this.$root.storage()[`chart-period-${this.monitorId}`] = newPeriod;
|
||||
}
|
||||
this.loading = false;
|
||||
});
|
||||
|
||||
this.chartDataFetchInterval = setInterval(() => {
|
||||
this.$root.getMonitorChartData(this.monitorId, period, (res) => {
|
||||
if (res.ok) {
|
||||
this.chartRawData = res.data;
|
||||
}
|
||||
});
|
||||
}, 5 * 60 * 1000);
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// Load chart period from storage if saved
|
||||
let period = this.$root.storage()[`chart-period-${this.monitorId}`];
|
||||
if (period != null) {
|
||||
// Has this ever been not a string?
|
||||
if (typeof period !== "string") {
|
||||
period = period.toString();
|
||||
}
|
||||
this.chartPeriodHrs = period;
|
||||
} else {
|
||||
this.chartPeriodHrs = "24";
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.chartDataFetchInterval) {
|
||||
clearInterval(this.chartDataFetchInterval);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// Get color of bar chart for this datapoint
|
||||
getBarColorForDatapoint(datapoint) {
|
||||
if (datapoint.maintenance != null) {
|
||||
// Target is in maintenance
|
||||
return "rgba(23,71,245,0.41)";
|
||||
} else if (datapoint.down === 0) {
|
||||
// Target is up, no need to display a bar
|
||||
return "#000";
|
||||
} else if (datapoint.up === 0) {
|
||||
// Target is down
|
||||
return "rgba(220, 53, 69, 0.41)";
|
||||
} else {
|
||||
// Show yellow for mixed status
|
||||
return "rgba(245, 182, 23, 0.41)";
|
||||
}
|
||||
},
|
||||
// push datapoint to chartData
|
||||
pushDatapoint(datapoint, avgPingData, minPingData, maxPingData, downData, colorData) {
|
||||
const x = this.$root.unixToDateTime(datapoint.timestamp);
|
||||
|
||||
// Show ping values if it was up in this period
|
||||
avgPingData.push({
|
||||
x,
|
||||
y: datapoint.up > 0 && datapoint.avgPing > 0 ? datapoint.avgPing : null,
|
||||
});
|
||||
minPingData.push({
|
||||
x,
|
||||
y: datapoint.up > 0 && datapoint.avgPing > 0 ? datapoint.minPing : null,
|
||||
});
|
||||
maxPingData.push({
|
||||
x,
|
||||
y: datapoint.up > 0 && datapoint.avgPing > 0 ? datapoint.maxPing : null,
|
||||
});
|
||||
downData.push({
|
||||
x,
|
||||
y: datapoint.down + (datapoint.maintenance || 0),
|
||||
});
|
||||
|
||||
colorData.push(this.getBarColorForDatapoint(datapoint));
|
||||
},
|
||||
// get the average of a set of datapoints
|
||||
getAverage(datapoints) {
|
||||
const totalUp = datapoints.reduce((total, current) => total + current.up, 0);
|
||||
const totalDown = datapoints.reduce((total, current) => total + current.down, 0);
|
||||
const totalMaintenance = datapoints.reduce((total, current) => total + (current.maintenance || 0), 0);
|
||||
const totalPing = datapoints.reduce((total, current) => total + current.avgPing * current.up, 0);
|
||||
const minPing = datapoints.reduce((min, current) => Math.min(min, current.minPing), Infinity);
|
||||
const maxPing = datapoints.reduce((max, current) => Math.max(max, current.maxPing), 0);
|
||||
|
||||
// Find the middle timestamp to use
|
||||
let midpoint = Math.floor(datapoints.length / 2);
|
||||
|
||||
return {
|
||||
timestamp: datapoints[midpoint].timestamp,
|
||||
up: totalUp,
|
||||
down: totalDown,
|
||||
maintenance: totalMaintenance > 0 ? totalMaintenance : undefined,
|
||||
avgPing: totalUp > 0 ? totalPing / totalUp : 0,
|
||||
minPing,
|
||||
maxPing,
|
||||
};
|
||||
},
|
||||
getChartDatapointsFromHeartbeatList() {
|
||||
// Render chart using heartbeatList
|
||||
let lastHeartbeatTime;
|
||||
const monitorInterval = this.$root.monitorList[this.monitorId]?.interval;
|
||||
let pingData = []; // Ping Data for Line Chart, y-axis contains ping time
|
||||
let downData = []; // Down Data for Bar Chart, y-axis is 1 if target is down (red color), under maintenance (blue color) or pending (orange color), 0 if target is up
|
||||
let colorData = []; // Color Data for Bar Chart
|
||||
|
||||
let heartbeatList = this.heartbeatList ||
|
||||
(this.monitorId in this.$root.heartbeatList && this.$root.heartbeatList[this.monitorId]) ||
|
||||
[];
|
||||
let heartbeatList = (this.monitorId in this.$root.heartbeatList && this.$root.heartbeatList[this.monitorId]) || [];
|
||||
|
||||
heartbeatList
|
||||
.filter(
|
||||
// Filtering as data gets appended
|
||||
// not the most efficient, but works for now
|
||||
(beat) => dayjs.utc(beat.time).tz(this.$root.timezone).isAfter(
|
||||
dayjs().subtract(Math.max(this.chartPeriodHrs, 6), "hours")
|
||||
)
|
||||
)
|
||||
.map((beat) => {
|
||||
const x = this.$root.datetime(beat.time);
|
||||
pingData.push({
|
||||
x,
|
||||
y: beat.ping,
|
||||
});
|
||||
downData.push({
|
||||
x,
|
||||
y: (beat.status === DOWN || beat.status === MAINTENANCE || beat.status === PENDING) ? 1 : 0,
|
||||
});
|
||||
colorData.push((beat.status === MAINTENANCE) ? "rgba(23,71,245,0.41)" : ((beat.status === PENDING) ? "rgba(245,182,23,0.41)" : "#DC354568"));
|
||||
for (const beat of heartbeatList) {
|
||||
const beatTime = this.$root.toDayjs(beat.time);
|
||||
const x = beatTime.format("YYYY-MM-DD HH:mm:ss");
|
||||
|
||||
// Insert empty datapoint to separate big gaps
|
||||
if (lastHeartbeatTime && monitorInterval) {
|
||||
const diff = Math.abs(beatTime.diff(lastHeartbeatTime));
|
||||
if (diff > monitorInterval * 1000 * 10) {
|
||||
// Big gap detected
|
||||
const gapX = [
|
||||
lastHeartbeatTime.add(monitorInterval, "second").format("YYYY-MM-DD HH:mm:ss"),
|
||||
beatTime.subtract(monitorInterval, "second").format("YYYY-MM-DD HH:mm:ss")
|
||||
];
|
||||
|
||||
for (const x of gapX) {
|
||||
pingData.push({
|
||||
x,
|
||||
y: null,
|
||||
});
|
||||
downData.push({
|
||||
x,
|
||||
y: null,
|
||||
});
|
||||
colorData.push("#000");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
pingData.push({
|
||||
x,
|
||||
y: beat.status === UP ? beat.ping : null,
|
||||
});
|
||||
downData.push({
|
||||
x,
|
||||
y: (beat.status === DOWN || beat.status === MAINTENANCE || beat.status === PENDING) ? 1 : 0,
|
||||
});
|
||||
switch (beat.status) {
|
||||
case MAINTENANCE:
|
||||
colorData.push("rgba(23 ,71, 245, 0.41)");
|
||||
break;
|
||||
case PENDING:
|
||||
colorData.push("rgba(245, 182, 23, 0.41)");
|
||||
break;
|
||||
default:
|
||||
colorData.push("rgba(220, 53, 69, 0.41)");
|
||||
}
|
||||
|
||||
lastHeartbeatTime = beatTime;
|
||||
}
|
||||
|
||||
return {
|
||||
datasets: [
|
||||
|
@ -217,54 +384,155 @@ export default {
|
|||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
// Update chart data when the selected chart period changes
|
||||
chartPeriodHrs: function (newPeriod) {
|
||||
getChartDatapointsFromStats() {
|
||||
// Render chart using UptimeCalculator data
|
||||
let lastHeartbeatTime;
|
||||
const monitorInterval = this.$root.monitorList[this.monitorId]?.interval;
|
||||
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (newPeriod == "0") {
|
||||
this.heartbeatList = null;
|
||||
this.$root.storage().removeItem(`chart-period-${this.monitorId}`);
|
||||
} else {
|
||||
this.loading = true;
|
||||
let avgPingData = []; // Ping Data for Line Chart, y-axis contains ping time
|
||||
let minPingData = []; // Ping Data for Line Chart, y-axis contains ping time
|
||||
let maxPingData = []; // Ping Data for Line Chart, y-axis contains ping time
|
||||
let downData = []; // Down Data for Bar Chart, y-axis is number of down datapoints in this period
|
||||
let colorData = []; // Color Data for Bar Chart
|
||||
|
||||
this.$root.getMonitorBeats(this.monitorId, newPeriod, (res) => {
|
||||
if (!res.ok) {
|
||||
toast.error(res.msg);
|
||||
const period = parseInt(this.chartPeriodHrs);
|
||||
let aggregatePoints = period > 6 ? 12 : 4;
|
||||
|
||||
let aggregateBuffer = [];
|
||||
|
||||
if (this.chartRawData) {
|
||||
for (const datapoint of this.chartRawData) {
|
||||
// Empty datapoints are ignored
|
||||
if (datapoint.up === 0 && datapoint.down === 0 && datapoint.maintenance === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const beatTime = this.$root.unixToDayjs(datapoint.timestamp);
|
||||
|
||||
// Insert empty datapoint to separate big gaps
|
||||
if (lastHeartbeatTime && monitorInterval) {
|
||||
const diff = Math.abs(beatTime.diff(lastHeartbeatTime));
|
||||
const oneSecond = 1000;
|
||||
const oneMinute = oneSecond * 60;
|
||||
const oneHour = oneMinute * 60;
|
||||
if ((period <= 24 && diff > Math.max(oneMinute * 10, monitorInterval * oneSecond * 10)) ||
|
||||
(period > 24 && diff > Math.max(oneHour * 10, monitorInterval * oneSecond * 10))) {
|
||||
// Big gap detected
|
||||
// Clear the aggregate buffer
|
||||
if (aggregateBuffer.length > 0) {
|
||||
const average = this.getAverage(aggregateBuffer);
|
||||
this.pushDatapoint(average, avgPingData, minPingData, maxPingData, downData, colorData);
|
||||
aggregateBuffer = [];
|
||||
}
|
||||
|
||||
const gapX = [
|
||||
lastHeartbeatTime.subtract(monitorInterval, "second").format("YYYY-MM-DD HH:mm:ss"),
|
||||
this.$root.unixToDateTime(datapoint.timestamp + 60),
|
||||
];
|
||||
|
||||
for (const x of gapX) {
|
||||
avgPingData.push({
|
||||
x,
|
||||
y: null,
|
||||
});
|
||||
minPingData.push({
|
||||
x,
|
||||
y: null,
|
||||
});
|
||||
maxPingData.push({
|
||||
x,
|
||||
y: null,
|
||||
});
|
||||
downData.push({
|
||||
x,
|
||||
y: null,
|
||||
});
|
||||
colorData.push("#000");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (datapoint.up > 0 && this.chartRawData.length > aggregatePoints * 2) {
|
||||
// Aggregate Up data using a sliding window
|
||||
aggregateBuffer.push(datapoint);
|
||||
|
||||
if (aggregateBuffer.length === aggregatePoints) {
|
||||
const average = this.getAverage(aggregateBuffer);
|
||||
this.pushDatapoint(average, avgPingData, minPingData, maxPingData, downData, colorData);
|
||||
// Remove the first half of the buffer
|
||||
aggregateBuffer = aggregateBuffer.slice(Math.floor(aggregatePoints / 2));
|
||||
}
|
||||
} else {
|
||||
this.heartbeatList = res.data;
|
||||
this.$root.storage()[`chart-period-${this.monitorId}`] = newPeriod;
|
||||
}
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// Setup Watcher on the root heartbeatList,
|
||||
// And mirror latest change to this.heartbeatList
|
||||
this.$watch(() => this.$root.heartbeatList[this.monitorId],
|
||||
(heartbeatList) => {
|
||||
// datapoint is fully down or too few datapoints, no need to aggregate
|
||||
// Clear the aggregate buffer
|
||||
if (aggregateBuffer.length > 0) {
|
||||
const average = this.getAverage(aggregateBuffer);
|
||||
this.pushDatapoint(average, avgPingData, minPingData, maxPingData, downData, colorData);
|
||||
aggregateBuffer = [];
|
||||
}
|
||||
|
||||
log.debug("ping_chart", `this.chartPeriodHrs type ${typeof this.chartPeriodHrs}, value: ${this.chartPeriodHrs}`);
|
||||
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (this.chartPeriodHrs != "0") {
|
||||
const newBeat = heartbeatList.at(-1);
|
||||
if (newBeat && dayjs.utc(newBeat.time) > dayjs.utc(this.heartbeatList.at(-1)?.time)) {
|
||||
this.heartbeatList.push(heartbeatList.at(-1));
|
||||
this.pushDatapoint(datapoint, avgPingData, minPingData, maxPingData, downData, colorData);
|
||||
}
|
||||
|
||||
lastHeartbeatTime = beatTime;
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
// Clear the aggregate buffer if there are still datapoints
|
||||
if (aggregateBuffer.length > 0) {
|
||||
const average = this.getAverage(aggregateBuffer);
|
||||
this.pushDatapoint(average, avgPingData, minPingData, maxPingData, downData, colorData);
|
||||
aggregateBuffer = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Load chart period from storage if saved
|
||||
let period = this.$root.storage()[`chart-period-${this.monitorId}`];
|
||||
if (period != null) {
|
||||
this.chartPeriodHrs = Math.min(period, 6);
|
||||
}
|
||||
return {
|
||||
datasets: [
|
||||
{
|
||||
// average ping chart
|
||||
data: avgPingData,
|
||||
fill: "origin",
|
||||
tension: 0.2,
|
||||
borderColor: "#5CDD8B",
|
||||
backgroundColor: "#5CDD8B06",
|
||||
yAxisID: "y",
|
||||
label: "avg-ping",
|
||||
},
|
||||
{
|
||||
// minimum ping chart
|
||||
data: minPingData,
|
||||
fill: "origin",
|
||||
tension: 0.2,
|
||||
borderColor: "#3CBD6B38",
|
||||
backgroundColor: "#5CDD8B06",
|
||||
yAxisID: "y",
|
||||
label: "min-ping",
|
||||
},
|
||||
{
|
||||
// maximum ping chart
|
||||
data: maxPingData,
|
||||
fill: "origin",
|
||||
tension: 0.2,
|
||||
borderColor: "#7CBD6B38",
|
||||
backgroundColor: "#5CDD8B06",
|
||||
yAxisID: "y",
|
||||
label: "max-ping",
|
||||
},
|
||||
{
|
||||
// Bar Chart
|
||||
type: "bar",
|
||||
data: downData,
|
||||
borderColor: "#00000000",
|
||||
backgroundColor: colorData,
|
||||
yAxisID: "y1",
|
||||
barThickness: "flex",
|
||||
barPercentage: 1,
|
||||
categoryPercentage: 1,
|
||||
inflateAmount: 0.05,
|
||||
label: "status",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -299,6 +567,7 @@ export default {
|
|||
|
||||
.dark & {
|
||||
background: $dark-bg;
|
||||
color: $dark-font-color;
|
||||
}
|
||||
|
||||
.dark &:hover {
|
||||
|
|
|
@ -131,7 +131,10 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
/** Show dialog to confirm deletion */
|
||||
/**
|
||||
* Show dialog to confirm deletion
|
||||
* @returns {void}
|
||||
*/
|
||||
deleteConfirm() {
|
||||
this.modal.hide();
|
||||
this.$refs.confirmDelete.show();
|
||||
|
@ -140,6 +143,7 @@ export default {
|
|||
/**
|
||||
* Show settings for specified proxy
|
||||
* @param {number} proxyID ID of proxy to show
|
||||
* @returns {void}
|
||||
*/
|
||||
show(proxyID) {
|
||||
if (proxyID) {
|
||||
|
@ -169,7 +173,10 @@ export default {
|
|||
this.modal.show();
|
||||
},
|
||||
|
||||
/** Submit form data for saving */
|
||||
/**
|
||||
* Submit form data for saving
|
||||
* @returns {void}
|
||||
*/
|
||||
submit() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("addProxy", this.proxy, this.id, (res) => {
|
||||
|
@ -187,7 +194,10 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
/** Delete this proxy */
|
||||
/**
|
||||
* Delete this proxy
|
||||
* @returns {void}
|
||||
*/
|
||||
deleteProxy() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("deleteProxy", this.id, (res) => {
|
||||
|
|
|
@ -131,6 +131,7 @@ export default {
|
|||
/**
|
||||
* Remove the specified group
|
||||
* @param {number} index Index of group to remove
|
||||
* @returns {void}
|
||||
*/
|
||||
removeGroup(index) {
|
||||
this.$root.publicGroupList.splice(index, 1);
|
||||
|
@ -141,6 +142,7 @@ export default {
|
|||
* @param {number} groupIndex Index of group to remove monitor
|
||||
* from
|
||||
* @param {number} index Index of monitor to remove
|
||||
* @returns {void}
|
||||
*/
|
||||
removeMonitor(groupIndex, index) {
|
||||
this.$root.publicGroupList[groupIndex].monitorList.splice(index, 1);
|
||||
|
@ -150,10 +152,10 @@ export default {
|
|||
* Should a link to the monitor be shown?
|
||||
* Attempts to guess if a link should be shown based upon if
|
||||
* sendUrl is set and if the URL is default or not.
|
||||
* @param {Object} monitor Monitor to check
|
||||
* @param {boolean} [ignoreSendUrl=false] Should the presence of the sendUrl
|
||||
* @param {object} monitor Monitor to check
|
||||
* @param {boolean} ignoreSendUrl Should the presence of the sendUrl
|
||||
* property be ignored. This will only work in edit mode.
|
||||
* @returns {boolean}
|
||||
* @returns {boolean} Should the link be shown
|
||||
*/
|
||||
showLink(monitor, ignoreSendUrl = false) {
|
||||
// We must check if there are any elements in monitorList to
|
||||
|
@ -161,13 +163,13 @@ export default {
|
|||
if (this.$parent.editMode && ignoreSendUrl && Object.keys(this.$root.monitorList).length) {
|
||||
return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword" || this.$root.monitorList[monitor.element.id].type === "json-query";
|
||||
}
|
||||
return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://" && !this.editMode;
|
||||
return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://";
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns formatted certificate expiry or Bad cert message
|
||||
* @param {Object} monitor Monitor to show expiry for
|
||||
* @returns {string}
|
||||
* @param {object} monitor Monitor to show expiry for
|
||||
* @returns {string} Certificate expiry message
|
||||
*/
|
||||
formattedCertExpiryMessage(monitor) {
|
||||
if (monitor?.element?.validCert && monitor?.element?.certExpiryDaysRemaining) {
|
||||
|
@ -180,9 +182,9 @@ export default {
|
|||
},
|
||||
|
||||
/**
|
||||
* Returns certificate expiry based on days remaining
|
||||
* @param {Object} monitor Monitor to show expiry for
|
||||
* @returns {string}
|
||||
* Returns certificate expiry color based on days remaining
|
||||
* @param {object} monitor Monitor to show expiry for
|
||||
* @returns {string} Color for certificate expiry
|
||||
*/
|
||||
certExpiryColor(monitor) {
|
||||
if (monitor?.element?.validCert && monitor.element.certExpiryDaysRemaining > 7) {
|
||||
|
|
185
src/components/RemoteBrowserDialog.vue
Normal file
185
src/components/RemoteBrowserDialog.vue
Normal file
|
@ -0,0 +1,185 @@
|
|||
<template>
|
||||
<form @submit.prevent="submit">
|
||||
<div ref="modal" class="modal fade" tabindex="-1" data-bs-backdrop="static">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 id="exampleModalLabel" class="modal-title">
|
||||
{{ $t("Add a Remote Browser") }}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="remote-browser-name" class="form-label">{{ $t("Friendly Name") }}</label>
|
||||
<input id="remote-browser-name" v-model="remoteBrowser.name" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="remote-browser-url" class="form-label">{{ $t("URL") }}</label>
|
||||
<input id="remote-browser-url" v-model="remoteBrowser.url" type="text" class="form-control" required>
|
||||
|
||||
<div class="form-text mt-3">
|
||||
{{ $t("Examples") }}:
|
||||
<ul>
|
||||
<li>ws://chrome.browserless.io/playwright?token=YOUR-API-TOKEN</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button v-if="id" type="button" class="btn btn-danger" :disabled="processing" @click="deleteConfirm">
|
||||
{{ $t("Delete") }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning" :disabled="processing" @click="test">
|
||||
{{ $t("Test") }}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" :disabled="processing">
|
||||
<div v-if="processing" class="spinner-border spinner-border-sm me-1"></div>
|
||||
{{ $t("Save") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteDockerHost">
|
||||
{{ $t("deleteRemoteBrowserMessage") }}
|
||||
</Confirm>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Modal } from "bootstrap";
|
||||
import Confirm from "./Confirm.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Confirm,
|
||||
},
|
||||
props: {},
|
||||
emits: [ "added" ],
|
||||
data() {
|
||||
return {
|
||||
modal: null,
|
||||
processing: false,
|
||||
id: null,
|
||||
remoteBrowser: {
|
||||
name: "",
|
||||
url: "",
|
||||
// Do not set default value here, please scroll to show()
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.modal = new Modal(this.$refs.modal);
|
||||
},
|
||||
methods: {
|
||||
|
||||
/**
|
||||
* Confirm deletion of docker host
|
||||
* @returns {void}
|
||||
*/
|
||||
deleteConfirm() {
|
||||
this.modal.hide();
|
||||
this.$refs.confirmDelete.show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Show specified docker host
|
||||
* @param {number} remoteBrowserID ID of host to show
|
||||
* @returns {void}
|
||||
*/
|
||||
show(remoteBrowserID) {
|
||||
if (remoteBrowserID) {
|
||||
let found = false;
|
||||
|
||||
this.id = remoteBrowserID;
|
||||
|
||||
for (let n of this.$root.remoteBrowserList) {
|
||||
if (n.id === remoteBrowserID) {
|
||||
this.remoteBrowser = n;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
this.$root.toastError(this.$t("Remote Browser not found!"));
|
||||
}
|
||||
|
||||
} else {
|
||||
this.id = null;
|
||||
this.remoteBrowser = {
|
||||
name: "",
|
||||
url: "",
|
||||
};
|
||||
}
|
||||
|
||||
this.modal.show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Add docker host
|
||||
* @returns {void}
|
||||
*/
|
||||
submit() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("addRemoteBrowser", this.remoteBrowser, this.id, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok) {
|
||||
this.modal.hide();
|
||||
|
||||
// Emit added event, doesn't emit edit.
|
||||
if (! this.id) {
|
||||
this.$emit("added", res.id);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Test the docker host
|
||||
* @returns {void}
|
||||
*/
|
||||
test() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("testRemoteBrowser", this.remoteBrowser, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
this.processing = false;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete this docker host
|
||||
* @returns {void}
|
||||
*/
|
||||
deleteDockerHost() {
|
||||
this.processing = true;
|
||||
this.$root.getSocket().emit("deleteRemoteBrowser", this.id, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok) {
|
||||
this.modal.hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.dark {
|
||||
.modal-dialog .form-text, .modal-dialog p {
|
||||
color: $dark-font-color;
|
||||
}
|
||||
}
|
||||
</style>
|
52
src/components/ScreenshotDialog.vue
Normal file
52
src/components/ScreenshotDialog.vue
Normal file
|
@ -0,0 +1,52 @@
|
|||
<template>
|
||||
<div ref="modal" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
{{ $t("Browser Screenshot") }}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||
</div>
|
||||
<div class="modal-body"></div>
|
||||
<img :src="imageURL" alt="screenshot of the website">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Modal } from "bootstrap";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
imageURL: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modal: null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.modal = new Modal(this.$refs.modal);
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.modal.show();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.dark {
|
||||
.modal-dialog .form-text, .modal-dialog p {
|
||||
color: $dark-font-color;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -19,12 +19,13 @@
|
|||
|
||||
<script>
|
||||
/**
|
||||
* @typedef {import('./TagsManager.vue').Tag} Tag
|
||||
*/
|
||||
* @typedef {import('./TagsManager.vue').Tag} Tag
|
||||
*/
|
||||
|
||||
export default {
|
||||
props: {
|
||||
/** Object representing tag
|
||||
/**
|
||||
* Object representing tag
|
||||
* @type {Tag}
|
||||
*/
|
||||
item: {
|
||||
|
|
|
@ -123,9 +123,7 @@ import Confirm from "./Confirm.vue";
|
|||
import Tag from "./Tag.vue";
|
||||
import VueMultiselect from "vue-multiselect";
|
||||
import { colorOptions } from "../util-frontend";
|
||||
import { useToast } from "vue-toastification";
|
||||
import { getMonitorRelativeURL } from "../util.ts";
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -207,6 +205,8 @@ export default {
|
|||
},
|
||||
/**
|
||||
* Selected a monitor and add to the list.
|
||||
* @param {object} monitor Monitor to add
|
||||
* @returns {void}
|
||||
*/
|
||||
selectedAddMonitor(monitor) {
|
||||
if (monitor) {
|
||||
|
@ -227,6 +227,7 @@ export default {
|
|||
methods: {
|
||||
/**
|
||||
* Show confirmation for deleting a tag
|
||||
* @returns {void}
|
||||
*/
|
||||
deleteConfirm() {
|
||||
this.$refs.confirmDelete.show();
|
||||
|
@ -234,6 +235,7 @@ export default {
|
|||
|
||||
/**
|
||||
* Reset the editTag form
|
||||
* @returns {void}
|
||||
*/
|
||||
reset() {
|
||||
this.selectedColor = null;
|
||||
|
@ -263,7 +265,7 @@ export default {
|
|||
|
||||
/**
|
||||
* Load tag information for display in the edit dialog
|
||||
* @param {Object} tag tag object to edit
|
||||
* @param {object} tag tag object to edit
|
||||
* @returns {void}
|
||||
*/
|
||||
show(tag) {
|
||||
|
@ -286,7 +288,7 @@ export default {
|
|||
|
||||
/**
|
||||
* Submit tag and monitorTag changes to server
|
||||
* @returns {void}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async submit() {
|
||||
this.processing = true;
|
||||
|
@ -316,7 +318,7 @@ export default {
|
|||
for (let addId of this.addingMonitor) {
|
||||
await this.addMonitorTagAsync(this.tag.id, addId, "").then((res) => {
|
||||
if (!res.ok) {
|
||||
toast.error(res.msg);
|
||||
this.$root.toastError(res.msg);
|
||||
editResult = false;
|
||||
}
|
||||
});
|
||||
|
@ -326,7 +328,7 @@ export default {
|
|||
this.monitors.find(monitor => monitor.id === removeId)?.tags.forEach(async (monitorTag) => {
|
||||
await this.deleteMonitorTagAsync(this.tag.id, removeId, monitorTag.value).then((res) => {
|
||||
if (!res.ok) {
|
||||
toast.error(res.msg);
|
||||
this.$root.toastError(res.msg);
|
||||
editResult = false;
|
||||
}
|
||||
});
|
||||
|
@ -346,7 +348,7 @@ export default {
|
|||
|
||||
/**
|
||||
* Delete the editing tag from server
|
||||
* @returns {void}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async deleteTag() {
|
||||
this.processing = true;
|
||||
|
@ -377,7 +379,7 @@ export default {
|
|||
/**
|
||||
* Get monitors which has a specific tag locally
|
||||
* @param {number} tagId id of the tag to filter
|
||||
* @returns {Object[]} list of monitors which has a specific tag
|
||||
* @returns {object[]} list of monitors which has a specific tag
|
||||
*/
|
||||
monitorsByTag(tagId) {
|
||||
return Object.values(this.$root.monitorList).filter((monitor) => {
|
||||
|
@ -396,7 +398,7 @@ export default {
|
|||
|
||||
/**
|
||||
* Add a tag asynchronously
|
||||
* @param {Object} newTag Object representing new tag to add
|
||||
* @param {object} newTag Object representing new tag to add
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
addTagAsync(newTag) {
|
||||
|
|
|
@ -129,21 +129,20 @@
|
|||
<script>
|
||||
import { Modal } from "bootstrap";
|
||||
import VueMultiselect from "vue-multiselect";
|
||||
import { useToast } from "vue-toastification";
|
||||
import { colorOptions } from "../util-frontend";
|
||||
import Tag from "../components/Tag.vue";
|
||||
const toast = useToast();
|
||||
|
||||
/**
|
||||
* @typedef Tag
|
||||
* @type {object}
|
||||
* @property {number | undefined} id
|
||||
* @property {number | undefined} monitor_id
|
||||
* @property {number | undefined} tag_id
|
||||
* @property {string} value
|
||||
* @property {string} name
|
||||
* @property {string} color
|
||||
* @property {boolean | undefined} new
|
||||
* @property {number | undefined} id ID of tag assignment
|
||||
* @property {number | undefined} monitor_id ID of monitor tag is
|
||||
* assigned to
|
||||
* @property {number | undefined} tag_id ID of tag
|
||||
* @property {string} value Value given to tag
|
||||
* @property {string} name Name of tag
|
||||
* @property {string} color Colour of tag
|
||||
* @property {boolean | undefined} new Should a new tag be created?
|
||||
*/
|
||||
|
||||
export default {
|
||||
|
@ -152,7 +151,8 @@ export default {
|
|||
VueMultiselect,
|
||||
},
|
||||
props: {
|
||||
/** Array of tags to be pre-selected
|
||||
/**
|
||||
* Array of tags to be pre-selected
|
||||
* @type {Tag[]}
|
||||
*/
|
||||
preSelectedTags: {
|
||||
|
@ -244,23 +244,30 @@ export default {
|
|||
this.getExistingTags();
|
||||
},
|
||||
methods: {
|
||||
/** Show the add tag dialog */
|
||||
/**
|
||||
* Show the add tag dialog
|
||||
* @returns {void}
|
||||
*/
|
||||
showAddDialog() {
|
||||
this.modal.show();
|
||||
},
|
||||
/** Get all existing tags */
|
||||
/**
|
||||
* Get all existing tags
|
||||
* @returns {void}
|
||||
*/
|
||||
getExistingTags() {
|
||||
this.$root.getSocket().emit("getTags", (res) => {
|
||||
if (res.ok) {
|
||||
this.existingTags = res.tags;
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
this.$root.toastError(res.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Delete the specified tag
|
||||
* @param {Object} tag Object representing tag to delete
|
||||
* @param {object} item Object representing tag to delete
|
||||
* @returns {void}
|
||||
*/
|
||||
deleteTag(item) {
|
||||
if (item.new) {
|
||||
|
@ -273,10 +280,10 @@ export default {
|
|||
},
|
||||
/**
|
||||
* Get colour of text inside the tag
|
||||
* @param {Object} option The tag that needs to be displayed.
|
||||
* @param {object} option The tag that needs to be displayed.
|
||||
* Defaults to "white" unless the tag has no color, which will
|
||||
* then return the body color (based on application theme)
|
||||
* @returns string
|
||||
* @returns {string} Text color
|
||||
*/
|
||||
textColor(option) {
|
||||
if (option.color) {
|
||||
|
@ -285,7 +292,10 @@ export default {
|
|||
return this.$root.theme === "light" ? "var(--bs-body-color)" : "inherit";
|
||||
}
|
||||
},
|
||||
/** Add a draft tag */
|
||||
/**
|
||||
* Add a draft tag
|
||||
* @returns {void}
|
||||
*/
|
||||
addDraftTag() {
|
||||
console.log("Adding Draft Tag: ", this.newDraftTag);
|
||||
if (this.newDraftTag.select != null) {
|
||||
|
@ -313,7 +323,10 @@ export default {
|
|||
}
|
||||
this.clearDraftTag();
|
||||
},
|
||||
/** Remove a draft tag */
|
||||
/**
|
||||
* Remove a draft tag
|
||||
* @returns {void}
|
||||
*/
|
||||
clearDraftTag() {
|
||||
this.newDraftTag = {
|
||||
name: null,
|
||||
|
@ -327,7 +340,7 @@ export default {
|
|||
},
|
||||
/**
|
||||
* Add a tag asynchronously
|
||||
* @param {Object} newTag Object representing new tag to add
|
||||
* @param {object} newTag Object representing new tag to add
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
addTagAsync(newTag) {
|
||||
|
@ -359,7 +372,10 @@ export default {
|
|||
this.$root.getSocket().emit("deleteMonitorTag", tagId, monitorId, value, resolve);
|
||||
});
|
||||
},
|
||||
/** Handle pressing Enter key when inside the modal */
|
||||
/**
|
||||
* Handle pressing Enter key when inside the modal
|
||||
* @returns {void}
|
||||
*/
|
||||
onEnter() {
|
||||
if (!this.validateDraftTag.invalid) {
|
||||
this.addDraftTag();
|
||||
|
@ -368,7 +384,7 @@ export default {
|
|||
/**
|
||||
* Submit the form data
|
||||
* @param {number} monitorId ID of monitor this change affects
|
||||
* @returns {void}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async submit(monitorId) {
|
||||
console.log(`Submitting tag changes for monitor ${monitorId}...`);
|
||||
|
@ -381,7 +397,7 @@ export default {
|
|||
let newTagResult;
|
||||
await this.addTagAsync(newTag).then((res) => {
|
||||
if (!res.ok) {
|
||||
toast.error(res.msg);
|
||||
this.$root.toastError(res.msg);
|
||||
newTagResult = false;
|
||||
}
|
||||
newTagResult = res.tag;
|
||||
|
@ -406,7 +422,7 @@ export default {
|
|||
// Assign tag to monitor
|
||||
await this.addMonitorTagAsync(tagId, monitorId, newTag.value).then((res) => {
|
||||
if (!res.ok) {
|
||||
toast.error(res.msg);
|
||||
this.$root.toastError(res.msg);
|
||||
newMonitorTagResult = false;
|
||||
}
|
||||
newMonitorTagResult = true;
|
||||
|
@ -422,7 +438,7 @@ export default {
|
|||
let deleteMonitorTagResult;
|
||||
await this.deleteMonitorTagAsync(deleteTag.tag_id, deleteTag.monitor_id, deleteTag.value).then((res) => {
|
||||
if (!res.ok) {
|
||||
toast.error(res.msg);
|
||||
this.$root.toastError(res.msg);
|
||||
deleteMonitorTagResult = false;
|
||||
}
|
||||
deleteMonitorTagResult = true;
|
||||
|
|
|
@ -76,8 +76,6 @@
|
|||
import { Modal } from "bootstrap";
|
||||
import Confirm from "./Confirm.vue";
|
||||
import VueQrcode from "vue-qrcode";
|
||||
import { useToast } from "vue-toastification";
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -101,22 +99,34 @@ export default {
|
|||
this.getStatus();
|
||||
},
|
||||
methods: {
|
||||
/** Show the dialog */
|
||||
/**
|
||||
* Show the dialog
|
||||
* @returns {void}
|
||||
*/
|
||||
show() {
|
||||
this.modal.show();
|
||||
},
|
||||
|
||||
/** Show dialog to confirm enabling 2FA */
|
||||
/**
|
||||
* Show dialog to confirm enabling 2FA
|
||||
* @returns {void}
|
||||
*/
|
||||
confirmEnableTwoFA() {
|
||||
this.$refs.confirmEnableTwoFA.show();
|
||||
},
|
||||
|
||||
/** Show dialog to confirm disabling 2FA */
|
||||
/**
|
||||
* Show dialog to confirm disabling 2FA
|
||||
* @returns {void}
|
||||
*/
|
||||
confirmDisableTwoFA() {
|
||||
this.$refs.confirmDisableTwoFA.show();
|
||||
},
|
||||
|
||||
/** Prepare 2FA configuration */
|
||||
/**
|
||||
* Prepare 2FA configuration
|
||||
* @returns {void}
|
||||
*/
|
||||
prepare2FA() {
|
||||
this.processing = true;
|
||||
|
||||
|
@ -126,12 +136,15 @@ export default {
|
|||
if (res.ok) {
|
||||
this.uri = res.uri;
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
this.$root.toastError(res.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/** Save the current 2FA configuration */
|
||||
/**
|
||||
* Save the current 2FA configuration
|
||||
* @returns {void}
|
||||
*/
|
||||
save2FA() {
|
||||
this.processing = true;
|
||||
|
||||
|
@ -144,12 +157,15 @@ export default {
|
|||
this.currentPassword = "";
|
||||
this.modal.hide();
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
this.$root.toastError(res.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/** Disable 2FA for this user */
|
||||
/**
|
||||
* Disable 2FA for this user
|
||||
* @returns {void}
|
||||
*/
|
||||
disable2FA() {
|
||||
this.processing = true;
|
||||
|
||||
|
@ -162,29 +178,35 @@ export default {
|
|||
this.currentPassword = "";
|
||||
this.modal.hide();
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
this.$root.toastError(res.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/** Verify the token generated by the user */
|
||||
/**
|
||||
* Verify the token generated by the user
|
||||
* @returns {void}
|
||||
*/
|
||||
verifyToken() {
|
||||
this.$root.getSocket().emit("verifyToken", this.token, this.currentPassword, (res) => {
|
||||
if (res.ok) {
|
||||
this.tokenValid = res.valid;
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
this.$root.toastError(res.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/** Get current status of 2FA */
|
||||
/**
|
||||
* Get current status of 2FA
|
||||
* @returns {void}
|
||||
*/
|
||||
getStatus() {
|
||||
this.$root.getSocket().emit("twoFAStatus", (res) => {
|
||||
if (res.ok) {
|
||||
this.twoFAStatus = res.status;
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
this.$root.toastError(res.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -84,10 +84,12 @@ export default {
|
|||
},
|
||||
|
||||
title() {
|
||||
if (this.type === "1y") {
|
||||
return `1${this.$t("-year")}`;
|
||||
}
|
||||
if (this.type === "720") {
|
||||
return `30${this.$t("-day")}`;
|
||||
}
|
||||
|
||||
return `24${this.$t("-hour")}`;
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="Bark API Version" class="form-label">{{ $t("Bark API Version") }}</label>
|
||||
<select id="Bark API Version" v-model="$parent.notification.apiVersion" class="form-select" required>
|
||||
<option value="v1">v1</option>
|
||||
<option value="v2">v2</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="Bark Endpoint" class="form-label">{{ $t("Bark Endpoint") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="Bark Endpoint" v-model="$parent.notification.barkEndpoint" type="text" class="form-control" required>
|
||||
|
|
24
src/components/notifications/Bitrix24.vue
Normal file
24
src/components/notifications/Bitrix24.vue
Normal file
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="bitrix24-webhook-url" class="form-label">{{ $t("Bitrix24 Webhook URL") }}</label>
|
||||
<HiddenInput id="bitrix24-webhook-url" v-model="$parent.notification.bitrix24WebhookURL" :required="true" autocomplete="new-password"></HiddenInput>
|
||||
<i18n-t tag="div" keypath="wayToGetBitrix24Webhook" class="form-text">
|
||||
<a href="https://helpdesk.bitrix24.com/open/12357038/" target="_blank">https://helpdesk.bitrix24.com/open/12357038/</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="bitrix24-user-id" class="form-label">{{ $t("User ID") }}</label>
|
||||
<input id="bitrix24-user-id" v-model="$parent.notification.bitrix24UserID" type="text" class="form-control" required>
|
||||
<div class="form-text">{{ $t("bitrix24SupportUserID") }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
}
|
||||
};
|
||||
</script>
|
13
src/components/notifications/CallMeBot.vue
Normal file
13
src/components/notifications/CallMeBot.vue
Normal file
|
@ -0,0 +1,13 @@
|
|||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="callmebot-endpoint" class="form-label">{{ $t("Endpoint") }}</label>
|
||||
<input id="callmebot-endpoint" v-model="$parent.notification.callMeBotEndpoint" type="text" class="form-control" required>
|
||||
<i18n-t tag="div" keypath="callMeBotGet" class="form-text">
|
||||
<a href="https://www.callmebot.com/blog/free-api-facebook-messenger/" target="_blank">Facebook Messenger</a>
|
||||
<a href="https://www.callmebot.com/blog/test-whatsapp-api/" target="_blank">WhatsApp</a>
|
||||
<a href="https://www.callmebot.com/blog/telegram-phone-call-using-your-browser/" target="_blank">Telegram Call</a>
|
||||
1 message / 10 sec; 1 call / 65 sec
|
||||
<!--There is no public documentation available. This data is based on testing!-->
|
||||
</i18n-t>
|
||||
</div>
|
||||
</template>
|
54
src/components/notifications/Cellsynt.vue
Normal file
54
src/components/notifications/Cellsynt.vue
Normal file
|
@ -0,0 +1,54 @@
|
|||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="cellsynt-login" class="form-label">{{ $t("Username") }}</label>
|
||||
<input id="cellsynt-login" v-model="$parent.notification.cellsyntLogin" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="cellsynt-key" class="form-label">{{ $t("Password") }}</label>
|
||||
<HiddenInput id="cellsynt-key" v-model="$parent.notification.cellsyntPassword" :required="true" autocomplete="new-password"></HiddenInput>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="cellsynt-Originatortype" class="form-label">{{ $t("Originator type") }}</label>
|
||||
<select id="cellsynt-Originatortype" v-model="$parent.notification.cellsyntOriginatortype" :required="true" class="form-select">
|
||||
<option value="alpha">{{ $t("Alphanumeric (recommended)") }}</option>
|
||||
<option value="numeric">{{ $t("Telephone number") }}</option>
|
||||
</select>
|
||||
<div class="form-text">
|
||||
<p><b>{{ $t("Alphanumeric (recommended)") }}:</b><br /> {{ $t("Alphanumeric string (max 11 alphanumeric characters). Recipients can not reply to the message.") }}</p>
|
||||
<p><b>{{ $t("Telephone number") }}:</b><br /> {{ $t("Numeric value (max 15 digits) with telephone number on international format without leading 00 (example UK number 07920 110 000 should be set as 447920110000). Recipients can reply to the message.") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="cellsynt-originator" class="form-label">{{ $t("Originator") }} <small>({{ $parent.notification.cellsyntOriginatortype === 'alpha' ? $t("max 11 alphanumeric characters") : $t("max 15 digits") }})</small></label>
|
||||
<input v-if="$parent.notification.cellsyntOriginatortype === 'alpha'" id="cellsynt-originator" v-model="$parent.notification.cellsyntOriginator" type="text" class="form-control" pattern="[a-zA-Z0-9\s]+" maxlength="11" required>
|
||||
<input v-else id="cellsynt-originator" v-model="$parent.notification.cellsyntOriginator" type="number" class="form-control" pattern="[0-9]+" maxlength="15" required>
|
||||
<div class="form-text"><p>{{ $t("Visible on recipient's mobile phone as originator of the message. Allowed values and function depends on parameter originatortype.") }}</p></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="cellsynt-destination" class="form-label">{{ $t("Destination") }}</label>
|
||||
<input id="cellsynt-destination" v-model="$parent.notification.cellsyntDestination" type="text" class="form-control" required>
|
||||
<div class="form-text"><p>{{ $t("Recipient's telephone number using international format with leading 00 followed by country code, e.g. 00447920110000 for the UK number 07920 110 000 (max 17 digits in total). Max 25000 comma separated recipients per HTTP request.") }}</p></div>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input id="cellsynt-allow-long" v-model="$parent.notification.cellsyntAllowLongSMS" type="checkbox" class="form-check-input">
|
||||
<label for="cellsynt-allow-long" class="form-label">{{ $t("Allow Long SMS") }}</label>
|
||||
<div class="form-text">{{ $t("Split long messages into up to 6 parts. 153 x 6 = 918 characters.") }}</div>
|
||||
</div>
|
||||
<i18n-t tag="p" keypath="More info on:" style="margin-top: 8px;">
|
||||
<a href="https://www.cellsynt.com/en/" target="_blank">https://www.cellsynt.com/en/</a>
|
||||
</i18n-t>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput
|
||||
},
|
||||
mounted() {
|
||||
this.$parent.notification.cellsyntOriginatortype ||= "alpha";
|
||||
this.$parent.notification.cellsyntOriginator ||= "uptimekuma";
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -2,9 +2,10 @@
|
|||
<div class="mb-3">
|
||||
<label for="WebHookUrl" class="form-label">{{ $t("WebHookUrl") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="WebHookUrl" v-model="$parent.notification.webHookUrl" type="text" class="form-control" required>
|
||||
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="secretKey" class="form-label">{{ $t("SecretKey") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="secretKey" v-model="$parent.notification.secretKey" type="text" class="form-control" required>
|
||||
<HiddenInput id="secretKey" v-model="$parent.notification.secretKey" :required="true" autocomplete="new-password"></HiddenInput>
|
||||
|
||||
<div class="form-text">
|
||||
<p>{{ $t("For safety, must use secret key") }}</p>
|
||||
|
@ -13,4 +14,24 @@
|
|||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="mentioning" class="form-label">{{ $t("Mentioning") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<select id="mentioning" v-model="$parent.notification.mentioning" class="form-select" required>
|
||||
<option value="nobody">{{ $t("Don't mention people") }}</option>
|
||||
<option value="everyone">{{ $t("Mention group", { group: "@everyone" }) }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: { HiddenInput },
|
||||
mounted() {
|
||||
if (typeof this.$parent.notification.mentioning === "undefined") {
|
||||
this.$parent.notification.mentioning = "nobody";
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -16,4 +16,50 @@
|
|||
<label for="discord-prefix-message" class="form-label">{{ $t("Prefix Custom Message") }}</label>
|
||||
<input id="discord-prefix-message" v-model="$parent.notification.discordPrefixMessage" type="text" class="form-control" autocomplete="false" :placeholder="$t('Hello @everyone is...')">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="discord-message-type" class="form-label">{{ $t("Select message type") }}</label>
|
||||
<select id="discord-message-type" v-model="$parent.notification.discordChannelType" class="form-select">
|
||||
<option value="channel">{{ $t("Send to channel") }}</option>
|
||||
<option value="createNewForumPost">{{ $t("Create new forum post") }}</option>
|
||||
<option value="postToThread">{{ $t("postToExistingThread") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="$parent.notification.discordChannelType === 'createNewForumPost'">
|
||||
<div class="mb-3">
|
||||
<label for="discord-target" class="form-label">
|
||||
{{ $t("forumPostName") }}
|
||||
</label>
|
||||
<input id="discord-target" v-model="$parent.notification.postName" type="text" class="form-control" autocomplete="false">
|
||||
<div class="form-text">
|
||||
{{ $t("whatHappensAtForumPost", { option: $t("postToExistingThread") }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$parent.notification.discordChannelType === 'postToThread'">
|
||||
<div class="mb-3">
|
||||
<label for="discord-target" class="form-label">
|
||||
{{ $t("threadForumPostID") }}
|
||||
</label>
|
||||
<input id="discord-target" v-model="$parent.notification.threadId" type="text" class="form-control" autocomplete="false" :placeholder="$t('e.g. {discordThreadID}', { discordThreadID: 1177566663751782411 })">
|
||||
<div class="form-text">
|
||||
<i18n-t keypath="wayToGetDiscordThreadId">
|
||||
<a
|
||||
href="https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-"
|
||||
target="_blank"
|
||||
>{{ $t("here") }}</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
mounted() {
|
||||
if (!this.$parent.notification.discordChannelType) {
|
||||
this.$parent.notification.discordChannelType = "channel";
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
7
src/components/notifications/GrafanaOncall.vue
Normal file
7
src/components/notifications/GrafanaOncall.vue
Normal file
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="GrafanaOncallURL" class="form-label">{{ $t("GrafanaOncallURL") }}<span style="color: red;"><sup>*</sup></span></label>
|
||||
<input id="GrafanaOncallURL" v-model="$parent.notification.GrafanaOncallURL" type="text" class="form-control" required>
|
||||
</div>
|
||||
</template>
|
||||
|
49
src/components/notifications/GtxMessaging.vue
Normal file
49
src/components/notifications/GtxMessaging.vue
Normal file
|
@ -0,0 +1,49 @@
|
|||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="gtxmessaging-api-key" class="form-label">{{ $t("API Key") }}</label>
|
||||
<HiddenInput id="gtxmessaging-api-key" v-model="$parent.notification.gtxMessagingApiKey" :required="true"></HiddenInput>
|
||||
<div class="form-text">
|
||||
{{ $t("gtxMessagingApiKeyHint") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="gtxmessaging-from" class="form-label">{{ $t("From Phone Number / Transmission Path Originating Address (TPOA)") }}</label>
|
||||
<input id="gtxmessaging-from" v-model="$parent.notification.gtxMessagingFrom" type="text" class="form-control" required>
|
||||
<i18n-t tag="div" keypath="gtxMessagingFromHint" class="form-text">
|
||||
<template #e164>
|
||||
<a href="https://wikipedia.org/wiki/E.164">E.164</a>
|
||||
</template>
|
||||
<template #e212>
|
||||
<a href="https://wikipedia.org/wiki/E.212">E.212</a>
|
||||
</template>
|
||||
<template #e214>
|
||||
<a href="https://wikipedia.org/wiki/E.214">E.214</a>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="gtxmessaging-to" class="form-label">{{ $t("To Phone Number") }}</label>
|
||||
<input id="gtxmessaging-to" v-model="$parent.notification.gtxMessagingTo" type="text" pattern="^\+\d+$" class="form-control" required>
|
||||
<i18n-t tag="div" keypath="gtxMessagingToHint" class="form-text">
|
||||
<template #e164>
|
||||
<a href="https://wikipedia.org/wiki/E.164">E.164</a>
|
||||
</template>
|
||||
<template #e212>
|
||||
<a href="https://wikipedia.org/wiki/E.212">E.212</a>
|
||||
</template>
|
||||
<template #e214>
|
||||
<a href="https://wikipedia.org/wiki/E.214">E.214</a>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput
|
||||
}
|
||||
};
|
||||
</script>
|
34
src/components/notifications/HeiiOnCall.vue
Normal file
34
src/components/notifications/HeiiOnCall.vue
Normal file
|
@ -0,0 +1,34 @@
|
|||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="heiioncall-apikey" class="form-label">{{ $t("API Key") }}<span
|
||||
style="color: red;"
|
||||
><sup>*</sup></span></label>
|
||||
<HiddenInput
|
||||
id="heiioncall-apikey" v-model="$parent.notification.heiiOnCallApiKey" required="true"
|
||||
autocomplete="false"
|
||||
></HiddenInput>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="heiioncall-trigger-id" class="form-label">Trigger ID<span
|
||||
style="color: red;"
|
||||
><sup>*</sup></span></label>
|
||||
<HiddenInput
|
||||
id="heiioncall-trigger-id" v-model="$parent.notification.heiiOnCallTriggerId" required="true"
|
||||
autocomplete="false"
|
||||
></HiddenInput>
|
||||
</div>
|
||||
<i18n-t tag="p" keypath="wayToGetHeiiOnCallDetails" class="form-text mt-3">
|
||||
<template #documentation>
|
||||
<a href="https://heiioncall.com/docs" target="_blank">{{ $t("documentationOf", ["Heii On-Call"]) }}</a>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
};
|
||||
</script>
|
42
src/components/notifications/Keep.vue
Normal file
42
src/components/notifications/Keep.vue
Normal file
|
@ -0,0 +1,42 @@
|
|||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="webhook-url" class="form-label">{{ $t("Host URL") }}</label>
|
||||
<input
|
||||
id="webhook-url"
|
||||
v-model="$parent.notification.webhookURL"
|
||||
type="url"
|
||||
pattern="https?://.+"
|
||||
class="form-control"
|
||||
required
|
||||
/>
|
||||
<div class="form-text">
|
||||
<i18n-t tag="p" keypath="Read more:">
|
||||
<a href="https://docs.keephq.dev/providers/documentation/uptimekuma-provider" target="_blank">https://docs.keephq.dev/providers/documentation/uptimekuma-provider</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="webhook-apikey" class="form-label">{{
|
||||
$t("API Key")
|
||||
}}</label>
|
||||
<HiddenInput
|
||||
id="webhook-apikey"
|
||||
v-model="$parent.notification.webhookAPIKey"
|
||||
:required="true"
|
||||
></HiddenInput>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
mounted() {
|
||||
this.$parent.notification.webhookURL ||= "";
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,17 +1,17 @@
|
|||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="line-channel-access-token" class="form-label">{{ $t("Channel access token") }}</label>
|
||||
<label for="line-channel-access-token" class="form-label">{{ $t("Channel access token (Long-lived)") }}</label>
|
||||
<HiddenInput id="line-channel-access-token" v-model="$parent.notification.lineChannelAccessToken" :required="true" autocomplete="new-password"></HiddenInput>
|
||||
</div>
|
||||
<i18n-t tag="div" keypath="lineDevConsoleTo" class="form-text">
|
||||
<b>{{ $t("Basic Settings") }}</b>
|
||||
<b>{{ $t("Messaging API") }}</b>
|
||||
</i18n-t>
|
||||
<div class="mb-3" style="margin-top: 12px;">
|
||||
<label for="line-user-id" class="form-label">{{ $t("User ID") }}</label>
|
||||
<label for="line-user-id" class="form-label">{{ $t("Your User ID") }}</label>
|
||||
<input id="line-user-id" v-model="$parent.notification.lineUserID" type="text" class="form-control" required>
|
||||
</div>
|
||||
<i18n-t tag="div" keypath="lineDevConsoleTo" class="form-text">
|
||||
<b>{{ $t("Messaging API") }}</b>
|
||||
<b>{{ $t("Basic Settings") }}</b>
|
||||
</i18n-t>
|
||||
<i18n-t tag="div" keypath="wayToGetLineChannelToken" class="form-text" style="margin-top: 8px;">
|
||||
<a href="https://developers.line.biz/console/" target="_blank">{{ $t("Line Developers Console") }}</a>
|
||||
|
|
39
src/components/notifications/SMSPartner.vue
Normal file
39
src/components/notifications/SMSPartner.vue
Normal file
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="smspartner-key" class="form-label">{{ $t("API Key") }}</label>
|
||||
<HiddenInput id="smspartner-key" v-model="$parent.notification.smspartnerApikey" :required="true" autocomplete="new-password"></HiddenInput>
|
||||
<div class="form-text">
|
||||
<i18n-t keypath="smspartnerApiurl" tag="div" class="form-text">
|
||||
<a href="https://my.smspartner.fr/dashboard/api" target="_blank">my.smspartner.fr/dashboard/api</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="smspartner-phone-number" class="form-label">{{ $t("smspartnerPhoneNumber") }}</label>
|
||||
<input id="smspartner-phone-number" v-model="$parent.notification.smspartnerPhoneNumber" type="text" minlength="3" maxlength="20" pattern="^[\d+,]+$" class="form-control" required>
|
||||
<div class="form-text">
|
||||
<i18n-t keypath="smspartnerPhoneNumberHelptext" tag="div" class="form-text">
|
||||
<code>+336xxxxxxxx</code>
|
||||
<code>+496xxxxxxxx</code>
|
||||
<code>,</code>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="smspartner-sender-name" class="form-label">{{ $t("smspartnerSenderName") }}</label>
|
||||
<input id="smspartner-sender-name" v-model="$parent.notification.smspartnerSenderName" type="text" minlength="3" maxlength="11" pattern="^[a-zA-Z0-9]*$" class="form-control" required>
|
||||
<div class="form-text">
|
||||
{{ $t("smspartnerSenderNameInfo") }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -67,6 +67,28 @@
|
|||
<input id="to-bcc" v-model="$parent.notification.smtpBCC" type="text" class="form-control" autocomplete="false" :required="!hasRecipient">
|
||||
</div>
|
||||
|
||||
<p class="form-text">
|
||||
<i18n-t tag="div" keypath="smtpLiquidIntroduction" class="form-text mb-3">
|
||||
<a href="https://liquidjs.com/" target="_blank">{{ $t("documentation") }}</a>
|
||||
</i18n-t>
|
||||
<code v-pre>{{name}}</code>: {{ $t("emailTemplateServiceName") }}<br />
|
||||
<code v-pre>{{msg}}</code>: {{ $t("emailTemplateMsg") }}<br />
|
||||
<code v-pre>{{status}}</code>: {{ $t("emailTemplateStatus") }}<br />
|
||||
<code v-pre>{{heartbeatJSON}}</code>: {{ $t("emailTemplateHeartbeatJSON") }}<b>{{ $t("emailTemplateLimitedToUpDownNotification") }}</b><br />
|
||||
<code v-pre>{{monitorJSON}}</code>: {{ $t("emailTemplateMonitorJSON") }} <b>{{ $t("emailTemplateLimitedToUpDownNotification") }}</b><br />
|
||||
<code v-pre>{{hostnameOrURL}}</code>: {{ $t("emailTemplateHostnameOrURL") }}<br />
|
||||
</p>
|
||||
<div class="mb-3">
|
||||
<label for="subject-email" class="form-label">{{ $t("emailCustomSubject") }}</label>
|
||||
<input id="subject-email" v-model="$parent.notification.customSubject" type="text" class="form-control" autocomplete="false" placeholder="">
|
||||
<div class="form-text">{{ $t("leave blank for default subject") }}</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="body-email" class="form-label">{{ $t("emailCustomBody") }}</label>
|
||||
<textarea id="body-email" v-model="$parent.notification.customBody" type="text" class="form-control" autocomplete="false" placeholder=""></textarea>
|
||||
<div class="form-text">{{ $t("leave blank for default body") }}</div>
|
||||
</div>
|
||||
|
||||
<ToggleSection :heading="$t('smtpDkimSettings')">
|
||||
<i18n-t tag="div" keypath="smtpDkimDesc" class="form-text mb-3">
|
||||
<a href="https://nodemailer.com/dkim/" target="_blank">{{ $t("documentation") }}</a>
|
||||
|
@ -97,17 +119,6 @@
|
|||
<input id="dkim-skip-fields" v-model="$parent.notification.smtpDkimskipFields" type="text" class="form-control" autocomplete="false" placeholder="message-id:date">
|
||||
</div>
|
||||
</ToggleSection>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="subject-email" class="form-label">{{ $t("emailCustomSubject") }}</label>
|
||||
<input id="subject-email" v-model="$parent.notification.customSubject" type="text" class="form-control" autocomplete="false" placeholder="">
|
||||
<div v-pre class="form-text">
|
||||
(leave blank for default one)<br />
|
||||
{{NAME}}: Service Name<br />
|
||||
{{HOSTNAME_OR_URL}}: Hostname or URL<br />
|
||||
{{STATUS}}: Status<br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
31
src/components/notifications/SevenIO.vue
Normal file
31
src/components/notifications/SevenIO.vue
Normal file
|
@ -0,0 +1,31 @@
|
|||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="sevenio-api-key" class="form-label">{{ $t("apiKeySevenIO") }}</label>
|
||||
<HiddenInput id="sevenio-api-key" v-model="$parent.notification.sevenioApiKey" :required="true" autocomplete="new-password"></HiddenInput>
|
||||
<div class="form-text">
|
||||
{{ $t("wayToGetSevenIOApiKey") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="sevenio-sender" class="form-label">{{ $t("senderSevenIO") }}</label>
|
||||
<input id="sevenio-sender" v-model="$parent.notification.sevenioSender" type="text" class="form-control" autocomplete="false" placeholder="Uptime Kuma">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="sevenio-receiver" class="form-label">{{ $t("receiverSevenIO") }}</label>
|
||||
<input id="sevenio-receiver" v-model="$parent.notification.sevenioReceiver" type="number" class="form-control" required autocomplete="false" placeholder="0123456789">
|
||||
<div class="form-text">
|
||||
{{ $t("receiverInfoSevenIO") }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -58,8 +58,6 @@
|
|||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
import axios from "axios";
|
||||
import { useToast } from "vue-toastification";
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -68,7 +66,7 @@ export default {
|
|||
methods: {
|
||||
/**
|
||||
* Get the URL for telegram updates
|
||||
* @param {string} [mode=masked] Should the token be masked?
|
||||
* @param {string} mode Should the token be masked?
|
||||
* @returns {string} formatted URL
|
||||
*/
|
||||
telegramGetUpdatesURL(mode = "masked") {
|
||||
|
@ -85,7 +83,11 @@ export default {
|
|||
return `https://api.telegram.org/bot${token}/getUpdates`;
|
||||
},
|
||||
|
||||
/** Get the telegram chat ID */
|
||||
/**
|
||||
* Get the telegram chat ID
|
||||
* @returns {Promise<void>}
|
||||
* @throws The chat ID could not be found
|
||||
*/
|
||||
async autoGetTelegramChatID() {
|
||||
try {
|
||||
let res = await axios.get(this.telegramGetUpdatesURL("withToken"));
|
||||
|
@ -106,7 +108,7 @@ export default {
|
|||
}
|
||||
|
||||
} catch (error) {
|
||||
toast.error(error.message);
|
||||
this.$root.toastError(error.message);
|
||||
}
|
||||
|
||||
},
|
||||
|
|
87
src/components/notifications/Threema.vue
Normal file
87
src/components/notifications/Threema.vue
Normal file
|
@ -0,0 +1,87 @@
|
|||
<template>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="threema-recipient">{{ $t("threemaRecipientType") }}</label>
|
||||
<select
|
||||
id="threema-recipient" v-model="$parent.notification.threemaRecipientType" required
|
||||
class="form-select"
|
||||
>
|
||||
<option value="identity">{{ $t("threemaRecipientTypeIdentity") }}</option>
|
||||
<option value="phone">{{ $t("threemaRecipientTypePhone") }}</option>
|
||||
<option value="email">{{ $t("threemaRecipientTypeEmail") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="$parent.notification.threemaRecipientType === 'identity'" class="mb-3">
|
||||
<label class="form-label" for="threema-recipient">{{ $t("threemaRecipient") }} {{ $t("threemaRecipientTypeIdentity") }}</label>
|
||||
<input
|
||||
id="threema-recipient"
|
||||
v-model="$parent.notification.threemaRecipient"
|
||||
class="form-control"
|
||||
minlength="8"
|
||||
maxlength="8"
|
||||
pattern="[A-Z0-9]{8}"
|
||||
required
|
||||
type="text"
|
||||
>
|
||||
<div class="form-text">
|
||||
<p>{{ $t("threemaRecipientTypeIdentityFormat") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="$parent.notification.threemaRecipientType === 'phone'" class="mb-3">
|
||||
<label class="form-label" for="threema-recipient">{{ $t("threemaRecipient") }} {{ $t("threemaRecipientTypePhone") }}</label>
|
||||
<input
|
||||
id="threema-recipient"
|
||||
v-model="$parent.notification.threemaRecipient"
|
||||
class="form-control"
|
||||
maxlength="15"
|
||||
pattern="\d{1,15}"
|
||||
required
|
||||
type="text"
|
||||
>
|
||||
<div class="form-text">
|
||||
<p>{{ $t("threemaRecipientTypePhoneFormat") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="$parent.notification.threemaRecipientType === 'email'" class="mb-3">
|
||||
<label class="form-label" for="threema-recipient">{{ $t("threemaRecipient") }} {{ $t("threemaRecipientTypeEmail") }}</label>
|
||||
<input
|
||||
id="threema-recipient"
|
||||
v-model="$parent.notification.threemaRecipient"
|
||||
class="form-control"
|
||||
maxlength="254"
|
||||
required
|
||||
type="email"
|
||||
>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="threema-sender">{{ $t("threemaSenderIdentity") }}</label>
|
||||
<input
|
||||
id="threema-sender"
|
||||
v-model="$parent.notification.threemaSenderIdentity"
|
||||
class="form-control"
|
||||
minlength="8"
|
||||
maxlength="8"
|
||||
pattern="^\*[A-Z0-9]{7}$"
|
||||
required
|
||||
type="text"
|
||||
>
|
||||
<div class="form-text">
|
||||
<p>{{ $t("threemaSenderIdentityFormat") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="threema-secret">{{ $t("threemaApiAuthenticationSecret") }}</label>
|
||||
<HiddenInput
|
||||
id="threema-secret" v-model="$parent.notification.threemaSecret" required
|
||||
autocomplete="false"
|
||||
></HiddenInput>
|
||||
</div>
|
||||
<i18n-t class="form-text" keypath="wayToGetThreemaGateway" tag="div">
|
||||
<a href="https://threema.ch/en/gateway" target="_blank">{{ $t("here") }}</a>
|
||||
</i18n-t>
|
||||
<i18n-t class="form-text" keypath="threemaBasicModeInfo" tag="div">
|
||||
<a href="https://gateway.threema.ch/en/developer/api" target="_blank">{{ $t("here") }}</a>
|
||||
</i18n-t>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
</script>
|
|
@ -12,9 +12,7 @@
|
|||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="webhook-request-body" class="form-label">{{
|
||||
$t("Request Body")
|
||||
}}</label>
|
||||
<label for="webhook-request-body" class="form-label">{{ $t("Request Body") }}</label>
|
||||
<select
|
||||
id="webhook-request-body"
|
||||
v-model="$parent.notification.webhookContentType"
|
||||
|
@ -26,40 +24,29 @@
|
|||
<option value="custom">{{ $t("webhookBodyCustomOption") }}</option>
|
||||
</select>
|
||||
|
||||
<div class="form-text">
|
||||
<div v-if="$parent.notification.webhookContentType == 'json'">
|
||||
<p>{{ $t("webhookJsonDesc", ['"application/json"']) }}</p>
|
||||
</div>
|
||||
<div v-if="$parent.notification.webhookContentType == 'form-data'">
|
||||
<i18n-t tag="p" keypath="webhookFormDataDesc">
|
||||
<template #multipart>multipart/form-data"</template>
|
||||
<template #decodeFunction>
|
||||
<strong>json_decode($_POST['data'])</strong>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
<div v-if="$parent.notification.webhookContentType == 'custom'">
|
||||
<i18n-t tag="p" keypath="webhookCustomBodyDesc">
|
||||
<template #msg>
|
||||
<code>msg</code>
|
||||
</template>
|
||||
<template #heartbeat>
|
||||
<code>heartbeatJSON</code>
|
||||
</template>
|
||||
<template #monitor>
|
||||
<code>monitorJSON</code>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$parent.notification.webhookContentType == 'json'" class="form-text">{{ $t("webhookJsonDesc", ['"application/json"']) }}</div>
|
||||
<i18n-t v-else-if="$parent.notification.webhookContentType == 'form-data'" tag="div" keypath="webhookFormDataDesc" class="form-text">
|
||||
<template #multipart>multipart/form-data"</template>
|
||||
<template #decodeFunction>
|
||||
<strong>json_decode($_POST['data'])</strong>
|
||||
</template>
|
||||
</i18n-t>
|
||||
<template v-else-if="$parent.notification.webhookContentType == 'custom'">
|
||||
<i18n-t tag="div" keypath="liquidIntroduction" class="form-text">
|
||||
<a href="https://liquidjs.com/" target="_blank">{{ $t("documentation") }}</a>
|
||||
</i18n-t>
|
||||
<code v-pre>{{msg}}</code>: {{ $t("templateMsg") }}<br />
|
||||
<code v-pre>{{heartbeatJSON}}</code>: {{ $t("templateHeartbeatJSON") }} <b>({{ $t("templateLimitedToUpDownNotifications") }})</b><br />
|
||||
<code v-pre>{{monitorJSON}}</code>: {{ $t("templateMonitorJSON") }} <b>({{ $t("templateLimitedToUpDownCertNotifications") }})</b><br />
|
||||
|
||||
<textarea
|
||||
v-if="$parent.notification.webhookContentType == 'custom'"
|
||||
id="customBody"
|
||||
v-model="$parent.notification.webhookCustomBody"
|
||||
class="form-control"
|
||||
:placeholder="customBodyPlaceholder"
|
||||
></textarea>
|
||||
<textarea
|
||||
id="customBody"
|
||||
v-model="$parent.notification.webhookCustomBody"
|
||||
class="form-control"
|
||||
:placeholder="customBodyPlaceholder"
|
||||
required
|
||||
></textarea>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
|
@ -67,15 +54,14 @@
|
|||
<input v-model="showAdditionalHeadersField" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label">{{ $t("webhookAdditionalHeadersTitle") }}</label>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
<i18n-t tag="p" keypath="webhookAdditionalHeadersDesc"> </i18n-t>
|
||||
</div>
|
||||
<div class="form-text">{{ $t("webhookAdditionalHeadersDesc") }}</div>
|
||||
<textarea
|
||||
v-if="showAdditionalHeadersField"
|
||||
id="additionalHeaders"
|
||||
v-model="$parent.notification.webhookAdditionalHeaders"
|
||||
class="form-control"
|
||||
:placeholder="headersPlaceholder"
|
||||
:required="showAdditionalHeadersField"
|
||||
></textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -90,18 +76,18 @@ export default {
|
|||
computed: {
|
||||
headersPlaceholder() {
|
||||
return this.$t("Example:", [
|
||||
`
|
||||
{
|
||||
`{
|
||||
"Authorization": "Authorization Token"
|
||||
}`,
|
||||
]);
|
||||
},
|
||||
customBodyPlaceholder() {
|
||||
return `Example:
|
||||
{
|
||||
"Title": "Uptime Kuma Alert - {{ monitorJSON['name'] }}",
|
||||
return this.$t("Example:", [
|
||||
`{
|
||||
"Title": "Uptime Kuma Alert{% if monitorJSON %} - {{ monitorJSON['name'] }}{% endif %}",
|
||||
"Body": "{{ msg }}"
|
||||
}`;
|
||||
}`
|
||||
]);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
33
src/components/notifications/Whapi.vue
Normal file
33
src/components/notifications/Whapi.vue
Normal file
|
@ -0,0 +1,33 @@
|
|||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="whapi-api-url" class="form-label">{{ $t("API URL") }}</label>
|
||||
<input id="whapi-api-url" v-model="$parent.notification.whapiApiUrl" placeholder="https://gate.whapi.cloud/" type="text" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="whapi-auth-token" class="form-label">{{ $t("Token") }}</label>
|
||||
<HiddenInput id="whapi-auth-token" v-model="$parent.notification.whapiAuthToken" :required="true" autocomplete="new-password"></HiddenInput>
|
||||
<i18n-t tag="div" keypath="wayToGetWhapiUrlAndToken" class="form-text">
|
||||
<a href="https://panel.whapi.cloud/dashboard" target="_blank">https://panel.whapi.cloud/dashboard</a>
|
||||
</i18n-t>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="whapi-recipient" class="form-label">{{ $t("whapiRecipient") }}</label>
|
||||
<input id="whapi-recipient" v-model="$parent.notification.whapiRecipient" type="text" pattern="^[\d-]{10,31}(@[\w\.]{1,})?$" class="form-control" required>
|
||||
<div class="form-text">{{ $t("wayToWriteWhapiRecipient", ["00117612345678", "00117612345678@s.whatsapp.net", "123456789012345678@g.us"]) }}</div>
|
||||
</div>
|
||||
|
||||
<i18n-t tag="div" keypath="More info on:" class="mb-3 form-text">
|
||||
<a href="https://whapi.cloud/" target="_blank">https://whapi.cloud/</a>
|
||||
</i18n-t>
|
||||
</template>
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -3,7 +3,9 @@ import AlertNow from "./AlertNow.vue";
|
|||
import AliyunSMS from "./AliyunSms.vue";
|
||||
import Apprise from "./Apprise.vue";
|
||||
import Bark from "./Bark.vue";
|
||||
import Bitrix24 from "./Bitrix24.vue";
|
||||
import ClickSendSMS from "./ClickSendSMS.vue";
|
||||
import CallMeBot from "./CallMeBot.vue";
|
||||
import SMSC from "./SMSC.vue";
|
||||
import DingDing from "./DingDing.vue";
|
||||
import Discord from "./Discord.vue";
|
||||
|
@ -12,7 +14,11 @@ import FreeMobile from "./FreeMobile.vue";
|
|||
import GoogleChat from "./GoogleChat.vue";
|
||||
import Gorush from "./Gorush.vue";
|
||||
import Gotify from "./Gotify.vue";
|
||||
import GrafanaOncall from "./GrafanaOncall.vue";
|
||||
import GtxMessaging from "./GtxMessaging.vue";
|
||||
import HomeAssistant from "./HomeAssistant.vue";
|
||||
import HeiiOnCall from "./HeiiOnCall.vue";
|
||||
import Keep from "./Keep.vue";
|
||||
import Kook from "./Kook.vue";
|
||||
import Line from "./Line.vue";
|
||||
import LineNotify from "./LineNotify.vue";
|
||||
|
@ -37,6 +43,7 @@ import ServerChan from "./ServerChan.vue";
|
|||
import SerwerSMS from "./SerwerSMS.vue";
|
||||
import Signal from "./Signal.vue";
|
||||
import SMSManager from "./SMSManager.vue";
|
||||
import SMSPartner from "./SMSPartner.vue";
|
||||
import Slack from "./Slack.vue";
|
||||
import Squadcast from "./Squadcast.vue";
|
||||
import SMSEagle from "./SMSEagle.vue";
|
||||
|
@ -45,16 +52,19 @@ import STMP from "./SMTP.vue";
|
|||
import Teams from "./Teams.vue";
|
||||
import TechulusPush from "./TechulusPush.vue";
|
||||
import Telegram from "./Telegram.vue";
|
||||
import Threema from "./Threema.vue";
|
||||
import Twilio from "./Twilio.vue";
|
||||
import Webhook from "./Webhook.vue";
|
||||
import WeCom from "./WeCom.vue";
|
||||
import GoAlert from "./GoAlert.vue";
|
||||
import ZohoCliq from "./ZohoCliq.vue";
|
||||
import Splunk from "./Splunk.vue";
|
||||
import SevenIO from "./SevenIO.vue";
|
||||
import Whapi from "./Whapi.vue";
|
||||
import Cellsynt from "./Cellsynt.vue";
|
||||
|
||||
/**
|
||||
* Manage all notification form.
|
||||
*
|
||||
* @type { Record<string, any> }
|
||||
*/
|
||||
const NotificationFormList = {
|
||||
|
@ -63,7 +73,9 @@ const NotificationFormList = {
|
|||
"AliyunSMS": AliyunSMS,
|
||||
"apprise": Apprise,
|
||||
"Bark": Bark,
|
||||
"Bitrix24": Bitrix24,
|
||||
"clicksendsms": ClickSendSMS,
|
||||
"CallMeBot": CallMeBot,
|
||||
"smsc": SMSC,
|
||||
"DingDing": DingDing,
|
||||
"discord": Discord,
|
||||
|
@ -72,7 +84,10 @@ const NotificationFormList = {
|
|||
"GoogleChat": GoogleChat,
|
||||
"gorush": Gorush,
|
||||
"gotify": Gotify,
|
||||
"GrafanaOncall": GrafanaOncall,
|
||||
"HomeAssistant": HomeAssistant,
|
||||
"HeiiOnCall": HeiiOnCall,
|
||||
"Keep": Keep,
|
||||
"Kook": Kook,
|
||||
"line": Line,
|
||||
"LineNotify": LineNotify,
|
||||
|
@ -97,6 +112,7 @@ const NotificationFormList = {
|
|||
"serwersms": SerwerSMS,
|
||||
"signal": Signal,
|
||||
"SMSManager": SMSManager,
|
||||
"SMSPartner": SMSPartner,
|
||||
"slack": Slack,
|
||||
"squadcast": Squadcast,
|
||||
"SMSEagle": SMSEagle,
|
||||
|
@ -104,13 +120,18 @@ const NotificationFormList = {
|
|||
"stackfield": Stackfield,
|
||||
"teams": Teams,
|
||||
"telegram": Telegram,
|
||||
"threema": Threema,
|
||||
"twilio": Twilio,
|
||||
"Splunk": Splunk,
|
||||
"webhook": Webhook,
|
||||
"WeCom": WeCom,
|
||||
"GoAlert": GoAlert,
|
||||
"ServerChan": ServerChan,
|
||||
"ZohoCliq": ZohoCliq
|
||||
"ZohoCliq": ZohoCliq,
|
||||
"SevenIO": SevenIO,
|
||||
"whapi": Whapi,
|
||||
"gtxmessaging": GtxMessaging,
|
||||
"Cellsynt": Cellsynt,
|
||||
};
|
||||
|
||||
export default NotificationFormList;
|
||||
|
|
|
@ -82,8 +82,6 @@
|
|||
<script>
|
||||
import APIKeyDialog from "../../components/APIKeyDialog.vue";
|
||||
import Confirm from "../Confirm.vue";
|
||||
import { useToast } from "vue-toastification";
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -109,6 +107,7 @@ export default {
|
|||
/**
|
||||
* Show dialog to confirm deletion
|
||||
* @param {number} keyID ID of monitor that is being deleted
|
||||
* @returns {void}
|
||||
*/
|
||||
deleteDialog(keyID) {
|
||||
this.selectedKeyID = keyID;
|
||||
|
@ -117,19 +116,18 @@ export default {
|
|||
|
||||
/**
|
||||
* Delete a key
|
||||
* @returns {void}
|
||||
*/
|
||||
deleteKey() {
|
||||
this.$root.deleteAPIKey(this.selectedKeyID, (res) => {
|
||||
if (res.ok) {
|
||||
toast.success(res.msg);
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
this.$root.toastRes(res);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show dialog to confirm pause
|
||||
* @param {number} keyID ID of key to pause
|
||||
* @returns {void}
|
||||
*/
|
||||
disableDialog(keyID) {
|
||||
this.selectedKeyID = keyID;
|
||||
|
@ -137,7 +135,8 @@ export default {
|
|||
},
|
||||
|
||||
/**
|
||||
* Pause maintenance
|
||||
* Pause API key
|
||||
* @returns {void}
|
||||
*/
|
||||
disableKey() {
|
||||
this.$root
|
||||
|
@ -148,7 +147,9 @@ export default {
|
|||
},
|
||||
|
||||
/**
|
||||
* Resume maintenance
|
||||
* Resume API key
|
||||
* @param {number} id Key to resume
|
||||
* @returns {void}
|
||||
*/
|
||||
enableKey(id) {
|
||||
this.$root.getSocket().emit("enableAPIKey", id, (res) => {
|
||||
|
|
|
@ -1,228 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="my-4">
|
||||
<div class="alert alert-warning" role="alert" style="border-radius: 15px;">
|
||||
{{ $t("backupOutdatedWarning") }}<br />
|
||||
<br />
|
||||
{{ $t("backupRecommend") }}
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4 mb-2">{{ $t("Export Backup") }}</h4>
|
||||
|
||||
<p>
|
||||
{{ $t("backupDescription") }} <br />
|
||||
({{ $t("backupDescription2") }}) <br />
|
||||
</p>
|
||||
|
||||
<div class="mb-2">
|
||||
<button class="btn btn-primary" @click="downloadBackup">
|
||||
{{ $t("Export") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<strong>{{ $t("backupDescription3") }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="my-4">
|
||||
<h4 class="mt-4 mb-2">{{ $t("Import Backup") }}</h4>
|
||||
|
||||
<label class="form-label">{{ $t("Options") }}:</label>
|
||||
<br />
|
||||
<div class="form-check form-check-inline">
|
||||
<input
|
||||
id="radioKeep"
|
||||
v-model="importHandle"
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="radioImportHandle"
|
||||
value="keep"
|
||||
/>
|
||||
<label class="form-check-label" for="radioKeep">
|
||||
{{ $t("Keep both") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input
|
||||
id="radioSkip"
|
||||
v-model="importHandle"
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="radioImportHandle"
|
||||
value="skip"
|
||||
/>
|
||||
<label class="form-check-label" for="radioSkip">
|
||||
{{ $t("Skip existing") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input
|
||||
id="radioOverwrite"
|
||||
v-model="importHandle"
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="radioImportHandle"
|
||||
value="overwrite"
|
||||
/>
|
||||
<label class="form-check-label" for="radioOverwrite">
|
||||
{{ $t("Overwrite") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text mb-2">
|
||||
{{ $t("importHandleDescription") }}
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<input
|
||||
id="import-backend"
|
||||
type="file"
|
||||
class="form-control"
|
||||
accept="application/json"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="input-group mb-2 justify-content-end">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-primary"
|
||||
:disabled="processing"
|
||||
@click="confirmImport"
|
||||
>
|
||||
<div
|
||||
v-if="processing"
|
||||
class="spinner-border spinner-border-sm me-1"
|
||||
></div>
|
||||
{{ $t("Import") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="importAlert"
|
||||
class="alert alert-danger mt-3"
|
||||
style="padding: 6px 16px;"
|
||||
>
|
||||
{{ importAlert }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Confirm
|
||||
ref="confirmImport"
|
||||
btn-style="btn-danger"
|
||||
:yes-text="$t('Yes')"
|
||||
:no-text="$t('No')"
|
||||
@yes="importBackup"
|
||||
>
|
||||
{{ $t("confirmImportMsg") }}
|
||||
</Confirm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Confirm from "../../components/Confirm.vue";
|
||||
import dayjs from "dayjs";
|
||||
import { useToast } from "vue-toastification";
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Confirm,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
processing: false,
|
||||
importHandle: "skip",
|
||||
importAlert: null,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Show the confimation dialog confirming the configuration
|
||||
* be imported
|
||||
*/
|
||||
confirmImport() {
|
||||
this.$refs.confirmImport.show();
|
||||
},
|
||||
|
||||
/** Download a backup of the configuration */
|
||||
downloadBackup() {
|
||||
let time = dayjs().format("YYYY_MM_DD-hh_mm_ss");
|
||||
let fileName = `Uptime_Kuma_Backup_${time}.json`;
|
||||
let monitorList = Object.values(this.$root.monitorList);
|
||||
let exportData = {
|
||||
version: this.$root.info.version,
|
||||
notificationList: this.$root.notificationList,
|
||||
monitorList: monitorList,
|
||||
};
|
||||
exportData = JSON.stringify(exportData, null, 4);
|
||||
let downloadItem = document.createElement("a");
|
||||
downloadItem.setAttribute(
|
||||
"href",
|
||||
"data:application/json;charset=utf-8," +
|
||||
encodeURIComponent(exportData)
|
||||
);
|
||||
downloadItem.setAttribute("download", fileName);
|
||||
downloadItem.click();
|
||||
},
|
||||
|
||||
/**
|
||||
* Import the specified backup file
|
||||
* @returns {?string}
|
||||
*/
|
||||
importBackup() {
|
||||
this.processing = true;
|
||||
let uploadItem = document.getElementById("import-backend").files;
|
||||
|
||||
if (uploadItem.length <= 0) {
|
||||
this.processing = false;
|
||||
return (this.importAlert = this.$t("alertNoFile"));
|
||||
}
|
||||
|
||||
if (uploadItem.item(0).type !== "application/json") {
|
||||
this.processing = false;
|
||||
return (this.importAlert = this.$t("alertWrongFileType"));
|
||||
}
|
||||
|
||||
let fileReader = new FileReader();
|
||||
fileReader.readAsText(uploadItem.item(0));
|
||||
|
||||
fileReader.onload = (item) => {
|
||||
this.$root.uploadBackup(
|
||||
item.target.result,
|
||||
this.importHandle,
|
||||
(res) => {
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok) {
|
||||
toast.success(res.msg);
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../assets/vars.scss";
|
||||
|
||||
.dark {
|
||||
#import-backend {
|
||||
&::file-selector-button {
|
||||
color: $primary;
|
||||
background-color: $dark-bg;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled):not([readonly])::file-selector-button {
|
||||
color: $dark-font-color2;
|
||||
background-color: $primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -187,46 +187,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DNS Cache -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label">
|
||||
{{ $t("Enable DNS Cache") }}
|
||||
<div class="form-text">
|
||||
⚠️ {{ $t("dnsCacheDescription") }}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div class="form-check">
|
||||
<input
|
||||
id="dnsCacheEnable"
|
||||
v-model="settings.dnsCache"
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="dnsCache"
|
||||
:value="true"
|
||||
required
|
||||
/>
|
||||
<label class="form-check-label" for="dnsCacheEnable">
|
||||
{{ $t("Enable") }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input
|
||||
id="dnsCacheDisable"
|
||||
v-model="settings.dnsCache"
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="dnsCache"
|
||||
:value="false"
|
||||
required
|
||||
/>
|
||||
<label class="form-check-label" for="dnsCacheDisable">
|
||||
{{ $t("Disable") }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chrome Executable -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="primaryBaseURL">
|
||||
|
@ -293,16 +253,25 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
/** Save the settings */
|
||||
/**
|
||||
* Save the settings
|
||||
* @returns {void}
|
||||
*/
|
||||
saveGeneral() {
|
||||
localStorage.timezone = this.$root.userTimezone;
|
||||
this.saveSettings();
|
||||
},
|
||||
/** Get the base URL of the application */
|
||||
/**
|
||||
* Get the base URL of the application
|
||||
* @returns {void}
|
||||
*/
|
||||
autoGetPrimaryBaseURL() {
|
||||
this.settings.primaryBaseURL = location.protocol + "//" + location.host;
|
||||
},
|
||||
|
||||
/**
|
||||
* Test the chrome executable
|
||||
* @returns {void}
|
||||
*/
|
||||
testChrome() {
|
||||
this.$root.getSocket().emit("testChrome", this.settings.chromeExecutable, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
</button>
|
||||
</div>
|
||||
<div class="my-4">
|
||||
<div class="my-3">
|
||||
<div v-if="$root.info.dbType === 'sqlite'" class="my-3">
|
||||
<button class="btn btn-outline-info me-2" @click="shrinkDatabase">
|
||||
{{ $t("Shrink Database") }} ({{ databaseSizeDisplay }})
|
||||
</button>
|
||||
|
@ -57,9 +57,6 @@
|
|||
<script>
|
||||
import Confirm from "../../components/Confirm.vue";
|
||||
import { log } from "../../util.ts";
|
||||
import { useToast } from "vue-toastification";
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -94,7 +91,10 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
/** Get the current size of the database */
|
||||
/**
|
||||
* Get the current size of the database
|
||||
* @returns {void}
|
||||
*/
|
||||
loadDatabaseSize() {
|
||||
log.debug("monitorhistory", "load database size");
|
||||
this.$root.getSocket().emit("getDatabaseSize", (res) => {
|
||||
|
@ -107,30 +107,39 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
/** Request that the database is shrunk */
|
||||
/**
|
||||
* Request that the database is shrunk
|
||||
* @returns {void}
|
||||
*/
|
||||
shrinkDatabase() {
|
||||
this.$root.getSocket().emit("shrinkDatabase", (res) => {
|
||||
if (res.ok) {
|
||||
this.loadDatabaseSize();
|
||||
toast.success("Done");
|
||||
this.$root.toastSuccess("Done");
|
||||
} else {
|
||||
log.debug("monitorhistory", res);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/** Show the dialog to confirm clearing stats */
|
||||
/**
|
||||
* Show the dialog to confirm clearing stats
|
||||
* @returns {void}
|
||||
*/
|
||||
confirmClearStatistics() {
|
||||
this.$refs.confirmClearStatistics.show();
|
||||
},
|
||||
|
||||
/** Send the request to clear stats */
|
||||
/**
|
||||
* Send the request to clear stats
|
||||
* @returns {void}
|
||||
*/
|
||||
clearStatistics() {
|
||||
this.$root.clearStatistics((res) => {
|
||||
if (res.ok) {
|
||||
this.$router.go();
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
this.$root.toastError(res.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -20,6 +20,39 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div class="my-4 pt-4">
|
||||
<h5 class="my-4 settings-subheading">{{ $t("monitorToastMessagesLabel") }}</h5>
|
||||
<p>{{ $t("monitorToastMessagesDescription") }}</p>
|
||||
|
||||
<div class="my-4">
|
||||
<label for="toastErrorTimeoutSecs" class="form-label">
|
||||
{{ $t("toastErrorTimeout") }}
|
||||
</label>
|
||||
<input
|
||||
id="toastErrorTimeoutSecs"
|
||||
v-model="toastErrorTimeoutSecs"
|
||||
type="number"
|
||||
class="form-control"
|
||||
min="-1"
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="my-4">
|
||||
<label for="toastSuccessTimeoutSecs" class="form-label">
|
||||
{{ $t("toastSuccessTimeout") }}
|
||||
</label>
|
||||
<input
|
||||
id="toastSuccessTimeoutSecs"
|
||||
v-model="toastSuccessTimeoutSecs"
|
||||
type="number"
|
||||
class="form-control"
|
||||
min="-1"
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-4 pt-4">
|
||||
<h5 class="my-4 settings-subheading">{{ $t("settingsCertificateExpiry") }}</h5>
|
||||
<p>{{ $t("certificationExpiryDescription") }}</p>
|
||||
|
@ -58,6 +91,8 @@ export default {
|
|||
|
||||
data() {
|
||||
return {
|
||||
toastSuccessTimeoutSecs: 20,
|
||||
toastErrorTimeoutSecs: -1,
|
||||
/**
|
||||
* Variable to store the input for new certificate expiry day.
|
||||
*/
|
||||
|
@ -77,10 +112,31 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
// Parse, store and apply new timeout settings.
|
||||
toastSuccessTimeoutSecs(newTimeout) {
|
||||
const parsedTimeout = parseInt(newTimeout);
|
||||
if (parsedTimeout != null && !Number.isNaN(parsedTimeout)) {
|
||||
localStorage.toastSuccessTimeout = newTimeout > 0 ? newTimeout * 1000 : newTimeout;
|
||||
}
|
||||
},
|
||||
toastErrorTimeoutSecs(newTimeout) {
|
||||
const parsedTimeout = parseInt(newTimeout);
|
||||
if (parsedTimeout != null && !Number.isNaN(parsedTimeout)) {
|
||||
localStorage.toastErrorTimeout = newTimeout > 0 ? newTimeout * 1000 : newTimeout;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.loadToastTimeoutSettings();
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Remove a day from expiry notification days.
|
||||
* @param {number} day The day to remove.
|
||||
* @returns {void}
|
||||
*/
|
||||
removeExpiryNotifDay(day) {
|
||||
this.settings.tlsExpiryNotifyDays = this.settings.tlsExpiryNotifyDays.filter(d => d !== day);
|
||||
|
@ -93,6 +149,7 @@ export default {
|
|||
* - day is > 0.
|
||||
* - The day is not already in the list.
|
||||
* @param {number} day The day number to add.
|
||||
* @returns {void}
|
||||
*/
|
||||
addExpiryNotifDay(day) {
|
||||
if (day != null && day !== "") {
|
||||
|
@ -106,6 +163,28 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads toast timeout settings from storage to component data.
|
||||
* @returns {void}
|
||||
*/
|
||||
loadToastTimeoutSettings() {
|
||||
const successTimeout = localStorage.toastSuccessTimeout;
|
||||
if (successTimeout !== undefined) {
|
||||
const parsedTimeout = parseInt(successTimeout);
|
||||
if (parsedTimeout != null && !Number.isNaN(parsedTimeout)) {
|
||||
this.toastSuccessTimeoutSecs = parsedTimeout > 0 ? parsedTimeout / 1000 : parsedTimeout;
|
||||
}
|
||||
}
|
||||
|
||||
const errorTimeout = localStorage.toastErrorTimeout;
|
||||
if (errorTimeout !== undefined) {
|
||||
const parsedTimeout = parseInt(errorTimeout);
|
||||
if (parsedTimeout != null && !Number.isNaN(parsedTimeout)) {
|
||||
this.toastErrorTimeoutSecs = parsedTimeout > 0 ? parsedTimeout / 1000 : parsedTimeout;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
53
src/components/settings/RemoteBrowsers.vue
Normal file
53
src/components/settings/RemoteBrowsers.vue
Normal file
|
@ -0,0 +1,53 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="dockerHost-list my-4">
|
||||
<p v-if="$root.remoteBrowserList.length === 0">
|
||||
{{ $t("Not available, please setup.") }}
|
||||
</p>
|
||||
|
||||
<ul class="list-group mb-3" style="border-radius: 1rem;">
|
||||
<li v-for="(remoteBrowser, index) in $root.remoteBrowserList" :key="index" class="list-group-item">
|
||||
{{ remoteBrowser.name }}<br>
|
||||
<a href="#" @click="$refs.remoteBrowserDialog.show(remoteBrowser.id)">{{ $t("Edit") }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button class="btn btn-primary me-2" type="button" @click="$refs.remoteBrowserDialog.show()">
|
||||
<font-awesome-icon icon="plus" /> {{ $t("Add Remote Browser") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="my-4 pt-4">
|
||||
<h5 class="my-4 settings-subheading">{{ $t("What is a Remote Browser?") }}</h5>
|
||||
<p>{{ $t("remoteBrowsersDescription") }} <a href="https://hub.docker.com/r/browserless/chrome">{{ $t("self-hosted container") }}</a></p>
|
||||
</div>
|
||||
|
||||
<RemoteBrowserDialog ref="remoteBrowserDialog" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RemoteBrowserDialog from "../../components/RemoteBrowserDialog.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RemoteBrowserDialog,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
|
||||
computed: {
|
||||
settings() {
|
||||
return this.$parent.$parent.$parent.settings;
|
||||
},
|
||||
saveSettings() {
|
||||
return this.$parent.$parent.$parent.saveSettings;
|
||||
},
|
||||
settingsLoaded() {
|
||||
return this.$parent.$parent.$parent.settingsLoaded;
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -175,17 +175,26 @@ export default {
|
|||
this.$root.getSocket().emit(prefix + "leave");
|
||||
},
|
||||
methods: {
|
||||
/** Start the Cloudflare tunnel */
|
||||
/**
|
||||
* Start the Cloudflare tunnel
|
||||
* @returns {void}
|
||||
*/
|
||||
start() {
|
||||
this.$root.getSocket().emit(prefix + "start", this.cloudflareTunnelToken);
|
||||
},
|
||||
/** Stop the Cloudflare tunnel */
|
||||
/**
|
||||
* Stop the Cloudflare tunnel
|
||||
* @returns {void}
|
||||
*/
|
||||
stop() {
|
||||
this.$root.getSocket().emit(prefix + "stop", this.currentPassword, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
});
|
||||
},
|
||||
/** Remove the token for the Cloudflare tunnel */
|
||||
/**
|
||||
* Remove the token for the Cloudflare tunnel
|
||||
* @returns {void}
|
||||
*/
|
||||
removeToken() {
|
||||
this.$root.getSocket().emit(prefix + "removeToken");
|
||||
this.cloudflareTunnelToken = "";
|
||||
|
|
|
@ -93,10 +93,16 @@
|
|||
<TwoFADialog ref="TwoFADialog" />
|
||||
|
||||
<Confirm ref="confirmDisableAuth" btn-style="btn-danger" :yes-text="$t('I understand, please disable')" :no-text="$t('Leave')" @yes="disableAuth">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<p v-html="$t('disableauth.message1')"></p>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<p v-html="$t('disableauth.message2')"></p>
|
||||
<i18n-t tag="p" keypath="disableauth.message1">
|
||||
<template #disableAuth>
|
||||
<strong>{{ $t('disable authentication') }}</strong>
|
||||
</template>
|
||||
</i18n-t>
|
||||
<i18n-t tag="p" keypath="disableauth.message2">
|
||||
<template #intendThirdPartyAuth>
|
||||
<strong>{{ $t('intend to implement third-party authentication') }}</strong>
|
||||
</template>
|
||||
</i18n-t>
|
||||
<p>{{ $t("Please use this option carefully!") }}</p>
|
||||
|
||||
<div class="mb-3">
|
||||
|
@ -155,7 +161,10 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
/** Check new passwords match before saving them */
|
||||
/**
|
||||
* Check new passwords match before saving them
|
||||
* @returns {void}
|
||||
*/
|
||||
savePassword() {
|
||||
if (this.password.newPassword !== this.password.repeatNewPassword) {
|
||||
this.invalidPassword = true;
|
||||
|
@ -168,12 +177,21 @@ export default {
|
|||
this.password.currentPassword = "";
|
||||
this.password.newPassword = "";
|
||||
this.password.repeatNewPassword = "";
|
||||
|
||||
// Update token of the current session
|
||||
if (res.token) {
|
||||
this.$root.storage().token = res.token;
|
||||
this.$root.socket.token = res.token;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/** Disable authentication for web app access */
|
||||
/**
|
||||
* Disable authentication for web app access
|
||||
* @returns {void}
|
||||
*/
|
||||
disableAuth() {
|
||||
this.settings.disableAuth = true;
|
||||
|
||||
|
@ -186,7 +204,10 @@ export default {
|
|||
}, this.password.currentPassword);
|
||||
},
|
||||
|
||||
/** Enable authentication for web app access */
|
||||
/**
|
||||
* Enable authentication for web app access
|
||||
* @returns {void}
|
||||
*/
|
||||
enableAuth() {
|
||||
this.settings.disableAuth = false;
|
||||
this.saveSettings();
|
||||
|
@ -194,7 +215,10 @@ export default {
|
|||
location.reload();
|
||||
},
|
||||
|
||||
/** Show confirmation dialog for disable auth */
|
||||
/**
|
||||
* Show confirmation dialog for disable auth
|
||||
* @returns {void}
|
||||
*/
|
||||
confirmDisableAuth() {
|
||||
this.$refs.confirmDisableAuth.show();
|
||||
},
|
||||
|
|
|
@ -28,11 +28,9 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { useToast } from "vue-toastification";
|
||||
import TagEditDialog from "../../components/TagEditDialog.vue";
|
||||
import Tag from "../Tag.vue";
|
||||
import Confirm from "../Confirm.vue";
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -86,7 +84,7 @@ export default {
|
|||
if (res.ok) {
|
||||
this.tagsList = res.tags;
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
this.$root.toastError(res.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -138,7 +136,7 @@ export default {
|
|||
/**
|
||||
* Get monitors which has a specific tag locally
|
||||
* @param {number} tagId id of the tag to filter
|
||||
* @returns {Object[]} list of monitors which has a specific tag
|
||||
* @returns {object[]} list of monitors which has a specific tag
|
||||
*/
|
||||
monitorsByTag(tagId) {
|
||||
return Object.values(this.$root.monitorList).filter((monitor) => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue