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:
Louis Lam 2024-06-26 10:00:30 +08:00
commit cc52ee3feb
440 changed files with 26491 additions and 16428 deletions

View file

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

View file

@ -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 = {

View file

@ -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) {

View file

@ -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");

View file

@ -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) {

View file

@ -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: {

View file

@ -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();

View file

@ -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) => {

View file

@ -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;
}

View file

@ -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";
},

View file

@ -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;

View file

@ -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>

View file

@ -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>

View file

@ -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)) {

View file

@ -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

View file

@ -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) {

View file

@ -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] }}&nbsp;
</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 {

View file

@ -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) => {

View file

@ -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) {

View 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>

View 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>

View file

@ -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: {

View file

@ -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) {

View file

@ -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;

View file

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

View file

@ -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")}`;
}
},

View file

@ -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>

View 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>

View 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>

View 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>

View file

@ -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>

View file

@ -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>

View 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>

View 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>

View 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>

View 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>

View file

@ -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>

View 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>

View file

@ -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>

View 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>

View file

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

View 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>

View file

@ -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 }}"
}`;
}`
]);
}
},
};

View 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>

View file

@ -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;

View file

@ -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) => {

View file

@ -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>

View file

@ -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);

View file

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

View file

@ -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>

View 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>

View file

@ -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 = "";

View file

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

View file

@ -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) => {