mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-08-21 06:46:40 +08:00
Merge remote-tracking branch 'origin/master' into skaempfe#2593
# Conflicts: # server/model/monitor.js # src/pages/Details.vue
This commit is contained in:
@@ -2,12 +2,13 @@
|
||||
<transition name="slide-fade" appear>
|
||||
<div v-if="monitor">
|
||||
<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' || monitor.type === 'mp-health' " :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>
|
||||
@@ -17,6 +18,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">
|
||||
@@ -30,6 +46,9 @@
|
||||
<router-link :to=" '/edit/' + monitor.id " class="btn btn-normal">
|
||||
<font-awesome-icon icon="edit" /> {{ $t("Edit") }}
|
||||
</router-link>
|
||||
<router-link :to=" '/clone/' + monitor.id " class="btn btn-normal">
|
||||
<font-awesome-icon icon="clone" /> {{ $t("Clone") }}
|
||||
</router-link>
|
||||
<button class="btn btn-danger" @click="deleteDialog">
|
||||
<font-awesome-icon icon="trash" /> {{ $t("Delete") }}
|
||||
</button>
|
||||
@@ -50,35 +69,41 @@
|
||||
|
||||
<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 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 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>
|
||||
@@ -132,7 +157,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>
|
||||
@@ -189,6 +214,7 @@ 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 { URL } from "whatwg-url";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -286,6 +312,10 @@ export default {
|
||||
const endIndex = startIndex + this.perPage;
|
||||
return this.heartBeatList.slice(startIndex, endIndex);
|
||||
},
|
||||
|
||||
pushURL() {
|
||||
return this.$root.baseURL + "/api/push/" + this.monitor.pushToken + "?status=up&msg=OK&ping=";
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -372,12 +402,26 @@ 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");
|
||||
},
|
||||
|
||||
/** 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>
|
||||
@@ -411,6 +455,7 @@ export default {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 10px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
a.btn {
|
||||
@@ -467,6 +512,18 @@ table {
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
.stats {
|
||||
.col {
|
||||
margin: 10px 0 !important;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.keyword {
|
||||
color: black;
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@
|
||||
<div>
|
||||
<h1 class="mb-3">{{ pageName }}</h1>
|
||||
<form @submit.prevent="submit">
|
||||
<div class="shadow-box">
|
||||
<div class="shadow-box shadow-box-with-fixed-bottom-bar">
|
||||
<div class="row">
|
||||
<div class="col-xl-10">
|
||||
<!-- Title -->
|
||||
@@ -21,6 +21,9 @@
|
||||
<textarea
|
||||
id="description" v-model="maintenance.description" class="form-control"
|
||||
></textarea>
|
||||
<div class="form-text">
|
||||
{{ $t("markdownSupported") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Affected Monitors -->
|
||||
@@ -82,35 +85,39 @@
|
||||
|
||||
<h2 class="mt-5">{{ $t("Date and Time") }}</h2>
|
||||
|
||||
<div>⚠️ {{ $t("warningTimezone") }}: <mark>{{ $root.info.serverTimezone }} ({{ $root.info.serverTimezoneOffset }})</mark></div>
|
||||
|
||||
<!-- Strategy -->
|
||||
<div class="my-3">
|
||||
<label for="strategy" class="form-label">{{ $t("Strategy") }}</label>
|
||||
<select id="strategy" v-model="maintenance.strategy" class="form-select">
|
||||
<option value="manual">{{ $t("strategyManual") }}</option>
|
||||
<option value="single">{{ $t("Single Maintenance Window") }}</option>
|
||||
<option value="cron">{{ $t("cronExpression") }}</option>
|
||||
<option value="recurring-interval">{{ $t("Recurring") }} - {{ $t("recurringInterval") }}</option>
|
||||
<option value="recurring-weekday">{{ $t("Recurring") }} - {{ $t("dayOfWeek") }}</option>
|
||||
<option value="recurring-day-of-month">{{ $t("Recurring") }} - {{ $t("dayOfMonth") }}</option>
|
||||
<option v-if="false" value="recurring-day-of-year">{{ $t("Recurring") }} - Day of Year</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Single Maintenance Window -->
|
||||
<template v-if="maintenance.strategy === 'single'">
|
||||
<!-- DateTime Range -->
|
||||
</template>
|
||||
|
||||
<template v-if="maintenance.strategy === 'cron'">
|
||||
<!-- Cron -->
|
||||
<div class="my-3">
|
||||
<label class="form-label">{{ $t("DateTime Range") }}</label>
|
||||
<Datepicker
|
||||
v-model="maintenance.dateRange"
|
||||
:dark="$root.isDark"
|
||||
range
|
||||
:monthChangeOnScroll="false"
|
||||
:minDate="minDate"
|
||||
format="yyyy-MM-dd HH:mm"
|
||||
modelType="yyyy-MM-dd HH:mm:ss"
|
||||
/>
|
||||
<label for="cron" class="form-label">
|
||||
{{ $t("cronExpression") }}
|
||||
</label>
|
||||
<p>{{ $t("cronSchedule") }}{{ cronDescription }}</p>
|
||||
<input id="cron" v-model="maintenance.cron" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<!-- Duration -->
|
||||
<label for="duration" class="form-label">
|
||||
{{ $t("Duration (Minutes)") }}
|
||||
</label>
|
||||
<input id="duration" v-model="maintenance.durationMinutes" type="number" class="form-control" required min="1" step="1">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -177,7 +184,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- For any recurring types -->
|
||||
<template v-if="maintenance.strategy === 'recurring-interval' || maintenance.strategy === 'recurring-weekday' || maintenance.strategy === 'recurring-day-of-month'">
|
||||
<!-- Maintenance Time Window of a Day -->
|
||||
<div class="my-3">
|
||||
@@ -189,33 +195,50 @@
|
||||
disableTimeRangeValidation range
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="maintenance.strategy === 'recurring-interval' || maintenance.strategy === 'recurring-weekday' || maintenance.strategy === 'recurring-day-of-month' || maintenance.strategy === 'cron' || maintenance.strategy === 'single'">
|
||||
<!-- Timezone -->
|
||||
<div class="mb-4">
|
||||
<label for="timezone" class="form-label">
|
||||
{{ $t("Timezone") }}
|
||||
</label>
|
||||
<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"
|
||||
:key="index"
|
||||
:value="timezone.value"
|
||||
>
|
||||
{{ timezone.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Date Range -->
|
||||
<div class="my-3">
|
||||
<label class="form-label">{{ $t("Effective Date Range") }}</label>
|
||||
<Datepicker
|
||||
v-model="maintenance.dateRange"
|
||||
:dark="$root.isDark"
|
||||
range datePicker
|
||||
:monthChangeOnScroll="false"
|
||||
:minDate="minDate"
|
||||
format="yyyy-MM-dd HH:mm:ss"
|
||||
modelType="yyyy-MM-dd HH:mm:ss"
|
||||
required
|
||||
/>
|
||||
<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" :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" :required="maintenance.strategy === 'single'">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="mt-4 mb-1">
|
||||
<button
|
||||
id="monitor-submit-btn" class="btn btn-primary" type="submit"
|
||||
:disabled="processing"
|
||||
>
|
||||
{{ $t("Save") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fixed-bottom-bar p-3">
|
||||
<button id="monitor-submit-btn" class="btn btn-primary" type="submit" :disabled="processing">{{ $t("Save") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -223,11 +246,11 @@
|
||||
</template>
|
||||
|
||||
<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";
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
@@ -239,6 +262,7 @@ export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
timezoneList: timezoneList(),
|
||||
processing: false,
|
||||
maintenance: {},
|
||||
affectedMonitors: [],
|
||||
@@ -247,24 +271,11 @@ export default {
|
||||
selectedStatusPages: [],
|
||||
dark: (this.$root.theme === "dark"),
|
||||
neverEnd: false,
|
||||
minDate: this.$root.date(dayjs()) + " 00:00",
|
||||
lastDays: [
|
||||
{
|
||||
langKey: "lastDay1",
|
||||
value: "lastDay1",
|
||||
},
|
||||
{
|
||||
langKey: "lastDay2",
|
||||
value: "lastDay2",
|
||||
},
|
||||
{
|
||||
langKey: "lastDay3",
|
||||
value: "lastDay3",
|
||||
},
|
||||
{
|
||||
langKey: "lastDay4",
|
||||
value: "lastDay4",
|
||||
}
|
||||
],
|
||||
weekdays: [
|
||||
{
|
||||
@@ -308,6 +319,34 @@ export default {
|
||||
|
||||
computed: {
|
||||
|
||||
cronDescription() {
|
||||
if (! this.maintenance.cron) {
|
||||
return "";
|
||||
}
|
||||
|
||||
let locale = "";
|
||||
|
||||
if (this.$root.language) {
|
||||
locale = this.$root.language.replace("-", "_");
|
||||
}
|
||||
|
||||
// Special handling
|
||||
// If locale is also not working in your language, you can map it here
|
||||
// https://github.com/bradymholt/cRonstrue/tree/master/src/i18n/locales
|
||||
if (locale === "zh_HK") {
|
||||
locale = "zh_TW";
|
||||
}
|
||||
|
||||
try {
|
||||
return cronstrue.toString(this.maintenance.cron, {
|
||||
locale,
|
||||
});
|
||||
} catch (e) {
|
||||
return this.$t("invalidCronExpression", e.message);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
selectedStatusPagesOptions() {
|
||||
return Object.values(this.$root.statusPageList).map(statusPage => {
|
||||
return {
|
||||
@@ -356,6 +395,7 @@ export default {
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
/** Initialise page */
|
||||
init() {
|
||||
this.affectedMonitors = [];
|
||||
this.selectedStatusPages = [];
|
||||
@@ -366,8 +406,10 @@ export default {
|
||||
description: "",
|
||||
strategy: "single",
|
||||
active: 1,
|
||||
cron: "30 3 * * *",
|
||||
durationMinutes: 60,
|
||||
intervalDay: 1,
|
||||
dateRange: [ this.minDate ],
|
||||
dateRange: [],
|
||||
timeRange: [{
|
||||
hours: 2,
|
||||
minutes: 0,
|
||||
@@ -377,6 +419,7 @@ export default {
|
||||
}],
|
||||
weekdays: [],
|
||||
daysOfMonth: [],
|
||||
timezoneOption: null,
|
||||
};
|
||||
} else if (this.isEdit) {
|
||||
this.$root.getSocket().emit("getMaintenance", this.$route.params.id, (res) => {
|
||||
@@ -414,6 +457,7 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
/** Create new maintenance */
|
||||
async submit() {
|
||||
this.processing = true;
|
||||
|
||||
@@ -458,6 +502,11 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Add monitor to maintenance
|
||||
* @param {number} maintenanceID
|
||||
* @param {socketCB} callback
|
||||
*/
|
||||
async addMonitorMaintenance(maintenanceID, callback) {
|
||||
await this.$root.addMonitorMaintenance(maintenanceID, this.affectedMonitors, async (res) => {
|
||||
if (!res.ok) {
|
||||
@@ -470,6 +519,11 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Add status page to maintenance
|
||||
* @param {number} maintenanceID
|
||||
* @param {socketCB} callback
|
||||
*/
|
||||
async addMaintenanceStatusPage(maintenanceID, callback) {
|
||||
await this.$root.addMaintenanceStatusPage(maintenanceID, (this.showOnAllPages) ? this.selectedStatusPagesOptions : this.selectedStatusPages, async (res) => {
|
||||
if (!res.ok) {
|
||||
@@ -486,10 +540,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.shadow-box {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 150px;
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@
|
||||
<div>
|
||||
<h1 class="mb-3">{{ pageName }}</h1>
|
||||
<form @submit.prevent="submit">
|
||||
<div class="shadow-box">
|
||||
<div class="shadow-box shadow-box-with-fixed-bottom-bar">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h2 class="mb-2">{{ $t("General") }}</h2>
|
||||
@@ -45,6 +45,9 @@
|
||||
<option value="steam">
|
||||
{{ $t("Steam Game Server") }}
|
||||
</option>
|
||||
<option value="gamedig">
|
||||
GameDig
|
||||
</option>
|
||||
<option value="mqtt">
|
||||
MQTT
|
||||
</option>
|
||||
@@ -57,10 +60,26 @@
|
||||
<option value="mysql">
|
||||
MySQL/MariaDB
|
||||
</option>
|
||||
<option value="mongodb">
|
||||
MongoDB
|
||||
</option>
|
||||
<option value="radius">
|
||||
Radius
|
||||
</option>
|
||||
<option value="redis">
|
||||
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>
|
||||
|
||||
@@ -71,7 +90,7 @@
|
||||
</div>
|
||||
|
||||
<!-- URL -->
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'browser' " class="my-3">
|
||||
<label for="url" class="form-label">{{ $t("URL") }}</label>
|
||||
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
|
||||
</div>
|
||||
@@ -79,7 +98,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 -->
|
||||
@@ -93,7 +112,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Keyword -->
|
||||
<div v-if="monitor.type === 'keyword' || monitor.type === 'grpc-keyword' " class="my-3">
|
||||
<div v-if="monitor.type === 'keyword' || monitor.type === 'grpc-keyword'" class="my-3">
|
||||
<label for="keyword" class="form-label">{{ $t("Keyword") }}</label>
|
||||
<input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required>
|
||||
<div class="form-text">
|
||||
@@ -101,16 +120,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game -->
|
||||
<!-- GameDig only -->
|
||||
<div v-if="monitor.type === 'gamedig'" class="my-3">
|
||||
<label for="game" class="form-label"> {{ $t("Game") }} </label>
|
||||
<select id="game" v-model="monitor.game" class="form-select" required>
|
||||
<option v-for="game in gameList" :key="game.keys[0]" :value="game.keys[0]">
|
||||
{{ game.pretty }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Hostname -->
|
||||
<!-- TCP Port / Ping / DNS / Steam / MQTT / Radius only -->
|
||||
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'mqtt' || monitor.type === 'radius'" class="my-3">
|
||||
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'gamedig' ||monitor.type === 'mqtt' || monitor.type === 'radius'" class="my-3">
|
||||
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
|
||||
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" :pattern="`${ipRegexPattern}|${hostnameRegexPattern}`" required>
|
||||
<input id="hostname" v-model="monitor.hostname" type="text" class="form-control" :pattern="`${monitor.type === 'mqtt' ? mqttIpOrHostnameRegexPattern : ipOrHostnameRegexPattern}`" required>
|
||||
</div>
|
||||
|
||||
<!-- Port -->
|
||||
<!-- For TCP Port / Steam / MQTT / Radius Type -->
|
||||
<div v-if="monitor.type === 'port' || monitor.type === 'steam' || monitor.type === 'mqtt' || monitor.type === 'radius'" class="my-3">
|
||||
<div v-if="monitor.type === 'port' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius'" class="my-3">
|
||||
<label for="port" class="form-label">{{ $t("Port") }}</label>
|
||||
<input id="port" v-model="monitor.port" type="number" class="form-control" required min="0" max="65535" step="1">
|
||||
</div>
|
||||
@@ -253,13 +283,13 @@
|
||||
<label for="sqlConnectionString" class="form-label">{{ $t("Connection String") }}</label>
|
||||
|
||||
<template v-if="monitor.type === 'sqlserver'">
|
||||
<input id="sqlConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control" placeholder="Server=<hostname>,<port>;Database=<your database>;User Id=<your user id>;Password=<your password>;Encrypt=<true/false>;TrustServerCertificate=<Yes/No>;Connection Timeout=<int>">
|
||||
<input id="sqlConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control">
|
||||
</template>
|
||||
<template v-if="monitor.type === 'postgres'">
|
||||
<input id="sqlConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control" placeholder="postgres://username:password@host:port/database">
|
||||
<input id="sqlConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control">
|
||||
</template>
|
||||
<template v-if="monitor.type === 'mysql'">
|
||||
<input id="sqlConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control" placeholder="mysql://username:password@host:port/database">
|
||||
<input id="sqlConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control">
|
||||
</template>
|
||||
</div>
|
||||
<div class="my-3">
|
||||
@@ -267,6 +297,24 @@
|
||||
<textarea id="sqlQuery" v-model="monitor.databaseQuery" class="form-control" placeholder="Example: select getdate()"></textarea>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Redis -->
|
||||
<template v-if="monitor.type === 'redis'">
|
||||
<div class="my-3">
|
||||
<label for="redisConnectionString" class="form-label">{{ $t("Connection String") }}</label>
|
||||
<input id="redisConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- MongoDB -->
|
||||
<template v-if="monitor.type === 'mongodb'">
|
||||
<div class="my-3">
|
||||
<label for="sqlConnectionString" class="form-label">{{ $t("Connection String") }}</label>
|
||||
|
||||
<template v-if="monitor.type === 'mongodb'">
|
||||
<input id="sqlConnectionString" v-model="monitor.databaseConnectionString" type="text" class="form-control">
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Interval -->
|
||||
<div class="my-3">
|
||||
@@ -292,7 +340,7 @@
|
||||
|
||||
<div class="my-3">
|
||||
<label for="resend-interval" class="form-label">
|
||||
{{ $t("Resend Notification if Down X times consequently") }}
|
||||
{{ $t("Resend Notification if Down X times consecutively") }}
|
||||
<span v-if="monitor.resendInterval > 0">({{ $t("resendEveryXTimes", [ monitor.resendInterval ]) }})</span>
|
||||
<span v-else>({{ $t("resendDisabled") }})</span>
|
||||
</label>
|
||||
@@ -327,6 +375,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ping packet size -->
|
||||
<div v-if="monitor.type === 'ping'" class="my-3">
|
||||
<label for="packet-size" class="form-label">{{ $t("Packet Size") }}</label>
|
||||
<input id="packet-size" v-model="monitor.packetSize" type="number" class="form-control" required min="1" max="65500" step="1">
|
||||
</div>
|
||||
|
||||
<!-- HTTP / Keyword only -->
|
||||
<template v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'grpc-keyword' ">
|
||||
<div class="my-3">
|
||||
@@ -360,12 +414,14 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="my-3">
|
||||
<tags-manager ref="tagsManager" :pre-selected-tags="monitor.tags"></tags-manager>
|
||||
<label for="description" class="form-label">{{ $t("Description") }}</label>
|
||||
<input id="description" v-model="monitor.description" type="text" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="mt-5 mb-1">
|
||||
<button id="monitor-submit-btn" class="btn btn-primary" type="submit" :disabled="processing">{{ $t("Save") }}</button>
|
||||
<div class="my-3">
|
||||
<tags-manager ref="tagsManager" :pre-selected-tags="monitor.tags"></tags-manager>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -453,6 +509,15 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Encoding -->
|
||||
<div class="my-3">
|
||||
<label for="httpBodyEncoding" class="form-label">{{ $t("Body Encoding") }}</label>
|
||||
<select id="httpBodyEncoding" v-model="monitor.httpBodyEncoding" class="form-select">
|
||||
<option value="json">JSON</option>
|
||||
<option value="xml">XML</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="my-3">
|
||||
<label for="body" class="form-label">{{ $t("Body") }}</label>
|
||||
@@ -481,28 +546,47 @@
|
||||
<option value="ntlm">
|
||||
NTLM
|
||||
</option>
|
||||
<option value="mtls">
|
||||
mTLS
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<template v-if="monitor.authMethod && monitor.authMethod !== null ">
|
||||
<div class="my-3">
|
||||
<label for="basicauth" class="form-label">{{ $t("Username") }}</label>
|
||||
<input id="basicauth-user" v-model="monitor.basic_auth_user" type="text" class="form-control" :placeholder="$t('Username')">
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="basicauth" class="form-label">{{ $t("Password") }}</label>
|
||||
<input id="basicauth-pass" v-model="monitor.basic_auth_pass" type="password" autocomplete="new-password" class="form-control" :placeholder="$t('Password')">
|
||||
</div>
|
||||
<template v-if="monitor.authMethod === 'ntlm' ">
|
||||
<template v-if="monitor.authMethod === 'mtls' ">
|
||||
<div class="my-3">
|
||||
<label for="basicauth" class="form-label">{{ $t("Domain") }}</label>
|
||||
<input id="basicauth-domain" v-model="monitor.authDomain" type="text" class="form-control" :placeholder="$t('Domain')">
|
||||
<label for="tls-cert" class="form-label">{{ $t("Cert") }}</label>
|
||||
<textarea id="tls-cert" v-model="monitor.tlsCert" class="form-control" :placeholder="$t('Cert body')" required></textarea>
|
||||
</div>
|
||||
<div class="my-3">
|
||||
<label for="tls-key" class="form-label">{{ $t("Key") }}</label>
|
||||
<textarea id="tls-key" v-model="monitor.tlsKey" class="form-control" :placeholder="$t('Key body')" required></textarea>
|
||||
</div>
|
||||
<div class="my-3">
|
||||
<label for="tls-ca" class="form-label">{{ $t("CA") }}</label>
|
||||
<textarea id="tls-ca" v-model="monitor.tlsCa" class="form-control" :placeholder="$t('Server CA')"></textarea>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="my-3">
|
||||
<label for="basicauth-user" class="form-label">{{ $t("Username") }}</label>
|
||||
<input id="basicauth-user" v-model="monitor.basic_auth_user" type="text" class="form-control" :placeholder="$t('Username')">
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="basicauth" class="form-label">{{ $t("Workstation") }}</label>
|
||||
<input id="basicauth-workstation" v-model="monitor.authWorkstation" type="text" class="form-control" :placeholder="$t('Workstation')">
|
||||
<label for="basicauth-pass" class="form-label">{{ $t("Password") }}</label>
|
||||
<input id="basicauth-pass" v-model="monitor.basic_auth_pass" type="password" autocomplete="new-password" class="form-control" :placeholder="$t('Password')">
|
||||
</div>
|
||||
<template v-if="monitor.authMethod === 'ntlm' ">
|
||||
<div class="my-3">
|
||||
<label for="ntlm-domain" class="form-label">{{ $t("Domain") }}</label>
|
||||
<input id="ntlm-domain" v-model="monitor.authDomain" type="text" class="form-control" :placeholder="$t('Domain')">
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="ntlm-workstation" class="form-label">{{ $t("Workstation") }}</label>
|
||||
<input id="ntlm-workstation" v-model="monitor.authWorkstation" type="text" class="form-control" :placeholder="$t('Workstation')">
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
@@ -557,6 +641,10 @@
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fixed-bottom-bar p-3">
|
||||
<button id="monitor-submit-btn" class="btn btn-primary" type="submit" :disabled="processing">{{ $t("Save") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -576,6 +664,7 @@ import DockerHostDialog from "../components/DockerHostDialog.vue";
|
||||
import ProxyDialog from "../components/ProxyDialog.vue";
|
||||
import TagsManager from "../components/TagsManager.vue";
|
||||
import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } from "../util.ts";
|
||||
import { hostNameRegexPattern } from "../util-frontend";
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
@@ -600,11 +689,16 @@ export default {
|
||||
},
|
||||
acceptedStatusCodeOptions: [],
|
||||
dnsresolvetypeOptions: [],
|
||||
|
||||
// Source: https://digitalfortress.tech/tips/top-15-commonly-used-regex/
|
||||
ipRegexPattern: "((^\\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\\s*$)|(^\\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?\\s*$))",
|
||||
// Source: https://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address
|
||||
hostnameRegexPattern: "^(([a-zA-Z0-9_]|[a-zA-Z0-9_][a-zA-Z0-9\\-_]*[a-zA-Z0-9_])\\.)*([A-Za-z0-9_]|[A-Za-z0-9_][A-Za-z0-9\\-_]*[A-Za-z0-9_])$"
|
||||
ipOrHostnameRegexPattern: hostNameRegexPattern(),
|
||||
mqttIpOrHostnameRegexPattern: hostNameRegexPattern(true),
|
||||
gameList: null,
|
||||
connectionStringTemplates: {
|
||||
"sqlserver": "Server=<hostname>,<port>;Database=<your database>;User Id=<your user id>;Password=<your password>;Encrypt=<true/false>;TrustServerCertificate=<Yes/No>;Connection Timeout=<int>",
|
||||
"postgres": "postgres://username:password@host:port/database",
|
||||
"mysql": "mysql://username:password@host:port/database",
|
||||
"redis": "redis://user:password@host:port",
|
||||
"mongodb": "mongodb://username:password@host:port/database",
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
@@ -620,13 +714,23 @@ export default {
|
||||
},
|
||||
|
||||
pageName() {
|
||||
return this.$t((this.isAdd) ? "Add New Monitor" : "Edit");
|
||||
let name = "Add New Monitor";
|
||||
if (this.isClone) {
|
||||
name = "Clone Monitor";
|
||||
} else if (this.isEdit) {
|
||||
name = "Edit";
|
||||
}
|
||||
return this.$t(name);
|
||||
},
|
||||
|
||||
isAdd() {
|
||||
return this.$route.path === "/add";
|
||||
},
|
||||
|
||||
isClone() {
|
||||
return this.$route.path.startsWith("/clone");
|
||||
},
|
||||
|
||||
isEdit() {
|
||||
return this.$route.path.startsWith("/edit");
|
||||
},
|
||||
@@ -670,6 +774,15 @@ message HealthCheckResponse {
|
||||
` ]);
|
||||
},
|
||||
bodyPlaceholder() {
|
||||
if (this.monitor && this.monitor.httpBodyEncoding && this.monitor.httpBodyEncoding === "xml") {
|
||||
return this.$t("Example:", [ `
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<soap:Body>
|
||||
<Uptime>Kuma</Uptime>
|
||||
</soap:Body>
|
||||
</soap:Envelope>` ]);
|
||||
}
|
||||
return this.$t("Example:", [ `
|
||||
{
|
||||
"key": "value"
|
||||
@@ -681,7 +794,18 @@ message HealthCheckResponse {
|
||||
{
|
||||
"HeaderName": "HeaderValue"
|
||||
}` ]);
|
||||
}
|
||||
},
|
||||
|
||||
currentGameObject() {
|
||||
if (this.gameList) {
|
||||
for (let game of this.gameList) {
|
||||
if (game.keys[0] === this.monitor.game) {
|
||||
return game;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
},
|
||||
watch: {
|
||||
@@ -725,8 +849,43 @@ message HealthCheckResponse {
|
||||
this.monitor.port = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the game list from server
|
||||
if (this.monitor.type === "gamedig") {
|
||||
this.$root.getSocket().emit("getGameList", (res) => {
|
||||
if (res.ok) {
|
||||
this.gameList = res.gameList;
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Set default database connection string if empty or it is a template from another database monitor type
|
||||
for (let monitorType in this.connectionStringTemplates) {
|
||||
if (this.monitor.type === monitorType) {
|
||||
let isTemplate = false;
|
||||
for (let key in this.connectionStringTemplates) {
|
||||
if (this.monitor.databaseConnectionString === this.connectionStringTemplates[key]) {
|
||||
isTemplate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!this.monitor.databaseConnectionString || isTemplate) {
|
||||
this.monitor.databaseConnectionString = this.connectionStringTemplates[monitorType];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
currentGameObject(newGameObject, previousGameObject) {
|
||||
if (!this.monitor.port || (previousGameObject && previousGameObject.options.port === this.monitor.port)) {
|
||||
this.monitor.port = newGameObject.options.port;
|
||||
}
|
||||
this.monitor.game = newGameObject.keys[0];
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
@@ -772,10 +931,11 @@ message HealthCheckResponse {
|
||||
interval: 60,
|
||||
retryInterval: this.interval,
|
||||
resendInterval: 0,
|
||||
maxretries: 0,
|
||||
maxretries: 1,
|
||||
notificationIDList: {},
|
||||
ignoreTls: false,
|
||||
upsideDown: false,
|
||||
packetSize: 56,
|
||||
expiryNotification: false,
|
||||
maxredirects: 10,
|
||||
accepted_statuscodes: [ "200-299" ],
|
||||
@@ -789,6 +949,7 @@ message HealthCheckResponse {
|
||||
mqttTopic: "",
|
||||
mqttSuccessMessage: "",
|
||||
authMethod: null,
|
||||
httpBodyEncoding: "json"
|
||||
};
|
||||
|
||||
if (this.$root.proxyList && !this.monitor.proxyId) {
|
||||
@@ -804,11 +965,40 @@ message HealthCheckResponse {
|
||||
this.monitor.notificationIDList[this.$root.notificationList[i].id] = true;
|
||||
}
|
||||
}
|
||||
} else if (this.isEdit) {
|
||||
} else if (this.isEdit || this.isClone) {
|
||||
this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => {
|
||||
if (res.ok) {
|
||||
|
||||
if (this.isClone) {
|
||||
// Reset push token for cloned monitors
|
||||
if (res.monitor.type === "push") {
|
||||
res.monitor.pushToken = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
this.monitor = res.monitor;
|
||||
|
||||
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.
|
||||
*/
|
||||
this.monitor.id = undefined; // Remove id when cloning as we want a new id
|
||||
this.monitor.includeSensitiveData = undefined;
|
||||
this.monitor.maintenance = undefined;
|
||||
this.monitor.name = this.$t("cloneOf", [ this.monitor.name ]);
|
||||
this.$refs.tagsManager.newTags = this.monitor.tags.map((monitorTag) => {
|
||||
return {
|
||||
id: monitorTag.tag_id,
|
||||
name: monitorTag.name,
|
||||
color: monitorTag.color,
|
||||
value: monitorTag.value,
|
||||
new: true,
|
||||
};
|
||||
});
|
||||
this.monitor.tags = undefined;
|
||||
}
|
||||
|
||||
// Handling for monitors that are created before 1.7.0
|
||||
if (this.monitor.retryInterval === 0) {
|
||||
this.monitor.retryInterval = this.monitor.interval;
|
||||
@@ -826,7 +1016,7 @@ message HealthCheckResponse {
|
||||
* @returns {boolean} Is the form input valid?
|
||||
*/
|
||||
isInputValid() {
|
||||
if (this.monitor.body) {
|
||||
if (this.monitor.body && (!this.monitor.httpBodyEncoding || this.monitor.httpBodyEncoding === "json")) {
|
||||
try {
|
||||
JSON.parse(this.monitor.body);
|
||||
} catch (err) {
|
||||
@@ -850,6 +1040,7 @@ message HealthCheckResponse {
|
||||
* @returns {void}
|
||||
*/
|
||||
async submit() {
|
||||
|
||||
this.processing = true;
|
||||
|
||||
if (!this.isInputValid()) {
|
||||
@@ -857,16 +1048,28 @@ message HealthCheckResponse {
|
||||
return;
|
||||
}
|
||||
|
||||
// Beautify the JSON format
|
||||
if (this.monitor.body) {
|
||||
// Beautify the JSON format (only if httpBodyEncoding is not set or === json)
|
||||
if (this.monitor.body && (!this.monitor.httpBodyEncoding || this.monitor.httpBodyEncoding === "json")) {
|
||||
this.monitor.body = JSON.stringify(JSON.parse(this.monitor.body), null, 4);
|
||||
}
|
||||
|
||||
if (this.monitor.type && this.monitor.type !== "http" && this.monitor.type !== "keyword") {
|
||||
this.monitor.httpBodyEncoding = null;
|
||||
}
|
||||
|
||||
if (this.monitor.headers) {
|
||||
this.monitor.headers = JSON.stringify(JSON.parse(this.monitor.headers), null, 4);
|
||||
}
|
||||
|
||||
if (this.isAdd) {
|
||||
if (this.monitor.hostname) {
|
||||
this.monitor.hostname = this.monitor.hostname.trim();
|
||||
}
|
||||
|
||||
if (this.monitor.url) {
|
||||
this.monitor.url = this.monitor.url.trim();
|
||||
}
|
||||
|
||||
if (this.isAdd || this.isClone) {
|
||||
this.$root.add(this.monitor, async (res) => {
|
||||
|
||||
if (res.ok) {
|
||||
@@ -915,15 +1118,13 @@ message HealthCheckResponse {
|
||||
// Enable it if the Docker Host is added in EditMonitor.vue
|
||||
addedDockerHost(id) {
|
||||
this.monitor.docker_host = id;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.shadow-box {
|
||||
padding: 20px;
|
||||
}
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
textarea {
|
||||
min-height: 200px;
|
||||
|
@@ -65,6 +65,7 @@ export default {
|
||||
this.init();
|
||||
},
|
||||
methods: {
|
||||
/** Initialise page */
|
||||
init() {
|
||||
this.$root.getSocket().emit("getMonitorMaintenance", this.$route.params.id, (res) => {
|
||||
if (res.ok) {
|
||||
@@ -83,10 +84,12 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
/** Confirm deletion */
|
||||
deleteDialog() {
|
||||
this.$refs.confirmDelete.show();
|
||||
},
|
||||
|
||||
/** Delete maintenance after showing confirmation */
|
||||
deleteMaintenance() {
|
||||
this.$root.deleteMaintenance(this.maintenance.id, (res) => {
|
||||
if (res.ok) {
|
||||
|
@@ -62,7 +62,7 @@
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-3" style="font-size: 13px;">
|
||||
<a href="https://github.com/louislam/uptime-kuma/wiki/Maintenance" target="_blank">Learn More</a>
|
||||
<a href="https://github.com/louislam/uptime-kuma/wiki/Maintenance" target="_blank">{{ $t("Learn More") }}</a>
|
||||
</div>
|
||||
|
||||
<Confirm ref="confirmPause" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="pauseMaintenance">
|
||||
@@ -133,15 +133,25 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get maintenance URL
|
||||
* @param {number} id
|
||||
* @returns {string} Relative URL
|
||||
*/
|
||||
maintenanceURL(id) {
|
||||
return getMaintenanceRelativeURL(id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Show delete confirmation
|
||||
* @param {number} maintenanceID
|
||||
*/
|
||||
deleteDialog(maintenanceID) {
|
||||
this.selectedMaintenanceID = maintenanceID;
|
||||
this.$refs.confirmDelete.show();
|
||||
},
|
||||
|
||||
/** Delete maintenance after showing confirmation dialog */
|
||||
deleteMaintenance() {
|
||||
this.$root.deleteMaintenance(this.selectedMaintenanceID, (res) => {
|
||||
if (res.ok) {
|
||||
|
@@ -95,6 +95,9 @@ export default {
|
||||
"reverse-proxy": {
|
||||
title: this.$t("Reverse Proxy"),
|
||||
},
|
||||
tags: {
|
||||
title: this.$t("Tags"),
|
||||
},
|
||||
"monitor-history": {
|
||||
title: this.$t("Monitor History"),
|
||||
},
|
||||
@@ -104,12 +107,21 @@ export default {
|
||||
security: {
|
||||
title: this.$t("Security"),
|
||||
},
|
||||
"api-keys": {
|
||||
title: this.$t("API Keys")
|
||||
},
|
||||
proxies: {
|
||||
title: this.$t("Proxies"),
|
||||
},
|
||||
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"),
|
||||
},
|
||||
|
@@ -14,7 +14,7 @@
|
||||
</p>
|
||||
|
||||
<div class="form-floating">
|
||||
<select id="language" v-model="$i18n.locale" class="form-select">
|
||||
<select id="language" v-model="$root.language" class="form-select">
|
||||
<option v-for="(lang, i) in $i18n.availableLocales" :key="`Lang${i}`" :value="lang">
|
||||
{{ $i18n.messages[lang].languageName }}
|
||||
</option>
|
||||
@@ -59,9 +59,7 @@ export default {
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
"$i18n.locale"() {
|
||||
localStorage.locale = this.$i18n.locale;
|
||||
},
|
||||
|
||||
},
|
||||
mounted() {
|
||||
this.$root.getSocket().emit("needSetup", (needSetup) => {
|
||||
|
@@ -20,17 +20,27 @@
|
||||
<div class="my-3">
|
||||
<label for="description" class="form-label">{{ $t("Description") }}</label>
|
||||
<textarea id="description" v-model="config.description" class="form-control"></textarea>
|
||||
<div class="form-text">
|
||||
{{ $t("markdownSupported") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer Text -->
|
||||
<div class="my-3">
|
||||
<label for="footer-text" class="form-label">{{ $t("Footer Text") }}</label>
|
||||
<textarea id="footer-text" v-model="config.footerText" class="form-control"></textarea>
|
||||
<div class="form-text">
|
||||
{{ $t("markdownSupported") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-3 form-check form-switch">
|
||||
<input id="switch-theme" v-model="config.theme" class="form-check-input" type="checkbox" true-value="dark" false-value="light">
|
||||
<label class="form-check-label" for="switch-theme">{{ $t("Switch to Dark Theme") }}</label>
|
||||
<div class="my-3">
|
||||
<label for="switch-theme" class="form-label">{{ $t("Theme") }}</label>
|
||||
<select id="switch-theme" v-model="config.theme" class="form-select">
|
||||
<option value="auto">{{ $t("Auto") }}</option>
|
||||
<option value="light">{{ $t("Light") }}</option>
|
||||
<option value="dark">{{ $t("Dark") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="my-3 form-check form-switch">
|
||||
@@ -64,6 +74,12 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Google Analytics -->
|
||||
<div class="my-3">
|
||||
<label for="googleAnalyticsTag" class="form-label">{{ $t("Google Analytics ID") }}</label>
|
||||
<input id="googleAnalyticsTag" v-model="config.googleAnalyticsId" type="text" class="form-control">
|
||||
</div>
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<div class="my-3">
|
||||
<div class="mb-1">{{ $t("Custom CSS") }}</div>
|
||||
@@ -148,7 +164,12 @@
|
||||
<Editable v-model="incident.title" tag="h4" :contenteditable="editIncidentMode" :noNL="true" class="alert-heading" />
|
||||
|
||||
<strong v-if="editIncidentMode">{{ $t("Content") }}:</strong>
|
||||
<Editable v-model="incident.content" tag="div" :contenteditable="editIncidentMode" class="content" />
|
||||
<Editable v-if="editIncidentMode" v-model="incident.content" tag="div" :contenteditable="editIncidentMode" class="content" />
|
||||
<div v-if="editIncidentMode" class="form-text">
|
||||
{{ $t("markdownSupported") }}
|
||||
</div>
|
||||
<!-- eslint-disable-next-line vue/no-v-html-->
|
||||
<div v-if="! editIncidentMode" class="content" v-html="incidentHTML"></div>
|
||||
|
||||
<!-- Incident Date -->
|
||||
<div class="date mt-3">
|
||||
@@ -236,14 +257,17 @@
|
||||
class="shadow-box alert mb-4 p-3 bg-maintenance mt-4 position-relative" role="alert"
|
||||
>
|
||||
<h4 class="alert-heading">{{ maintenance.title }}</h4>
|
||||
<div class="content">{{ maintenance.description }}</div>
|
||||
<!-- eslint-disable-next-line vue/no-v-html-->
|
||||
<div class="content" v-html="maintenanceHTML(maintenance.description)"></div>
|
||||
<MaintenanceTime :maintenance="maintenance" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Description -->
|
||||
<strong v-if="editMode">{{ $t("Description") }}:</strong>
|
||||
<Editable v-model="config.description" :contenteditable="editMode" tag="div" class="mb-4 description" />
|
||||
<Editable v-if="enableEditMode" v-model="config.description" :contenteditable="editMode" tag="div" class="mb-4 description" />
|
||||
<!-- eslint-disable-next-line vue/no-v-html-->
|
||||
<div v-if="! enableEditMode" class="alert-heading p-2" v-html="descriptionHTML"></div>
|
||||
|
||||
<div v-if="editMode" class="mb-4">
|
||||
<div>
|
||||
@@ -256,9 +280,22 @@
|
||||
<div class="mt-3">
|
||||
<div v-if="allMonitorList.length > 0 && loadedData">
|
||||
<label>{{ $t("Add a monitor") }}:</label>
|
||||
<select v-model="selectedMonitor" class="form-control">
|
||||
<option v-for="monitor in allMonitorList" :key="monitor.id" :value="monitor">{{ monitor.name }}</option>
|
||||
</select>
|
||||
<VueMultiselect
|
||||
v-model="selectedMonitor"
|
||||
:options="allMonitorList"
|
||||
:multiple="false"
|
||||
:searchable="true"
|
||||
:placeholder="$t('Add a monitor')"
|
||||
label="name"
|
||||
trackBy="name"
|
||||
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>
|
||||
</template>
|
||||
</VueMultiselect>
|
||||
</div>
|
||||
<div v-else class="text-center">
|
||||
{{ $t("No monitors available.") }} <router-link to="/add">{{ $t("Add one") }}</router-link>
|
||||
@@ -279,11 +316,18 @@
|
||||
<div class="custom-footer-text text-start">
|
||||
<strong v-if="enableEditMode">{{ $t("Custom Footer") }}:</strong>
|
||||
</div>
|
||||
<Editable v-model="config.footerText" tag="div" :contenteditable="enableEditMode" :noNL="false" class="alert-heading p-2" />
|
||||
<Editable v-if="enableEditMode" v-model="config.footerText" tag="div" :contenteditable="enableEditMode" :noNL="false" class="alert-heading p-2" />
|
||||
<!-- eslint-disable-next-line vue/no-v-html-->
|
||||
<div v-if="! enableEditMode" class="alert-heading p-2" v-html="footerHTML"></div>
|
||||
|
||||
<p v-if="config.showPoweredBy">
|
||||
{{ $t("Powered by") }} <a target="_blank" rel="noopener noreferrer" href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma" ) }}</a>
|
||||
</p>
|
||||
|
||||
<div class="refresh-info mb-2">
|
||||
<div>{{ $t("Last Updated") }}: <date-time :value="lastUpdateTime" /></div>
|
||||
<div>{{ $tc("statusPageRefreshIn", [ updateCountdownText]) }}</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -300,6 +344,7 @@
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import dayjs from "dayjs";
|
||||
import duration from "dayjs/plugin/duration";
|
||||
import Favico from "favico.js";
|
||||
// import highlighting library (you can use any library you want just return html string)
|
||||
import { highlight, languages } from "prismjs/components/prism-core";
|
||||
@@ -310,13 +355,19 @@ import ImageCropUpload from "vue-image-crop-upload";
|
||||
import { PrismEditor } from "vue-prism-editor";
|
||||
import "vue-prism-editor/dist/prismeditor.min.css"; // import the styles somewhere
|
||||
import { useToast } from "vue-toastification";
|
||||
import { marked } from "marked";
|
||||
import DOMPurify from "dompurify";
|
||||
import Confirm from "../components/Confirm.vue";
|
||||
import PublicGroupList from "../components/PublicGroupList.vue";
|
||||
import MaintenanceTime from "../components/MaintenanceTime.vue";
|
||||
import DateTime from "../components/Datetime.vue";
|
||||
import { getResBaseURL } from "../util-frontend";
|
||||
import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_MAINTENANCE, STATUS_PAGE_PARTIAL_DOWN, UP, MAINTENANCE } from "../util.ts";
|
||||
import Tag from "../components/Tag.vue";
|
||||
import VueMultiselect from "vue-multiselect";
|
||||
|
||||
const toast = useToast();
|
||||
dayjs.extend(duration);
|
||||
|
||||
const leavePageMsg = "Do you really want to leave? you have unsaved changes!";
|
||||
|
||||
@@ -335,6 +386,9 @@ export default {
|
||||
Confirm,
|
||||
PrismEditor,
|
||||
MaintenanceTime,
|
||||
DateTime,
|
||||
Tag,
|
||||
VueMultiselect
|
||||
},
|
||||
|
||||
// Leave Page for vue route change
|
||||
@@ -376,6 +430,10 @@ export default {
|
||||
baseURL: "",
|
||||
clickedEditButton: false,
|
||||
maintenanceList: [],
|
||||
autoRefreshInterval: 5,
|
||||
lastUpdateTime: dayjs(),
|
||||
updateCountdown: null,
|
||||
updateCountdownText: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -477,6 +535,29 @@ export default {
|
||||
return this.overallStatus === STATUS_PAGE_MAINTENANCE;
|
||||
},
|
||||
|
||||
incidentHTML() {
|
||||
if (this.incident.content != null) {
|
||||
return DOMPurify.sanitize(marked(this.incident.content));
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
|
||||
descriptionHTML() {
|
||||
if (this.config.description != null) {
|
||||
return DOMPurify.sanitize(marked(this.config.description));
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
|
||||
footerHTML() {
|
||||
if (this.config.footerText != null) {
|
||||
return DOMPurify.sanitize(marked(this.config.footerText));
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
||||
@@ -590,11 +671,13 @@ export default {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
// 5mins a loop
|
||||
// Configure auto-refresh loop
|
||||
this.updateHeartbeatList();
|
||||
feedInterval = setInterval(() => {
|
||||
this.updateHeartbeatList();
|
||||
}, (300 + 10) * 1000);
|
||||
}, (this.autoRefreshInterval * 60 + 10) * 1000);
|
||||
|
||||
this.updateUpdateTimer();
|
||||
|
||||
// Go to edit page if ?edit present
|
||||
// null means ?edit present, but no value
|
||||
@@ -653,10 +736,29 @@ export default {
|
||||
favicon.badge(downMonitors);
|
||||
|
||||
this.loadedData = true;
|
||||
this.lastUpdateTime = dayjs();
|
||||
this.updateUpdateTimer();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Setup timer to display countdown to refresh
|
||||
* @returns {void}
|
||||
*/
|
||||
updateUpdateTimer() {
|
||||
clearInterval(this.updateCountdown);
|
||||
|
||||
this.updateCountdown = setInterval(() => {
|
||||
const countdown = dayjs.duration(this.lastUpdateTime.add(this.autoRefreshInterval, "minutes").add(10, "seconds").diff(dayjs()));
|
||||
if (countdown.as("seconds") < 0) {
|
||||
clearInterval(this.updateCountdown);
|
||||
} else {
|
||||
this.updateCountdownText = countdown.format("mm:ss");
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
/** Enable editing mode */
|
||||
edit() {
|
||||
if (this.hasToken) {
|
||||
@@ -836,6 +938,19 @@ export default {
|
||||
this.config.domainNameList.splice(index, 1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate sanitized HTML from maintenance description
|
||||
* @param {string} description
|
||||
* @returns {string} Sanitized HTML
|
||||
*/
|
||||
maintenanceHTML(description) {
|
||||
if (description) {
|
||||
return DOMPurify.sanitize(marked(description));
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1062,4 +1177,8 @@ footer {
|
||||
}
|
||||
}
|
||||
|
||||
.refresh-info {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
Reference in New Issue
Block a user