Merge remote-tracking branch 'origin/master' into miles/invert-keyword

# Conflicts:
#	server/database.js
This commit is contained in:
Louis Lam
2023-07-08 16:19:44 +08:00
114 changed files with 6042 additions and 3755 deletions

View File

@@ -1,14 +1,15 @@
<template>
<transition name="slide-fade" appear>
<div v-if="monitor">
<router-link v-if="group !== ''" :to="monitorURL(monitor.parent)"> {{ group }}</router-link>
<h1> {{ monitor.name }}</h1>
<p v-if="monitor.description">{{ monitor.description }}</p>
<div class="tags">
<Tag v-for="tag in monitor.tags" :key="tag.id" :item="tag" :size="'sm'" />
</div>
<p class="url">
<a v-if="monitor.type === 'http' || monitor.type === 'keyword' " :href="monitor.url" target="_blank" rel="noopener noreferrer">{{ monitor.url }}</a>
<span v-if="monitor.type === 'port'">TCP Ping {{ monitor.hostname }}:{{ monitor.port }}</span>
<a v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'mp-health' " :href="monitor.url" target="_blank" rel="noopener noreferrer">{{ filterPassword(monitor.url) }}</a>
<span v-if="monitor.type === 'port'">TCP Port {{ monitor.hostname }}:{{ monitor.port }}</span>
<span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span>
<span v-if="monitor.type === 'keyword'">
<br>
@@ -20,6 +21,21 @@
<br>
<span>{{ $t("Last Result") }}:</span> <span class="keyword">{{ monitor.dns_last_result }}</span>
</span>
<span v-if="monitor.type === 'docker'">Docker container: {{ monitor.docker_container }}</span>
<span v-if="monitor.type === 'gamedig'">Gamedig - {{ monitor.game }}: {{ monitor.hostname }}:{{ monitor.port }}</span>
<span v-if="monitor.type === 'grpc-keyword'">gRPC - {{ filterPassword(monitor.grpcUrl) }}
<br>
<span>{{ $t("Keyword") }}:</span> <span class="keyword">{{ monitor.keyword }}</span>
</span>
<span v-if="monitor.type === 'mongodb'">{{ filterPassword(monitor.databaseConnectionString) }}</span>
<span v-if="monitor.type === 'mqtt'">MQTT: {{ monitor.hostname }}:{{ monitor.port }}/{{ monitor.mqttTopic }}</span>
<span v-if="monitor.type === 'mysql'">{{ filterPassword(monitor.databaseConnectionString) }}</span>
<span v-if="monitor.type === 'postgres'">{{ filterPassword(monitor.databaseConnectionString) }}</span>
<span v-if="monitor.type === 'push'">Push: <a :href="pushURL" target="_blank" rel="noopener noreferrer">{{ pushURL }}</a></span>
<span v-if="monitor.type === 'radius'">Radius: {{ filterPassword(monitor.hostname) }}</span>
<span v-if="monitor.type === 'redis'">{{ filterPassword(monitor.databaseConnectionString) }}</span>
<span v-if="monitor.type === 'sqlserver'">SQL Server: {{ filterPassword(monitor.databaseConnectionString) }}</span>
<span v-if="monitor.type === 'steam'">Steam Game Server: {{ monitor.hostname }}:{{ monitor.port }}</span>
</p>
<div class="functions">
@@ -27,7 +43,7 @@
<button v-if="monitor.active" class="btn btn-normal" @click="pauseDialog">
<font-awesome-icon icon="pause" /> {{ $t("Pause") }}
</button>
<button v-if="! monitor.active" class="btn btn-primary" @click="resumeMonitor">
<button v-if="! monitor.active" class="btn btn-primary" :disabled="monitor.forceInactive" @click="resumeMonitor">
<font-awesome-icon icon="play" /> {{ $t("Resume") }}
</button>
<router-link :to=" '/edit/' + monitor.id " class="btn btn-normal">
@@ -54,37 +70,44 @@
</div>
</div>
<!-- Stats -->
<div class="shadow-box big-padding text-center stats">
<div class="row">
<div class="col">
<h4>{{ pingTitle() }}</h4>
<p>({{ $t("Current") }})</p>
<span class="num">
<div v-if="monitor.type !== 'group'" class="col-12 col-sm col row d-flex align-items-center d-sm-block">
<h4 class="col-4 col-sm-12">{{ pingTitle() }}</h4>
<p class="col-4 col-sm-12 mb-0 mb-sm-2">({{ $t("Current") }})</p>
<span class="col-4 col-sm-12 num">
<a href="#" @click.prevent="showPingChartBox = !showPingChartBox">
<CountUp :value="ping" />
</a>
</span>
</div>
<div class="col">
<h4>{{ pingTitle(true) }}</h4>
<p>(24{{ $t("-hour") }})</p>
<span class="num"><CountUp :value="avgPing" /></span>
<div v-if="monitor.type !== 'group'" class="col-12 col-sm col row d-flex align-items-center d-sm-block">
<h4 class="col-4 col-sm-12">{{ pingTitle(true) }}</h4>
<p class="col-4 col-sm-12 mb-0 mb-sm-2">(24{{ $t("-hour") }})</p>
<span class="col-4 col-sm-12 num">
<CountUp :value="avgPing" />
</span>
</div>
<div class="col">
<h4>{{ $t("Uptime") }}</h4>
<p>(24{{ $t("-hour") }})</p>
<span class="num"><Uptime :monitor="monitor" type="24" /></span>
<div class="col-12 col-sm col row d-flex align-items-center d-sm-block">
<h4 class="col-4 col-sm-12">{{ $t("Uptime") }}</h4>
<p class="col-4 col-sm-12 mb-0 mb-sm-2">(24{{ $t("-hour") }})</p>
<span class="col-4 col-sm-12 num">
<Uptime :monitor="monitor" type="24" />
</span>
</div>
<div class="col">
<h4>{{ $t("Uptime") }}</h4>
<p>(30{{ $t("-day") }})</p>
<span class="num"><Uptime :monitor="monitor" type="720" /></span>
<div class="col-12 col-sm col row d-flex align-items-center d-sm-block">
<h4 class="col-4 col-sm-12">{{ $t("Uptime") }}</h4>
<p class="col-4 col-sm-12 mb-0 mb-sm-2">(30{{ $t("-day") }})</p>
<span class="col-4 col-sm-12 num">
<Uptime :monitor="monitor" type="720" />
</span>
</div>
<div v-if="tlsInfo" class="col">
<h4>{{ $t("Cert Exp.") }}</h4>
<p>(<Datetime :value="tlsInfo.certInfo.validTo" date-only />)</p>
<span class="num">
<div v-if="tlsInfo" class="col-12 col-sm col row d-flex align-items-center d-sm-block">
<h4 class="col-4 col-sm-12">{{ $t("Cert Exp.") }}</h4>
<p class="col-4 col-sm-12 mb-0 mb-sm-2">(<Datetime :value="tlsInfo.certInfo.validTo" date-only />)</p>
<span class="col-4 col-sm-12 num">
<a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{ tlsInfo.certInfo.daysRemaining }} {{ $tc("day", tlsInfo.certInfo.daysRemaining) }}</a>
</span>
</div>
@@ -111,6 +134,15 @@
</div>
</div>
<!-- Screenshot -->
<div v-if="monitor.type === 'real-browser'" class="shadow-box">
<div class="row">
<div class="col-md-6">
<img :src="screenshotURL" alt style="width: 100%;">
</div>
</div>
</div>
<div class="shadow-box table-shadow-box">
<div class="dropdown dropdown-clear-data">
<button class="btn btn-sm btn-outline-danger dropdown-toggle" type="button" data-bs-toggle="dropdown">
@@ -138,7 +170,7 @@
</tr>
</thead>
<tbody>
<tr v-for="(beat, index) in displayedRecords" :key="index" :class="{ 'shadow-box': $root.windowWidth <= 550}" style="padding: 10px;">
<tr v-for="(beat, index) in displayedRecords" :key="index" style="padding: 10px;">
<td><Status :status="beat.status" /></td>
<td :class="{ 'border-0':! beat.msg}"><Datetime :value="beat.time" /></td>
<td class="border-0">{{ beat.msg }}</td>
@@ -195,6 +227,9 @@ import Pagination from "v-pagination-3";
const PingChart = defineAsyncComponent(() => import("../components/PingChart.vue"));
import Tag from "../components/Tag.vue";
import CertificateInfo from "../components/CertificateInfo.vue";
import { getMonitorRelativeURL } from "../util.ts";
import { URL } from "whatwg-url";
import { getResBaseURL } from "../util-frontend";
export default {
components: {
@@ -220,6 +255,7 @@ export default {
hideCount: true,
chunksNavigation: "scroll",
},
cacheTime: Date.now(),
};
},
computed: {
@@ -229,6 +265,10 @@ export default {
},
lastHeartBeat() {
// Also trigger screenshot refresh here
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.cacheTime = Date.now();
if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) {
return this.$root.lastHeartbeatList[this.monitor.id];
}
@@ -292,11 +332,27 @@ export default {
const endIndex = startIndex + this.perPage;
return this.heartBeatList.slice(startIndex, endIndex);
},
group() {
if (!this.monitor.pathName.includes("/")) {
return "";
}
return this.monitor.pathName.substr(0, this.monitor.pathName.lastIndexOf("/"));
},
pushURL() {
return this.$root.baseURL + "/api/push/" + this.monitor.pushToken + "?status=up&msg=OK&ping=";
},
screenshotURL() {
return getResBaseURL() + this.monitor.screenshot + "?time=" + this.cacheTime;
}
},
mounted() {
},
methods: {
getResBaseURL,
/** Request a test notification be sent for this monitor */
testNotification() {
this.$root.getSocket().emit("testNotification", this.monitor.id);
@@ -378,12 +434,35 @@ export default {
translationPrefix = "Avg. ";
}
if (this.monitor.type === "http") {
if (this.monitor.type === "http" || this.monitor.type === "keyword") {
return this.$t(translationPrefix + "Response");
}
return this.$t(translationPrefix + "Ping");
},
/**
* Get URL of monitor
* @param {number} id ID of monitor
* @returns {string} Relative URL of monitor
*/
monitorURL(id) {
return getMonitorRelativeURL(id);
},
/** Filter and hide password in URL for display */
filterPassword(urlString) {
try {
let parsedUrl = new URL(urlString);
if (parsedUrl.password !== "") {
parsedUrl.password = "******";
}
return parsedUrl.toString();
} catch (e) {
// Handle SQL Server
return urlString.replaceAll(/Password=(.+);/ig, "Password=******;");
}
}
},
};
</script>
@@ -417,6 +496,7 @@ export default {
flex-direction: column;
align-items: center;
padding-top: 10px;
font-size: 0.9em;
}
a.btn {
@@ -473,6 +553,18 @@ table {
}
}
@media (max-width: 550px) {
.stats {
.col {
margin: 10px 0 !important;
}
h4 {
font-size: 1.1rem;
}
}
}
.keyword {
color: black;
}

View File

@@ -36,7 +36,7 @@
v-model="affectedMonitors"
:options="affectedMonitorsOptions"
track-by="id"
label="name"
label="pathName"
:multiple="true"
:close-on-select="false"
:clear-on-select="false"
@@ -203,8 +203,8 @@
<label for="timezone" class="form-label">
{{ $t("Timezone") }}
</label>
<select id="timezone" v-model="maintenance.timezone" class="form-select">
<option :value="null">{{ $t("sameAsServerTimezone") }}</option>
<select id="timezone" v-model="maintenance.timezoneOption" class="form-select">
<option value="SAME_AS_SERVER">{{ $t("sameAsServerTimezone") }}</option>
<option value="UTC">UTC</option>
<option
v-for="(timezone, index) in timezoneList"
@@ -218,17 +218,17 @@
<!-- Date Range -->
<div class="my-3">
<label class="form-label">{{ $t("Effective Date Range") }}</label>
<label v-if="maintenance.strategy !== 'single'" class="form-label">{{ $t("Effective Date Range") }}</label>
<div class="row">
<div class="col">
<div class="mb-2">{{ $t("startDateTime") }}</div>
<input v-model="maintenance.dateRange[0]" type="datetime-local" class="form-control">
<input v-model="maintenance.dateRange[0]" type="datetime-local" class="form-control" :required="maintenance.strategy === 'single'">
</div>
<div class="col">
<div class="mb-2">{{ $t("endDateTime") }}</div>
<input v-model="maintenance.dateRange[1]" type="datetime-local" class="form-control">
<input v-model="maintenance.dateRange[1]" type="datetime-local" class="form-control" :required="maintenance.strategy === 'single'">
</div>
</div>
</div>
@@ -248,7 +248,6 @@
<script>
import { useToast } from "vue-toastification";
import VueMultiselect from "vue-multiselect";
import dayjs from "dayjs";
import Datepicker from "@vuepic/vue-datepicker";
import { timezoneList } from "../util-frontend";
import cronstrue from "cronstrue/i18n";
@@ -272,7 +271,6 @@ export default {
selectedStatusPages: [],
dark: (this.$root.theme === "dark"),
neverEnd: false,
minDate: this.$root.date(dayjs()) + " 00:00",
lastDays: [
{
langKey: "lastDay1",
@@ -383,17 +381,39 @@ export default {
},
},
mounted() {
this.init();
this.$root.getMonitorList((res) => {
if (res.ok) {
Object.values(this.$root.monitorList).map(monitor => {
Object.values(this.$root.monitorList).sort((m1, m2) => {
if (m1.active !== m2.active) {
if (m1.active === 0) {
return 1;
}
if (m2.active === 0) {
return -1;
}
}
if (m1.weight !== m2.weight) {
if (m1.weight > m2.weight) {
return -1;
}
if (m1.weight < m2.weight) {
return 1;
}
}
return m1.pathName.localeCompare(m2.pathName);
}).map(monitor => {
this.affectedMonitorsOptions.push({
id: monitor.id,
name: monitor.name,
pathName: monitor.pathName,
});
});
}
this.init();
});
},
methods: {
@@ -411,7 +431,7 @@ export default {
cron: "30 3 * * *",
durationMinutes: 60,
intervalDay: 1,
dateRange: [ this.minDate ],
dateRange: [],
timeRange: [{
hours: 2,
minutes: 0,
@@ -421,7 +441,7 @@ export default {
}],
weekdays: [],
daysOfMonth: [],
timezone: null,
timezoneOption: null,
};
} else if (this.isEdit) {
this.$root.getSocket().emit("getMaintenance", this.$route.params.id, (res) => {
@@ -431,7 +451,7 @@ export default {
this.$root.getSocket().emit("getMonitorMaintenance", this.$route.params.id, (res) => {
if (res.ok) {
Object.values(res.monitors).map(monitor => {
this.affectedMonitors.push(monitor);
this.affectedMonitors.push(this.affectedMonitorsOptions.find(item => item.id === monitor.id));
});
} else {
toast.error(res.msg);

View File

@@ -12,6 +12,9 @@
<label for="type" class="form-label">{{ $t("Monitor Type") }}</label>
<select id="type" v-model="monitor.type" class="form-select">
<optgroup :label="$t('General Monitor Type')">
<option value="group">
{{ $t("Group") }}
</option>
<option value="http">
HTTP(s)
</option>
@@ -33,6 +36,10 @@
<option value="docker">
{{ $t("Docker Container") }}
</option>
<option value="real-browser">
HTTP(s) - Browser Engine (Chrome/Chromium) (Beta)
</option>
</optgroup>
<optgroup :label="$t('Passive Monitor Type')">
@@ -70,16 +77,6 @@
Redis
</option>
</optgroup>
<!--
Hidden for now: Reason refer to Setting.vue
<optgroup :label="$t('Custom Monitor Type')">
<option value="browser">
(Beta) HTTP(s) - Browser Engine (Chrome/Firefox)
</option>
</optgroup>
</select>
-->
</select>
</div>
@@ -89,8 +86,18 @@
<input id="name" v-model="monitor.name" type="text" class="form-control" required>
</div>
<!-- Parent Monitor -->
<div class="my-3">
<label for="parent" class="form-label">{{ $t("Monitor Group") }}</label>
<select v-model="monitor.parent" class="form-select" :disabled="sortedMonitorList.length === 0">
<option v-if="sortedMonitorList.length === 0" :value="null" selected>{{ $t("noGroupMonitorMsg") }}</option>
<option v-else :value="null" selected>{{ $t("None") }}</option>
<option v-for="parentMonitor in sortedMonitorList" :key="parentMonitor.id" :value="parentMonitor.id">{{ parentMonitor.pathName }}</option>
</select>
</div>
<!-- URL -->
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'browser' " class="my-3">
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'real-browser' " class="my-3">
<label for="url" class="form-label">{{ $t("URL") }}</label>
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
</div>
@@ -98,7 +105,7 @@
<!-- gRPC URL -->
<div v-if="monitor.type === 'grpc-keyword' " class="my-3">
<label for="grpc-url" class="form-label">{{ $t("URL") }}</label>
<input id="grpc-url" v-model="monitor.grpcUrl" type="url" class="form-control" pattern="[^\:]+:[0-9]{5}" required>
<input id="grpc-url" v-model="monitor.grpcUrl" type="text" class="form-control" required>
</div>
<!-- Push URL -->
@@ -818,6 +825,47 @@ message HealthCheckResponse {
return null;
},
// Filter result by active state, weight and alphabetical
// Only return groups which arent't itself and one of its decendants
sortedMonitorList() {
let result = Object.values(this.$root.monitorList);
// Only groups, not itself, not a decendant
result = result.filter(
monitor => monitor.type === "group" &&
monitor.id !== this.monitor.id &&
!this.monitor.childrenIDs?.includes(monitor.id)
);
// Filter result by active state, weight and alphabetical
result.sort((m1, m2) => {
if (m1.active !== m2.active) {
if (m1.active === 0) {
return 1;
}
if (m2.active === 0) {
return -1;
}
}
if (m1.weight !== m2.weight) {
if (m1.weight > m2.weight) {
return -1;
}
if (m1.weight < m2.weight) {
return 1;
}
}
return m1.pathName.localeCompare(m2.pathName);
});
return result;
},
},
watch: {
"$root.proxyList"() {
@@ -937,6 +985,7 @@ message HealthCheckResponse {
this.monitor = {
type: "http",
name: "",
parent: null,
url: "https://",
method: "GET",
interval: 60,
@@ -991,12 +1040,17 @@ message HealthCheckResponse {
if (this.isClone) {
/*
* Cloning a monitor will include properties that can not be posted to backend
* as they are not valid columns in the SQLite table.
*/
* Cloning a monitor will include properties that can not be posted to backend
* as they are not valid columns in the SQLite table.
*/
this.monitor.id = undefined; // Remove id when cloning as we want a new id
this.monitor.includeSensitiveData = undefined;
this.monitor.maintenance = undefined;
// group monitor fields
this.monitor.childrenIDs = undefined;
this.monitor.forceInactive = undefined;
this.monitor.pathName = undefined;
this.monitor.name = this.$t("cloneOf", [ this.monitor.name ]);
this.$refs.tagsManager.newTags = this.monitor.tags.map((monitorTag) => {
return {

View File

@@ -32,7 +32,7 @@
<ul>
<li>{{ $t("Retype the address.") }}</li>
<li><a href="#" class="go-back" @click="goBack()">{{ $t("Go back to the previous page.") }}</a></li>
<li><a href="/" class="go-back">Go back to home page.</a></li>
<li><a href="/" class="go-back">{{ $t("Go back to home page.") }}</a></li>
</ul>
</div>
</div>

View File

@@ -116,12 +116,6 @@ export default {
backup: {
title: this.$t("Backup"),
},
/*
Hidden for now: Unfortunately, after some test, I found that Playwright requires a lot of libraries to be installed on the Linux host in order to start Chrome or Firefox.
It will be hard to install, so I hide this feature for now. But it still accessible via URL: /settings/plugins.
plugins: {
title: this.$tc("plugin", 2),
},*/
about: {
title: this.$t("About"),
},

View File

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