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 feature/482-add-description-to-monitor
# Conflicts: # server/database.js # server/model/monitor.js # src/icon.js # src/languages/en.js # src/languages/es-ES.js
This commit is contained in:
82
src/pages/AddStatusPage.vue
Normal file
82
src/pages/AddStatusPage.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<transition name="slide-fade" appear>
|
||||
<div>
|
||||
<h1 class="mb-3">
|
||||
{{ $t("Add New Status Page") }}
|
||||
</h1>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div class="shadow-box">
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">{{ $t("Name") }}</label>
|
||||
<input id="name" v-model="title" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="slug" class="form-label">{{ $t("Slug") }}</label>
|
||||
<div class="input-group">
|
||||
<span id="basic-addon3" class="input-group-text">/status/</span>
|
||||
<input id="slug" v-model="slug" type="text" class="form-control" required>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
<ul>
|
||||
<li>{{ $t("Accept characters:") }} <mark>a-z</mark> <mark>0-9</mark> <mark>-</mark></li>
|
||||
<i18n-t tag="li" keypath="startOrEndWithOnly">
|
||||
<mark>a-z</mark> <mark>0-9</mark>
|
||||
</i18n-t>
|
||||
<li>{{ $t("No consecutive dashes") }} <mark>--</mark></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 mb-1">
|
||||
<button id="monitor-submit-btn" class="btn btn-primary w-100" type="submit" :disabled="processing">{{ $t("Next") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
components: {
|
||||
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: "",
|
||||
slug: "",
|
||||
processing: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
/** Submit form data to add new status page */
|
||||
async submit() {
|
||||
this.processing = true;
|
||||
|
||||
this.$root.getSocket().emit("addStatusPage", this.title, this.slug, (res) => {
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok) {
|
||||
location.href = "/status/" + this.slug + "?edit";
|
||||
} else {
|
||||
|
||||
if (res.msg.includes("UNIQUE constraint")) {
|
||||
this.$root.toastError(this.$t("The slug is already taken. Please choose another slug."));
|
||||
} else {
|
||||
this.$root.toastRes(res);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.shadow-box {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div v-if="! $root.isMobile" class="col-12 col-md-5 col-xl-4">
|
||||
<div v-if="!$root.isMobile" class="col-12 col-md-5 col-xl-4">
|
||||
<div>
|
||||
<router-link to="/add" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> {{ $t("Add New Monitor") }}</router-link>
|
||||
</div>
|
||||
@@ -25,9 +25,9 @@ export default {
|
||||
MonitorList,
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
return {};
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@@ -9,19 +9,23 @@
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h3>{{ $t("Up") }}</h3>
|
||||
<span class="num">{{ stats.up }}</span>
|
||||
<span class="num">{{ $root.stats.up }}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>{{ $t("Down") }}</h3>
|
||||
<span class="num text-danger">{{ stats.down }}</span>
|
||||
<span class="num text-danger">{{ $root.stats.down }}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>{{ $t("Maintenance") }}</h3>
|
||||
<span class="num text-maintenance">{{ $root.stats.maintenance }}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>{{ $t("Unknown") }}</h3>
|
||||
<span class="num text-secondary">{{ stats.unknown }}</span>
|
||||
<span class="num text-secondary">{{ $root.stats.unknown }}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h3>{{ $t("pauseDashboardHome") }}</h3>
|
||||
<span class="num text-secondary">{{ stats.pause }}</span>
|
||||
<span class="num text-secondary">{{ $root.stats.pause }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -89,37 +93,6 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
stats() {
|
||||
let result = {
|
||||
up: 0,
|
||||
down: 0,
|
||||
unknown: 0,
|
||||
pause: 0,
|
||||
};
|
||||
|
||||
for (let monitorID in this.$root.monitorList) {
|
||||
let beat = this.$root.lastHeartbeatList[monitorID];
|
||||
let monitor = this.$root.monitorList[monitorID];
|
||||
|
||||
if (monitor && ! monitor.active) {
|
||||
result.pause++;
|
||||
} else if (beat) {
|
||||
if (beat.status === 1) {
|
||||
result.up++;
|
||||
} else if (beat.status === 0) {
|
||||
result.down++;
|
||||
} else if (beat.status === 2) {
|
||||
result.up++;
|
||||
} else {
|
||||
result.unknown++;
|
||||
}
|
||||
} else {
|
||||
result.unknown++;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
importantHeartBeatList() {
|
||||
let result = [];
|
||||
@@ -149,6 +122,7 @@ export default {
|
||||
return 0;
|
||||
});
|
||||
|
||||
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
|
||||
this.heartBeatList = result;
|
||||
|
||||
return result;
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<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">{{ monitor.url }}</a>
|
||||
<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>
|
||||
<span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span>
|
||||
<span v-if="monitor.type === 'keyword'">
|
||||
@@ -21,18 +21,23 @@
|
||||
</p>
|
||||
|
||||
<div class="functions">
|
||||
<button v-if="monitor.active" class="btn btn-light" @click="pauseDialog">
|
||||
<font-awesome-icon icon="pause" /> {{ $t("Pause") }}
|
||||
</button>
|
||||
<button v-if="! monitor.active" class="btn btn-primary" @click="resumeMonitor">
|
||||
<font-awesome-icon icon="play" /> {{ $t("Resume") }}
|
||||
</button>
|
||||
<router-link :to=" '/edit/' + monitor.id " class="btn btn-secondary">
|
||||
<font-awesome-icon icon="edit" /> {{ $t("Edit") }}
|
||||
</router-link>
|
||||
<button class="btn btn-danger" @click="deleteDialog">
|
||||
<font-awesome-icon icon="trash" /> {{ $t("Delete") }}
|
||||
</button>
|
||||
<div class="btn-group" role="group">
|
||||
<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">
|
||||
<font-awesome-icon icon="play" /> {{ $t("Resume") }}
|
||||
</button>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shadow-box">
|
||||
@@ -78,7 +83,7 @@
|
||||
<h4>{{ $t("Cert Exp.") }}</h4>
|
||||
<p>(<Datetime :value="tlsInfo.certInfo.validTo" date-only />)</p>
|
||||
<span class="num">
|
||||
<a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{ tlsInfo.certInfo.daysRemaining }} {{ $t("days") }}</a>
|
||||
<a href="#" @click.prevent="toggleCertInfoBox = !toggleCertInfoBox">{{ tlsInfo.certInfo.daysRemaining }} {{ $tc("day", tlsInfo.certInfo.daysRemaining) }}</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -290,39 +295,47 @@ export default {
|
||||
|
||||
},
|
||||
methods: {
|
||||
/** Request a test notification be sent for this monitor */
|
||||
testNotification() {
|
||||
this.$root.getSocket().emit("testNotification", this.monitor.id);
|
||||
toast.success("Test notification is requested.");
|
||||
},
|
||||
|
||||
/** Show dialog to confirm pause */
|
||||
pauseDialog() {
|
||||
this.$refs.confirmPause.show();
|
||||
},
|
||||
|
||||
/** Resume this monitor */
|
||||
resumeMonitor() {
|
||||
this.$root.getSocket().emit("resumeMonitor", this.monitor.id, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
});
|
||||
},
|
||||
|
||||
/** Request that this monitor is paused */
|
||||
pauseMonitor() {
|
||||
this.$root.getSocket().emit("pauseMonitor", this.monitor.id, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
});
|
||||
},
|
||||
|
||||
/** Show dialog to confirm deletion */
|
||||
deleteDialog() {
|
||||
this.$refs.confirmDelete.show();
|
||||
},
|
||||
|
||||
/** Show dialog to confirm clearing events */
|
||||
clearEventsDialog() {
|
||||
this.$refs.confirmClearEvents.show();
|
||||
},
|
||||
|
||||
/** Show dialog to confirm clearing heartbeats */
|
||||
clearHeartbeatsDialog() {
|
||||
this.$refs.confirmClearHeartbeats.show();
|
||||
},
|
||||
|
||||
/** Request that this monitor is deleted */
|
||||
deleteMonitor() {
|
||||
this.$root.deleteMonitor(this.monitor.id, (res) => {
|
||||
if (res.ok) {
|
||||
@@ -334,6 +347,7 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
/** Request that this monitors events are cleared */
|
||||
clearEvents() {
|
||||
this.$root.clearEvents(this.monitor.id, (res) => {
|
||||
if (! res.ok) {
|
||||
@@ -342,6 +356,7 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
/** Request that this monitors heartbeats are cleared */
|
||||
clearHeartbeats() {
|
||||
this.$root.clearHeartbeats(this.monitor.id, (res) => {
|
||||
if (! res.ok) {
|
||||
@@ -350,6 +365,11 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the correct title for the ping stat
|
||||
* @param {boolean} [average=false] Is the statistic an average?
|
||||
* @returns {string} Title formated dependant on monitor type
|
||||
*/
|
||||
pingTitle(average = false) {
|
||||
let translationPrefix = "";
|
||||
if (average) {
|
||||
@@ -378,11 +398,6 @@ export default {
|
||||
@media (max-width: 550px) {
|
||||
.functions {
|
||||
text-align: center;
|
||||
|
||||
button, a {
|
||||
margin-left: 10px !important;
|
||||
margin-right: 10px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.ping-chart-wrapper {
|
||||
@@ -425,12 +440,6 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.functions {
|
||||
button, a {
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.shadow-box {
|
||||
padding: 20px;
|
||||
margin-top: 25px;
|
||||
@@ -468,6 +477,12 @@ table {
|
||||
|
||||
.dropdown-clear-data {
|
||||
float: right;
|
||||
|
||||
ul {
|
||||
width: 100%;
|
||||
min-width: unset;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
549
src/pages/EditMaintenance.vue
Normal file
549
src/pages/EditMaintenance.vue
Normal file
@@ -0,0 +1,549 @@
|
||||
<template>
|
||||
<transition name="slide-fade" appear>
|
||||
<div>
|
||||
<h1 class="mb-3">{{ pageName }}</h1>
|
||||
<form @submit.prevent="submit">
|
||||
<div class="shadow-box">
|
||||
<div class="row">
|
||||
<div class="col-xl-10">
|
||||
<!-- Title -->
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">{{ $t("Title") }}</label>
|
||||
<input
|
||||
id="name" v-model="maintenance.title" type="text" class="form-control"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="my-3">
|
||||
<label for="description" class="form-label">{{ $t("Description") }}</label>
|
||||
<textarea
|
||||
id="description" v-model="maintenance.description" class="form-control"
|
||||
></textarea>
|
||||
<div class="form-text">
|
||||
{{ $t("markdownSupported") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Affected Monitors -->
|
||||
<h2 class="mt-5">{{ $t("Affected Monitors") }}</h2>
|
||||
{{ $t("affectedMonitorsDescription") }}
|
||||
|
||||
<div class="my-3">
|
||||
<VueMultiselect
|
||||
id="affected_monitors"
|
||||
v-model="affectedMonitors"
|
||||
:options="affectedMonitorsOptions"
|
||||
track-by="id"
|
||||
label="name"
|
||||
:multiple="true"
|
||||
:close-on-select="false"
|
||||
:clear-on-select="false"
|
||||
:preserve-search="true"
|
||||
:placeholder="$t('Pick Affected Monitors...')"
|
||||
:preselect-first="false"
|
||||
:max-height="600"
|
||||
:taggable="false"
|
||||
></VueMultiselect>
|
||||
</div>
|
||||
|
||||
<!-- Status pages to display maintenance info on -->
|
||||
<h2 class="mt-5">{{ $t("Status Pages") }}</h2>
|
||||
{{ $t("affectedStatusPages") }}
|
||||
|
||||
<div class="my-3">
|
||||
<!-- Show on all pages -->
|
||||
<div class="form-check mb-2">
|
||||
<input
|
||||
id="show-on-all-pages" v-model="showOnAllPages" class="form-check-input"
|
||||
type="checkbox"
|
||||
>
|
||||
<label class="form-check-label" for="show-powered-by">{{
|
||||
$t("All Status Pages")
|
||||
}}</label>
|
||||
</div>
|
||||
|
||||
<div v-if="!showOnAllPages">
|
||||
<VueMultiselect
|
||||
id="selected_status_pages"
|
||||
v-model="selectedStatusPages"
|
||||
:options="selectedStatusPagesOptions"
|
||||
track-by="id"
|
||||
label="name"
|
||||
:multiple="true"
|
||||
:close-on-select="false"
|
||||
:clear-on-select="false"
|
||||
:preserve-search="true"
|
||||
:placeholder="$t('Select status pages...')"
|
||||
:preselect-first="false"
|
||||
:max-height="600"
|
||||
:taggable="false"
|
||||
></VueMultiselect>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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="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 -->
|
||||
<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"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Recurring - Interval -->
|
||||
<template v-if="maintenance.strategy === 'recurring-interval'">
|
||||
<div class="my-3">
|
||||
<label for="interval-day" class="form-label">
|
||||
{{ $t("recurringInterval") }}
|
||||
|
||||
<template v-if="maintenance.intervalDay >= 1">
|
||||
({{
|
||||
$tc("recurringIntervalMessage", maintenance.intervalDay, [
|
||||
maintenance.intervalDay
|
||||
])
|
||||
}})
|
||||
</template>
|
||||
</label>
|
||||
<input id="interval-day" v-model="maintenance.intervalDay" type="number" class="form-control" required min="1" max="3650" step="1">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Recurring - Weekday -->
|
||||
<template v-if="maintenance.strategy === 'recurring-weekday'">
|
||||
<div class="my-3">
|
||||
<label for="interval-day" class="form-label">
|
||||
{{ $t("dayOfWeek") }}
|
||||
</label>
|
||||
|
||||
<!-- Weekday Picker -->
|
||||
<div class="weekday-picker">
|
||||
<div v-for="(weekday, index) in weekdays" :key="index">
|
||||
<label class="form-check-label" :for="weekday.id">{{ $t(weekday.langKey) }}</label>
|
||||
<div class="form-check-inline"><input :id="weekday.id" v-model="maintenance.weekdays" type="checkbox" :value="weekday.value" class="form-check-input"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Recurring - Day of month -->
|
||||
<template v-if="maintenance.strategy === 'recurring-day-of-month'">
|
||||
<div class="my-3">
|
||||
<label for="interval-day" class="form-label">
|
||||
{{ $t("dayOfMonth") }}
|
||||
</label>
|
||||
|
||||
<!-- Day Picker -->
|
||||
<div class="day-picker">
|
||||
<div v-for="index in 31" :key="index">
|
||||
<label class="form-check-label" :for="'day' + index">{{ index }}</label>
|
||||
<div class="form-check-inline">
|
||||
<input :id="'day' + index" v-model="maintenance.daysOfMonth" type="checkbox" :value="index" class="form-check-input">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 mb-2">{{ $t("lastDay") }}</div>
|
||||
|
||||
<div v-for="(lastDay, index) in lastDays" :key="index" class="form-check">
|
||||
<input :id="lastDay.langKey" v-model="maintenance.daysOfMonth" type="checkbox" :value="lastDay.value" class="form-check-input">
|
||||
<label class="form-check-label" :for="lastDay.langKey">
|
||||
{{ $t(lastDay.langKey) }}
|
||||
</label>
|
||||
</div>
|
||||
</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">
|
||||
<label class="form-label">{{ $t("Maintenance Time Window of a Day") }}</label>
|
||||
<Datepicker
|
||||
v-model="maintenance.timeRange"
|
||||
:dark="$root.isDark"
|
||||
timePicker
|
||||
disableTimeRangeValidation range
|
||||
/>
|
||||
</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
|
||||
/>
|
||||
</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>
|
||||
</form>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { useToast } from "vue-toastification";
|
||||
import VueMultiselect from "vue-multiselect";
|
||||
import dayjs from "dayjs";
|
||||
import Datepicker from "@vuepic/vue-datepicker";
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
components: {
|
||||
VueMultiselect,
|
||||
Datepicker
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
processing: false,
|
||||
maintenance: {},
|
||||
affectedMonitors: [],
|
||||
affectedMonitorsOptions: [],
|
||||
showOnAllPages: false,
|
||||
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: [
|
||||
{
|
||||
id: "weekday1",
|
||||
langKey: "weekdayShortMon",
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
id: "weekday2",
|
||||
langKey: "weekdayShortTue",
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
id: "weekday3",
|
||||
langKey: "weekdayShortWed",
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
id: "weekday4",
|
||||
langKey: "weekdayShortThu",
|
||||
value: 4,
|
||||
},
|
||||
{
|
||||
id: "weekday5",
|
||||
langKey: "weekdayShortFri",
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
id: "weekday6",
|
||||
langKey: "weekdayShortSat",
|
||||
value: 6,
|
||||
},
|
||||
{
|
||||
id: "weekday0",
|
||||
langKey: "weekdayShortSun",
|
||||
value: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
selectedStatusPagesOptions() {
|
||||
return Object.values(this.$root.statusPageList).map(statusPage => {
|
||||
return {
|
||||
id: statusPage.id,
|
||||
name: statusPage.title
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
pageName() {
|
||||
return this.$t((this.isAdd) ? "Schedule Maintenance" : "Edit Maintenance");
|
||||
},
|
||||
|
||||
isAdd() {
|
||||
return this.$route.path === "/add-maintenance";
|
||||
},
|
||||
|
||||
isEdit() {
|
||||
return this.$route.path.startsWith("/maintenance/edit");
|
||||
},
|
||||
|
||||
},
|
||||
watch: {
|
||||
"$route.fullPath"() {
|
||||
this.init();
|
||||
},
|
||||
|
||||
neverEnd(value) {
|
||||
if (value) {
|
||||
this.maintenance.recurringEndDate = "";
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
|
||||
this.$root.getMonitorList((res) => {
|
||||
if (res.ok) {
|
||||
Object.values(this.$root.monitorList).map(monitor => {
|
||||
this.affectedMonitorsOptions.push({
|
||||
id: monitor.id,
|
||||
name: monitor.name,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
/** Initialise page */
|
||||
init() {
|
||||
this.affectedMonitors = [];
|
||||
this.selectedStatusPages = [];
|
||||
|
||||
if (this.isAdd) {
|
||||
this.maintenance = {
|
||||
title: "",
|
||||
description: "",
|
||||
strategy: "single",
|
||||
active: 1,
|
||||
intervalDay: 1,
|
||||
dateRange: [ this.minDate ],
|
||||
timeRange: [{
|
||||
hours: 2,
|
||||
minutes: 0,
|
||||
}, {
|
||||
hours: 3,
|
||||
minutes: 0,
|
||||
}],
|
||||
weekdays: [],
|
||||
daysOfMonth: [],
|
||||
};
|
||||
} else if (this.isEdit) {
|
||||
this.$root.getSocket().emit("getMaintenance", this.$route.params.id, (res) => {
|
||||
if (res.ok) {
|
||||
this.maintenance = res.maintenance;
|
||||
|
||||
this.$root.getSocket().emit("getMonitorMaintenance", this.$route.params.id, (res) => {
|
||||
if (res.ok) {
|
||||
Object.values(res.monitors).map(monitor => {
|
||||
this.affectedMonitors.push(monitor);
|
||||
});
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
|
||||
this.$root.getSocket().emit("getMaintenanceStatusPage", this.$route.params.id, (res) => {
|
||||
if (res.ok) {
|
||||
Object.values(res.statusPages).map(statusPage => {
|
||||
this.selectedStatusPages.push({
|
||||
id: statusPage.id,
|
||||
name: statusPage.title
|
||||
});
|
||||
});
|
||||
|
||||
this.showOnAllPages = Object.values(res.statusPages).length === this.selectedStatusPagesOptions.length;
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/** Create new maintenance */
|
||||
async submit() {
|
||||
this.processing = true;
|
||||
|
||||
if (this.affectedMonitors.length === 0) {
|
||||
toast.error(this.$t("atLeastOneMonitor"));
|
||||
return this.processing = false;
|
||||
}
|
||||
|
||||
if (this.isAdd) {
|
||||
this.$root.addMaintenance(this.maintenance, async (res) => {
|
||||
if (res.ok) {
|
||||
await this.addMonitorMaintenance(res.maintenanceID, async () => {
|
||||
await this.addMaintenanceStatusPage(res.maintenanceID, () => {
|
||||
toast.success(res.msg);
|
||||
this.processing = false;
|
||||
this.$root.getMaintenanceList();
|
||||
this.$router.push("/maintenance");
|
||||
});
|
||||
});
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
this.processing = false;
|
||||
}
|
||||
|
||||
});
|
||||
} else {
|
||||
this.$root.getSocket().emit("editMaintenance", this.maintenance, async (res) => {
|
||||
if (res.ok) {
|
||||
await this.addMonitorMaintenance(res.maintenanceID, async () => {
|
||||
await this.addMaintenanceStatusPage(res.maintenanceID, () => {
|
||||
this.processing = false;
|
||||
this.$root.toastRes(res);
|
||||
this.init();
|
||||
this.$router.push("/maintenance");
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.processing = false;
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
toast.error(res.msg);
|
||||
} else {
|
||||
this.$root.getMonitorList();
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
toast.error(res.msg);
|
||||
} else {
|
||||
this.$root.getMaintenanceList();
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.shadow-box {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.dark-calendar::-webkit-calendar-picker-indicator {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.weekday-picker {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 40px;
|
||||
|
||||
.form-check-inline {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.day-picker {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 40px;
|
||||
|
||||
.form-check-inline {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
@@ -11,27 +11,75 @@
|
||||
<div class="my-3">
|
||||
<label for="type" class="form-label">{{ $t("Monitor Type") }}</label>
|
||||
<select id="type" v-model="monitor.type" class="form-select">
|
||||
<option value="http">
|
||||
HTTP(s)
|
||||
</option>
|
||||
<option value="port">
|
||||
TCP Port
|
||||
</option>
|
||||
<option value="ping">
|
||||
Ping
|
||||
</option>
|
||||
<option value="keyword">
|
||||
HTTP(s) - {{ $t("Keyword") }}
|
||||
</option>
|
||||
<option value="dns">
|
||||
DNS
|
||||
</option>
|
||||
<option value="push">
|
||||
Push
|
||||
</option>
|
||||
<option value="steam">
|
||||
Steam Game Server
|
||||
</option>
|
||||
<optgroup :label="$t('General Monitor Type')">
|
||||
<option value="http">
|
||||
HTTP(s)
|
||||
</option>
|
||||
<option value="port">
|
||||
TCP Port
|
||||
</option>
|
||||
<option value="ping">
|
||||
Ping
|
||||
</option>
|
||||
<option value="keyword">
|
||||
HTTP(s) - {{ $t("Keyword") }}
|
||||
</option>
|
||||
<option value="grpc-keyword">
|
||||
gRPC(s) - {{ $t("Keyword") }}
|
||||
</option>
|
||||
<option value="dns">
|
||||
DNS
|
||||
</option>
|
||||
<option value="docker">
|
||||
{{ $t("Docker Container") }}
|
||||
</option>
|
||||
</optgroup>
|
||||
|
||||
<optgroup :label="$t('Passive Monitor Type')">
|
||||
<option value="push">
|
||||
Push
|
||||
</option>
|
||||
</optgroup>
|
||||
|
||||
<optgroup :label="$t('Specific Monitor Type')">
|
||||
<option value="steam">
|
||||
{{ $t("Steam Game Server") }}
|
||||
</option>
|
||||
<option value="gamedig">
|
||||
GameDig
|
||||
</option>
|
||||
<option value="mqtt">
|
||||
MQTT
|
||||
</option>
|
||||
<option value="sqlserver">
|
||||
Microsoft SQL Server
|
||||
</option>
|
||||
<option value="postgres">
|
||||
PostgreSQL
|
||||
</option>
|
||||
<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>
|
||||
|
||||
@@ -48,23 +96,29 @@
|
||||
</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>
|
||||
|
||||
<!-- 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>
|
||||
</div>
|
||||
|
||||
<!-- Push URL -->
|
||||
<div v-if="monitor.type === 'push' " class="my-3">
|
||||
<label for="push-url" class="form-label">{{ $t("PushUrl") }}</label>
|
||||
<CopyableInput id="push-url" v-model="pushURL" type="url" disabled="disabled" />
|
||||
<div class="form-text">
|
||||
{{ $t("needPushEvery", [monitor.interval]) }}<br />
|
||||
{{ $t("pushOptionalParams", ["msg, ping"]) }}
|
||||
{{ $t("pushOptionalParams", ["status, msg, ping"]) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Keyword -->
|
||||
<div v-if="monitor.type === '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">
|
||||
@@ -72,16 +126,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 only -->
|
||||
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam'" class="my-3">
|
||||
<!-- 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 === '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 Type -->
|
||||
<div v-if="monitor.type === 'port' || monitor.type === 'steam'" class="my-3">
|
||||
<!-- For TCP Port / Steam / MQTT / Radius Type -->
|
||||
<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>
|
||||
@@ -93,7 +158,16 @@
|
||||
<label for="dns_resolve_server" class="form-label">{{ $t("Resolver Server") }}</label>
|
||||
<input id="dns_resolve_server" v-model="monitor.dns_resolve_server" type="text" class="form-control" :pattern="ipRegex" required>
|
||||
<div class="form-text">
|
||||
{{ $t("resoverserverDescription") }}
|
||||
{{ $t("resolverserverDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Port -->
|
||||
<div 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 class="form-text">
|
||||
{{ $t("dnsPortDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -121,10 +195,137 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Docker Container Name / ID -->
|
||||
<!-- For Docker Type -->
|
||||
<div v-if="monitor.type === 'docker'" class="my-3">
|
||||
<label for="docker_container" class="form-label">{{ $t("Container Name / ID") }}</label>
|
||||
<input id="docker_container" v-model="monitor.docker_container" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<!-- Docker Host -->
|
||||
<!-- For Docker Type -->
|
||||
<div v-if="monitor.type === 'docker'" class="my-3">
|
||||
<h2 class="mb-2">{{ $t("Docker Host") }}</h2>
|
||||
<p v-if="$root.dockerHostList.length === 0">
|
||||
{{ $t("Not available, please setup.") }}
|
||||
</p>
|
||||
|
||||
<div v-else class="mb-3">
|
||||
<label for="docker-host" class="form-label">{{ $t("Docker Host") }}</label>
|
||||
<select id="docket-host" v-model="monitor.docker_host" class="form-select">
|
||||
<option v-for="host in $root.dockerHostList" :key="host.id" :value="host.id">{{ host.name }}</option>
|
||||
</select>
|
||||
<a href="#" @click="$refs.dockerHostDialog.show(monitor.docker_host)">{{ $t("Edit") }}</a>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary me-2" type="button" @click="$refs.dockerHostDialog.show()">
|
||||
{{ $t("Setup Docker Host") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- MQTT -->
|
||||
<!-- For MQTT Type -->
|
||||
<template v-if="monitor.type === 'mqtt'">
|
||||
<div class="my-3">
|
||||
<label for="mqttUsername" class="form-label">MQTT {{ $t("Username") }}</label>
|
||||
<input id="mqttUsername" v-model="monitor.mqttUsername" type="text" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="mqttPassword" class="form-label">MQTT {{ $t("Password") }}</label>
|
||||
<input id="mqttPassword" v-model="monitor.mqttPassword" type="password" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="mqttTopic" class="form-label">MQTT {{ $t("Topic") }}</label>
|
||||
<input id="mqttTopic" v-model="monitor.mqttTopic" type="text" class="form-control" required>
|
||||
<div class="form-text">
|
||||
{{ $t("topicExplanation") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="mqttSuccessMessage" class="form-label">MQTT {{ $t("successMessage") }}</label>
|
||||
<input id="mqttSuccessMessage" v-model="monitor.mqttSuccessMessage" type="text" class="form-control">
|
||||
<div class="form-text">
|
||||
{{ $t("successMessageExplanation") }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="monitor.type === 'radius'">
|
||||
<div class="my-3">
|
||||
<label for="radius_username" class="form-label">Radius {{ $t("Username") }}</label>
|
||||
<input id="radius_username" v-model="monitor.radiusUsername" type="text" class="form-control" required />
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="radius_password" class="form-label">Radius {{ $t("Password") }}</label>
|
||||
<input id="radius_password" v-model="monitor.radiusPassword" type="password" class="form-control" required />
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="radius_secret" class="form-label">{{ $t("RadiusSecret") }}</label>
|
||||
<input id="radius_secret" v-model="monitor.radiusSecret" type="password" class="form-control" required />
|
||||
<div class="form-text"> {{ $t( "RadiusSecretDescription") }} </div>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="radius_called_station_id" class="form-label">{{ $t("RadiusCalledStationId") }}</label>
|
||||
<input id="radius_called_station_id" v-model="monitor.radiusCalledStationId" type="text" class="form-control" required />
|
||||
<div class="form-text"> {{ $t( "RadiusCalledStationIdDescription") }} </div>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="radius_calling_station_id" class="form-label">{{ $t("RadiusCallingStationId") }}</label>
|
||||
<input id="radius_calling_station_id" v-model="monitor.radiusCallingStationId" type="text" class="form-control" required />
|
||||
<div class="form-text"> {{ $t( "RadiusCallingStationIdDescription") }} </div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- SQL Server / PostgreSQL / MySQL -->
|
||||
<template v-if="monitor.type === 'sqlserver' || monitor.type === 'postgres' || monitor.type === 'mysql'">
|
||||
<div class="my-3">
|
||||
<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>">
|
||||
</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">
|
||||
</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">
|
||||
</template>
|
||||
</div>
|
||||
<div class="my-3">
|
||||
<label for="sqlQuery" class="form-label">{{ $t("Query") }}</label>
|
||||
<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" placeholder="redis://user:password@host:port">
|
||||
</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" placeholder="mongodb://username:password@host:port/database">
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Interval -->
|
||||
<div class="my-3">
|
||||
<label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label>
|
||||
<input id="interval" v-model="monitor.interval" type="number" class="form-control" required min="20" step="1">
|
||||
<input id="interval" v-model="monitor.interval" type="number" class="form-control" required :min="minInterval" step="1" :max="maxInterval">
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
@@ -140,11 +341,29 @@
|
||||
{{ $t("Heartbeat Retry Interval") }}
|
||||
<span>({{ $t("retryCheckEverySecond", [ monitor.retryInterval ]) }})</span>
|
||||
</label>
|
||||
<input id="retry-interval" v-model="monitor.retryInterval" type="number" class="form-control" required min="20" step="1">
|
||||
<input id="retry-interval" v-model="monitor.retryInterval" type="number" class="form-control" required :min="minInterval" step="1">
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="resend-interval" class="form-label">
|
||||
{{ $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>
|
||||
<input id="resend-interval" v-model="monitor.resendInterval" type="number" class="form-control" required min="0" step="1">
|
||||
</div>
|
||||
|
||||
<h2 v-if="monitor.type !== 'push'" class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
|
||||
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3 form-check">
|
||||
<input id="expiry-notification" v-model="monitor.expiryNotification" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label" for="expiry-notification">
|
||||
{{ $t("Certificate Expiry Notification") }}
|
||||
</label>
|
||||
<div class="form-text">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3 form-check">
|
||||
<input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value="">
|
||||
<label class="form-check-label" for="ignore-tls">
|
||||
@@ -162,8 +381,14 @@
|
||||
</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' ">
|
||||
<template v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'grpc-keyword' ">
|
||||
<div class="my-3">
|
||||
<label for="maxRedirects" class="form-label">{{ $t("Max. Redirects") }}</label>
|
||||
<input id="maxRedirects" v-model="monitor.maxredirects" type="number" class="form-control" required min="0" step="1">
|
||||
@@ -198,10 +423,6 @@
|
||||
<div class="my-3">
|
||||
<tags-manager ref="tagsManager" :pre-selected-tags="monitor.tags"></tags-manager>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 mb-1">
|
||||
<button id="monitor-submit-btn" class="btn btn-primary" type="submit" :disabled="processing">{{ $t("Save") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
@@ -228,6 +449,34 @@
|
||||
{{ $t("Setup Notification") }}
|
||||
</button>
|
||||
|
||||
<!-- Proxies -->
|
||||
<div v-if="monitor.type === 'http' || monitor.type === 'keyword'">
|
||||
<h2 class="mt-5 mb-2">{{ $t("Proxy") }}</h2>
|
||||
<p v-if="$root.proxyList.length === 0">
|
||||
{{ $t("Not available, please setup.") }}
|
||||
</p>
|
||||
|
||||
<div v-if="$root.proxyList.length > 0" class="form-check my-3">
|
||||
<input id="proxy-disable" v-model="monitor.proxyId" :value="null" name="proxy" class="form-check-input" type="radio">
|
||||
<label class="form-check-label" for="proxy-disable">{{ $t("No Proxy") }}</label>
|
||||
</div>
|
||||
|
||||
<div v-for="proxy in $root.proxyList" :key="proxy.id" class="form-check my-3">
|
||||
<input :id="`proxy-${proxy.id}`" v-model="monitor.proxyId" :value="proxy.id" name="proxy" class="form-check-input" type="radio">
|
||||
|
||||
<label class="form-check-label" :for="`proxy-${proxy.id}`">
|
||||
{{ proxy.host }}:{{ proxy.port }} ({{ proxy.protocol }})
|
||||
<a href="#" @click="$refs.proxyDialog.show(proxy.id)">{{ $t("Edit") }}</a>
|
||||
</label>
|
||||
|
||||
<span v-if="proxy.default === true" class="badge bg-primary ms-2">{{ $t("default") }}</span>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary me-2" type="button" @click="$refs.proxyDialog.show()">
|
||||
{{ $t("Setup Proxy") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- HTTP Options -->
|
||||
<template v-if="monitor.type === 'http' || monitor.type === 'keyword' ">
|
||||
<h2 class="mt-5 mb-2">{{ $t("HTTP Options") }}</h2>
|
||||
@@ -260,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>
|
||||
@@ -272,50 +530,139 @@
|
||||
<textarea id="headers" v-model="monitor.headers" class="form-control" :placeholder="headersPlaceholder"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- HTTP Basic Auth -->
|
||||
<h4 class="mt-5 mb-2">{{ $t("HTTP Basic Auth") }}</h4>
|
||||
<!-- HTTP Auth -->
|
||||
<h4 class="mt-5 mb-2">{{ $t("Authentication") }}</h4>
|
||||
|
||||
<!-- Method -->
|
||||
<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')">
|
||||
<label for="method" class="form-label">{{ $t("Method") }}</label>
|
||||
<select id="method" v-model="monitor.authMethod" class="form-select">
|
||||
<option :value="null">
|
||||
{{ $t("None") }}
|
||||
</option>
|
||||
<option value="basic">
|
||||
{{ $t("HTTP Basic Auth") }}
|
||||
</option>
|
||||
<option value="ntlm">
|
||||
NTLM
|
||||
</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' ">
|
||||
<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')">
|
||||
</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')">
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- gRPC Options -->
|
||||
<template v-if="monitor.type === 'grpc-keyword' ">
|
||||
<!-- Proto service enable TLS -->
|
||||
<h2 class="mt-5 mb-2">{{ $t("GRPC Options") }}</h2>
|
||||
<div class="my-3 form-check">
|
||||
<input id="grpc-enable-tls" v-model="monitor.grpcEnableTls" class="form-check-input" type="checkbox" value="">
|
||||
<label class="form-check-label" for="grpc-enable-tls">
|
||||
{{ $t("Enable TLS") }}
|
||||
</label>
|
||||
<div class="form-text">
|
||||
{{ $t("enableGRPCTls") }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Proto service name data -->
|
||||
<div class="my-3">
|
||||
<label for="protobuf" class="form-label">{{ $t("Proto Service Name") }}</label>
|
||||
<input id="name" v-model="monitor.grpcServiceName" type="text" class="form-control" :placeholder="protoServicePlaceholder" required>
|
||||
</div>
|
||||
|
||||
<!-- Proto method data -->
|
||||
<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" class="form-control" :placeholder="$t('Password')">
|
||||
<label for="protobuf" class="form-label">{{ $t("Proto Method") }}</label>
|
||||
<input id="name" v-model="monitor.grpcMethod" type="text" class="form-control" :placeholder="protoMethodPlaceholder" required>
|
||||
<div class="form-text">
|
||||
{{ $t("grpcMethodDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Proto data -->
|
||||
<div class="my-3">
|
||||
<label for="protobuf" class="form-label">{{ $t("Proto Content") }}</label>
|
||||
<textarea id="protobuf" v-model="monitor.grpcProtobuf" class="form-control" :placeholder="protoBufDataPlaceholder"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="my-3">
|
||||
<label for="body" class="form-label">{{ $t("Body") }}</label>
|
||||
<textarea id="body" v-model="monitor.grpcBody" class="form-control" :placeholder="bodyPlaceholder"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Metadata: temporary disable waiting for next PR allow to send gRPC with metadata -->
|
||||
<template v-if="false">
|
||||
<div class="my-3">
|
||||
<label for="metadata" class="form-label">{{ $t("Metadata") }}</label>
|
||||
<textarea id="metadata" v-model="monitor.grpcMetadata" class="form-control" :placeholder="headersPlaceholder"></textarea>
|
||||
</div>
|
||||
</template>
|
||||
</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>
|
||||
|
||||
<NotificationDialog ref="notificationDialog" @added="addedNotification" />
|
||||
<DockerHostDialog ref="dockerHostDialog" @added="addedDockerHost" />
|
||||
<ProxyDialog ref="proxyDialog" @added="addedProxy" />
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NotificationDialog from "../components/NotificationDialog.vue";
|
||||
import TagsManager from "../components/TagsManager.vue";
|
||||
import CopyableInput from "../components/CopyableInput.vue";
|
||||
|
||||
import { useToast } from "vue-toastification";
|
||||
import VueMultiselect from "vue-multiselect";
|
||||
import { genSecret, isDev } from "../util.ts";
|
||||
import { useToast } from "vue-toastification";
|
||||
import CopyableInput from "../components/CopyableInput.vue";
|
||||
import NotificationDialog from "../components/NotificationDialog.vue";
|
||||
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();
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ProxyDialog,
|
||||
CopyableInput,
|
||||
NotificationDialog,
|
||||
DockerHostDialog,
|
||||
TagsManager,
|
||||
VueMultiselect,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
minInterval: MIN_INTERVAL_SECOND,
|
||||
maxInterval: MAX_INTERVAL_SECOND,
|
||||
processing: false,
|
||||
monitor: {
|
||||
notificationIDList: {},
|
||||
@@ -323,11 +670,9 @@ 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,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -343,37 +688,112 @@ 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");
|
||||
},
|
||||
|
||||
pushURL() {
|
||||
return this.$root.baseURL + "/api/push/" + this.monitor.pushToken + "?msg=OK&ping=";
|
||||
return this.$root.baseURL + "/api/push/" + this.monitor.pushToken + "?status=up&msg=OK&ping=";
|
||||
},
|
||||
|
||||
protoServicePlaceholder() {
|
||||
return this.$t("Example:", [ "Health" ]);
|
||||
},
|
||||
|
||||
protoMethodPlaceholder() {
|
||||
return this.$t("Example:", [ "check" ]);
|
||||
},
|
||||
|
||||
protoBufDataPlaceholder() {
|
||||
return this.$t("Example:", [ `
|
||||
syntax = "proto3";
|
||||
|
||||
package grpc.health.v1;
|
||||
|
||||
service Health {
|
||||
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
|
||||
rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
|
||||
}
|
||||
|
||||
message HealthCheckRequest {
|
||||
string service = 1;
|
||||
}
|
||||
|
||||
message HealthCheckResponse {
|
||||
enum ServingStatus {
|
||||
UNKNOWN = 0;
|
||||
SERVING = 1;
|
||||
NOT_SERVING = 2;
|
||||
SERVICE_UNKNOWN = 3; // Used only by the Watch method.
|
||||
}
|
||||
ServingStatus status = 1;
|
||||
}
|
||||
` ]);
|
||||
},
|
||||
bodyPlaceholder() {
|
||||
return this.$t("Example:", [`
|
||||
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"
|
||||
}`]);
|
||||
}` ]);
|
||||
},
|
||||
|
||||
headersPlaceholder() {
|
||||
return this.$t("Example:", [`
|
||||
return this.$t("Example:", [ `
|
||||
{
|
||||
"HeaderName": "HeaderValue"
|
||||
}`]);
|
||||
}
|
||||
}` ]);
|
||||
},
|
||||
|
||||
currentGameObject() {
|
||||
if (this.gameList) {
|
||||
for (let game of this.gameList) {
|
||||
if (game.keys[0] === this.monitor.game) {
|
||||
return game;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
},
|
||||
watch: {
|
||||
"$root.proxyList"() {
|
||||
if (this.isAdd) {
|
||||
if (this.$root.proxyList && !this.monitor.proxyId) {
|
||||
const proxy = this.$root.proxyList.find(proxy => proxy.default);
|
||||
|
||||
if (proxy) {
|
||||
this.monitor.proxyId = proxy.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"$route.fullPath"() {
|
||||
this.init();
|
||||
@@ -392,6 +812,35 @@ export default {
|
||||
this.monitor.pushToken = genSecret(10);
|
||||
}
|
||||
}
|
||||
|
||||
// Set default port for DNS if not already defined
|
||||
if (! this.monitor.port || this.monitor.port === "53" || this.monitor.port === "1812") {
|
||||
if (this.monitor.type === "dns") {
|
||||
this.monitor.port = "53";
|
||||
} else if (this.monitor.type === "radius") {
|
||||
this.monitor.port = "1812";
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
},
|
||||
@@ -427,6 +876,7 @@ export default {
|
||||
this.dnsresolvetypeOptions = dnsresolvetypeOptions;
|
||||
},
|
||||
methods: {
|
||||
/** Initialize the edit monitor form */
|
||||
init() {
|
||||
if (this.isAdd) {
|
||||
|
||||
@@ -437,26 +887,58 @@ export default {
|
||||
method: "GET",
|
||||
interval: 60,
|
||||
retryInterval: this.interval,
|
||||
resendInterval: 0,
|
||||
maxretries: 0,
|
||||
notificationIDList: {},
|
||||
ignoreTls: false,
|
||||
upsideDown: false,
|
||||
packetSize: 56,
|
||||
expiryNotification: false,
|
||||
maxredirects: 10,
|
||||
accepted_statuscodes: ["200-299"],
|
||||
accepted_statuscodes: [ "200-299" ],
|
||||
dns_resolve_type: "A",
|
||||
dns_resolve_server: "1.1.1.1",
|
||||
docker_container: "",
|
||||
docker_host: null,
|
||||
proxyId: null,
|
||||
mqttUsername: "",
|
||||
mqttPassword: "",
|
||||
mqttTopic: "",
|
||||
mqttSuccessMessage: "",
|
||||
authMethod: null,
|
||||
httpBodyEncoding: "json"
|
||||
};
|
||||
|
||||
if (this.$root.proxyList && !this.monitor.proxyId) {
|
||||
const proxy = this.$root.proxyList.find(proxy => proxy.default);
|
||||
|
||||
if (proxy) {
|
||||
this.monitor.proxyId = proxy.id;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.$root.notificationList.length; i++) {
|
||||
if (this.$root.notificationList[i].isDefault == true) {
|
||||
if (this.$root.notificationList[i].isDefault === true) {
|
||||
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) {
|
||||
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.monitor.tags = undefined; // FIXME: Cloning tags does not work yet
|
||||
}
|
||||
|
||||
// Handling for monitors that are created before 1.7.0
|
||||
if (this.monitor.retryInterval === 0) {
|
||||
this.monitor.retryInterval = this.monitor.interval;
|
||||
@@ -469,8 +951,12 @@ export default {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate form input
|
||||
* @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) {
|
||||
@@ -489,7 +975,12 @@ export default {
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Submit the form data for processing
|
||||
* @returns {void}
|
||||
*/
|
||||
async submit() {
|
||||
|
||||
this.processing = true;
|
||||
|
||||
if (!this.isInputValid()) {
|
||||
@@ -497,16 +988,28 @@ export default {
|
||||
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) {
|
||||
@@ -533,21 +1036,61 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
// Added a Notification Event
|
||||
// Enable it if the notification is added in EditMonitor.vue
|
||||
/**
|
||||
* Added a Notification Event
|
||||
* Enable it if the notification is added in EditMonitor.vue
|
||||
* @param {number} id ID of notification to add
|
||||
*/
|
||||
addedNotification(id) {
|
||||
this.monitor.notificationIDList[id] = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Added a Proxy Event
|
||||
* Enable it if the proxy is added in EditMonitor.vue
|
||||
* @param {number} id ID of proxy to add
|
||||
*/
|
||||
addedProxy(id) {
|
||||
this.monitor.proxyId = id;
|
||||
},
|
||||
|
||||
// Added a Docker Host Event
|
||||
// Enable it if the Docker Host is added in EditMonitor.vue
|
||||
addedDockerHost(id) {
|
||||
this.monitor.docker_host = id;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
$padding: 20px;
|
||||
|
||||
.shadow-box {
|
||||
padding: 20px;
|
||||
padding-top: $padding;
|
||||
padding-bottom: 0;
|
||||
padding-right: $padding;
|
||||
padding-left: $padding;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.fixed-bottom-bar {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
margin-left: -$padding;
|
||||
margin-right: -$padding;
|
||||
z-index: 100;
|
||||
background-color: rgba(white, 0.2);
|
||||
backdrop-filter: blur(2px);
|
||||
border-radius: 0 0 10px 10px;
|
||||
|
||||
.dark & {
|
||||
background-color: rgba($dark-header-bg, 0.9);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,19 +1,45 @@
|
||||
<template>
|
||||
<div></div>
|
||||
<div>
|
||||
<StatusPage v-if="statusPageSlug" :override-slug="statusPageSlug" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import StatusPage from "./StatusPage.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
StatusPage,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
statusPageSlug: null,
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
let entryPage = (await axios.get("/api/entry-page")).data;
|
||||
|
||||
if (entryPage === "statusPage") {
|
||||
this.$router.push("/status");
|
||||
// There are only 2 cases that could come in here.
|
||||
// 1. Matched status Page domain name
|
||||
// 2. Vue Frontend Dev
|
||||
let res = (await axios.get("/api/entry-page")).data;
|
||||
|
||||
if (res.type === "statusPageMatchedDomain") {
|
||||
this.statusPageSlug = res.statusPageSlug;
|
||||
this.$root.forceStatusPageTheme = true;
|
||||
|
||||
} else if (res.type === "entryPage") { // Dev only. For production, the logic is in the server side
|
||||
const entryPage = res.entryPage;
|
||||
|
||||
if (entryPage === "statusPage") {
|
||||
this.$router.push("/status");
|
||||
} else {
|
||||
this.$router.push("/dashboard");
|
||||
}
|
||||
} else {
|
||||
this.$router.push("/dashboard");
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
};
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<transition name="slide-fade" appear>
|
||||
<MonitorList />
|
||||
<MonitorList :scrollbar="true" />
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
@@ -11,6 +11,14 @@ export default {
|
||||
components: {
|
||||
MonitorList,
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars";
|
||||
|
||||
.shadow-box {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
164
src/pages/MaintenanceDetails.vue
Normal file
164
src/pages/MaintenanceDetails.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<transition name="slide-fade" appear>
|
||||
<div v-if="maintenance">
|
||||
<h1>{{ maintenance.title }}</h1>
|
||||
<p class="url">
|
||||
<span>{{ $t("Start") }}: {{ $root.datetimeMaintenance(maintenance.start_date) }}</span>
|
||||
<br>
|
||||
<span>{{ $t("End") }}: {{ $root.datetimeMaintenance(maintenance.end_date) }}</span>
|
||||
</p>
|
||||
|
||||
<div class="functions" style="margin-top: 10px;">
|
||||
<router-link :to=" '/maintenance/edit/' + maintenance.id " class="btn btn-secondary">
|
||||
<font-awesome-icon icon="edit" /> {{ $t("Edit") }}
|
||||
</router-link>
|
||||
<button class="btn btn-danger" @click="deleteDialog">
|
||||
<font-awesome-icon icon="trash" /> {{ $t("Delete") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label for="description" class="form-label" style="margin-top: 20px;">{{ $t("Description") }}</label>
|
||||
<textarea id="description" v-model="maintenance.description" class="form-control" disabled></textarea>
|
||||
|
||||
<label for="affected_monitors" class="form-label" style="margin-top: 20px;">{{ $t("Affected Monitors") }}</label>
|
||||
<br>
|
||||
<button v-for="monitor in affectedMonitors" :key="monitor.id" class="btn btn-monitor" style="margin: 5px; cursor: auto; color: white; font-weight: 500;">
|
||||
{{ monitor }}
|
||||
</button>
|
||||
<br />
|
||||
|
||||
<label for="selected_status_pages" class="form-label" style="margin-top: 20px;">{{ $t("Show this Maintenance Message on which Status Pages") }}</label>
|
||||
<br>
|
||||
<button v-for="statusPage in selectedStatusPages" :key="statusPage.id" class="btn btn-monitor" style="margin: 5px; cursor: auto; color: white; font-weight: 500;">
|
||||
{{ statusPage }}
|
||||
</button>
|
||||
|
||||
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteMaintenance">
|
||||
{{ $t("deleteMaintenanceMsg") }}
|
||||
</Confirm>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useToast } from "vue-toastification";
|
||||
const toast = useToast();
|
||||
import Confirm from "../components/Confirm.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Confirm,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
affectedMonitors: [],
|
||||
selectedStatusPages: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
maintenance() {
|
||||
let id = this.$route.params.id;
|
||||
return this.$root.maintenanceList[id];
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
},
|
||||
methods: {
|
||||
/** Initialise page */
|
||||
init() {
|
||||
this.$root.getSocket().emit("getMonitorMaintenance", this.$route.params.id, (res) => {
|
||||
if (res.ok) {
|
||||
this.affectedMonitors = Object.values(res.monitors).map(monitor => monitor.name);
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
|
||||
this.$root.getSocket().emit("getMaintenanceStatusPage", this.$route.params.id, (res) => {
|
||||
if (res.ok) {
|
||||
this.selectedStatusPages = Object.values(res.statusPages).map(statusPage => statusPage.title);
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/** Confirm deletion */
|
||||
deleteDialog() {
|
||||
this.$refs.confirmDelete.show();
|
||||
},
|
||||
|
||||
/** Delete maintenance after showing confirmation */
|
||||
deleteMaintenance() {
|
||||
this.$root.deleteMaintenance(this.maintenance.id, (res) => {
|
||||
if (res.ok) {
|
||||
toast.success(res.msg);
|
||||
this.$router.push("/maintenance");
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
@media (max-width: 550px) {
|
||||
.functions {
|
||||
text-align: center;
|
||||
|
||||
button, a {
|
||||
margin-left: 10px !important;
|
||||
margin-right: 10px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
a.btn {
|
||||
padding-left: 25px;
|
||||
padding-right: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.url {
|
||||
color: $primary;
|
||||
margin-bottom: 20px;
|
||||
font-weight: bold;
|
||||
|
||||
a {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
.functions {
|
||||
button, a {
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 100px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.btn-monitor {
|
||||
background-color: #5cdd8b;
|
||||
}
|
||||
|
||||
.dark .btn-monitor {
|
||||
color: #020b05 !important;
|
||||
}
|
||||
|
||||
</style>
|
303
src/pages/ManageMaintenance.vue
Normal file
303
src/pages/ManageMaintenance.vue
Normal file
@@ -0,0 +1,303 @@
|
||||
<template>
|
||||
<transition name="slide-fade" appear>
|
||||
<div>
|
||||
<h1 class="mb-3">
|
||||
{{ $t("Maintenance") }}
|
||||
</h1>
|
||||
|
||||
<div>
|
||||
<router-link to="/add-maintenance" class="btn btn-primary mb-3">
|
||||
<font-awesome-icon icon="plus" /> {{ $t("Schedule Maintenance") }}
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="shadow-box">
|
||||
<span v-if="Object.keys(sortedMaintenanceList).length === 0" class="d-flex align-items-center justify-content-center my-3">
|
||||
{{ $t("No Maintenance") }}
|
||||
</span>
|
||||
|
||||
<div
|
||||
v-for="(item, index) in sortedMaintenanceList"
|
||||
:key="index"
|
||||
class="item"
|
||||
:class="item.status"
|
||||
>
|
||||
<div class="left-part">
|
||||
<div
|
||||
class="circle"
|
||||
></div>
|
||||
<div class="info">
|
||||
<div class="title">{{ item.title }}</div>
|
||||
<div v-if="false">{{ item.description }}</div>
|
||||
<div class="status">
|
||||
{{ $t("maintenanceStatus-" + item.status) }}
|
||||
</div>
|
||||
|
||||
<MaintenanceTime :maintenance="item" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
<router-link v-if="false" :to="maintenanceURL(item.id)" class="btn btn-light">{{ $t("Details") }}</router-link>
|
||||
|
||||
<div class="btn-group" role="group">
|
||||
<button v-if="item.active" class="btn btn-normal" @click="pauseDialog(item.id)">
|
||||
<font-awesome-icon icon="pause" /> {{ $t("Pause") }}
|
||||
</button>
|
||||
|
||||
<button v-if="!item.active" class="btn btn-primary" @click="resumeMaintenance(item.id)">
|
||||
<font-awesome-icon icon="play" /> {{ $t("Resume") }}
|
||||
</button>
|
||||
|
||||
<router-link :to="'/maintenance/edit/' + item.id" class="btn btn-normal">
|
||||
<font-awesome-icon icon="edit" /> {{ $t("Edit") }}
|
||||
</router-link>
|
||||
|
||||
<button class="btn btn-danger" @click="deleteDialog(item.id)">
|
||||
<font-awesome-icon icon="trash" /> {{ $t("Delete") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-3" style="font-size: 13px;">
|
||||
<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">
|
||||
{{ $t("pauseMaintenanceMsg") }}
|
||||
</Confirm>
|
||||
|
||||
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteMaintenance">
|
||||
{{ $t("deleteMaintenanceMsg") }}
|
||||
</Confirm>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getResBaseURL } from "../util-frontend";
|
||||
import { getMaintenanceRelativeURL } from "../util.ts";
|
||||
import Confirm from "../components/Confirm.vue";
|
||||
import MaintenanceTime from "../components/MaintenanceTime.vue";
|
||||
import { useToast } from "vue-toastification";
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MaintenanceTime,
|
||||
Confirm,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedMaintenanceID: undefined,
|
||||
statusOrderList: {
|
||||
"under-maintenance": 1000,
|
||||
"scheduled": 900,
|
||||
"inactive": 800,
|
||||
"ended": 700,
|
||||
"unknown": 0,
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sortedMaintenanceList() {
|
||||
let result = Object.values(this.$root.maintenanceList);
|
||||
|
||||
result.sort((m1, m2) => {
|
||||
if (this.statusOrderList[m1.status] === this.statusOrderList[m2.status]) {
|
||||
return m1.title.localeCompare(m2.title);
|
||||
} else {
|
||||
return this.statusOrderList[m1.status] < this.statusOrderList[m2.status];
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Get the correct URL for the icon
|
||||
* @param {string} icon Path for icon
|
||||
* @returns {string} Correctly formatted path including port numbers
|
||||
*/
|
||||
icon(icon) {
|
||||
if (icon === "/icon.svg") {
|
||||
return icon;
|
||||
} else {
|
||||
return getResBaseURL() + icon;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
toast.success(res.msg);
|
||||
this.$router.push("/maintenance");
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show dialog to confirm pause
|
||||
*/
|
||||
pauseDialog(maintenanceID) {
|
||||
this.selectedMaintenanceID = maintenanceID;
|
||||
this.$refs.confirmPause.show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Pause maintenance
|
||||
*/
|
||||
pauseMaintenance() {
|
||||
this.$root.getSocket().emit("pauseMaintenance", this.selectedMaintenanceID, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Resume maintenance
|
||||
*/
|
||||
resumeMaintenance(id) {
|
||||
this.$root.getSocket().emit("resumeMaintenance", id, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.mobile {
|
||||
.item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
text-decoration: none;
|
||||
border-radius: 10px;
|
||||
transition: all ease-in-out 0.15s;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
min-height: 90px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
&:hover {
|
||||
background-color: $highlight-white;
|
||||
}
|
||||
|
||||
&.under-maintenance {
|
||||
background-color: rgba(23, 71, 245, 0.16);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(23, 71, 245, 0.3) !important;
|
||||
}
|
||||
|
||||
.circle {
|
||||
background-color: $maintenance;
|
||||
}
|
||||
}
|
||||
|
||||
&.scheduled {
|
||||
.circle {
|
||||
background-color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
&.inactive {
|
||||
.circle {
|
||||
background-color: $danger;
|
||||
}
|
||||
}
|
||||
|
||||
&.ended {
|
||||
.left-part {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.circle {
|
||||
background-color: $dark-font-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.unknown {
|
||||
.circle {
|
||||
background-color: $dark-font-color;
|
||||
}
|
||||
}
|
||||
|
||||
.left-part {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
|
||||
.circle {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50rem;
|
||||
}
|
||||
|
||||
.info {
|
||||
.title {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.btn-group {
|
||||
width: 310px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.item {
|
||||
&:hover {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
123
src/pages/ManageStatusPage.vue
Normal file
123
src/pages/ManageStatusPage.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<transition name="slide-fade" appear>
|
||||
<div>
|
||||
<h1 class="mb-3">
|
||||
{{ $t("Status Pages") }}
|
||||
</h1>
|
||||
|
||||
<div>
|
||||
<router-link to="/add-status-page" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> {{ $t("New Status Page") }}</router-link>
|
||||
</div>
|
||||
|
||||
<div class="shadow-box">
|
||||
<template v-if="$root.statusPageListLoaded">
|
||||
<span v-if="Object.keys($root.statusPageList).length === 0" class="d-flex align-items-center justify-content-center my-3">
|
||||
{{ $t("No status pages") }}
|
||||
</span>
|
||||
|
||||
<!-- use <a> instead of <router-link>, because the heartbeat won't load. -->
|
||||
<a v-for="statusPage in $root.statusPageList" :key="statusPage.slug" :href="'/status/' + statusPage.slug" class="item">
|
||||
<img :src="icon(statusPage.icon)" alt class="logo me-2" />
|
||||
<div class="info">
|
||||
<div class="title">{{ statusPage.title }}</div>
|
||||
<div class="slug">/status/{{ statusPage.slug }}</div>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
<div v-else class="d-flex align-items-center justify-content-center my-3 spinner">
|
||||
<font-awesome-icon icon="spinner" size="2x" spin />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { getResBaseURL } from "../util-frontend";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Get the correct URL for the icon
|
||||
* @param {string} icon Path for icon
|
||||
* @returns {string} Correctly formatted path including port numbers
|
||||
*/
|
||||
icon(icon) {
|
||||
if (icon === "/icon.svg") {
|
||||
return icon;
|
||||
} else {
|
||||
return getResBaseURL() + icon;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
text-decoration: none;
|
||||
border-radius: 10px;
|
||||
transition: all ease-in-out 0.15s;
|
||||
padding: 10px;
|
||||
|
||||
&:hover {
|
||||
background-color: $highlight-white;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #cdf8f4;
|
||||
}
|
||||
|
||||
$logo-width: 70px;
|
||||
|
||||
.logo {
|
||||
width: $logo-width;
|
||||
height: $logo-width;
|
||||
|
||||
// Better when the image is loading
|
||||
min-height: 1px;
|
||||
}
|
||||
|
||||
.info {
|
||||
.title {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.slug {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.item {
|
||||
&:hover {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
101
src/pages/NotFound.vue
Normal file
101
src/pages/NotFound.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Desktop header -->
|
||||
<header v-if="! $root.isMobile" class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom">
|
||||
<router-link to="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
|
||||
<object class="bi me-2 ms-4" width="40" height="40" data="/icon.svg" />
|
||||
<span class="fs-4 title">Uptime Kuma</span>
|
||||
</router-link>
|
||||
</header>
|
||||
|
||||
<!-- Mobile header -->
|
||||
<header v-else class="d-flex flex-wrap justify-content-center pt-2 pb-2 mb-3">
|
||||
<router-link to="/dashboard" class="d-flex align-items-center text-dark text-decoration-none">
|
||||
<object class="bi" width="40" height="40" data="/icon.svg" />
|
||||
<span class="fs-4 title ms-2">Uptime Kuma</span>
|
||||
</router-link>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<div>
|
||||
<strong>🐻 {{ $t("Page Not Found") }}</strong>
|
||||
</div>
|
||||
|
||||
<div class="guide">
|
||||
{{ $t("Most likely causes:") }}
|
||||
<ul>
|
||||
<li>{{ $t("The resource is no longer available.") }}</li>
|
||||
<li>{{ $t("There might be a typing error in the address.") }}</li>
|
||||
</ul>
|
||||
|
||||
{{ $t("What you can try:") }}<br />
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
async mounted() {
|
||||
|
||||
},
|
||||
methods: {
|
||||
/** Go back 1 in browser history */
|
||||
goBack() {
|
||||
history.back();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.go-back {
|
||||
text-decoration: none;
|
||||
color: $primary !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 50px;
|
||||
padding-top: 30px;
|
||||
|
||||
strong {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.guide {
|
||||
max-width: 800px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dark {
|
||||
header {
|
||||
background-color: $dark-header-bg;
|
||||
border-bottom-color: $dark-header-bg !important;
|
||||
|
||||
span {
|
||||
color: #f0f6fc;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-nav {
|
||||
background-color: $dark-bg;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,12 +1,21 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="$root.isMobile" class="shadow-box mb-3">
|
||||
<router-link to="/manage-status-page" class="nav-link">
|
||||
<font-awesome-icon icon="stream" /> {{ $t("Status Pages") }}
|
||||
</router-link>
|
||||
<router-link to="/maintenance" class="nav-link">
|
||||
<font-awesome-icon icon="wrench" /> {{ $t("Maintenance") }}
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<h1 v-show="show" class="mb-3">
|
||||
{{ $t("Settings") }}
|
||||
</h1>
|
||||
|
||||
<div class="shadow-box">
|
||||
<div class="shadow-box shadow-box-settings">
|
||||
<div class="row">
|
||||
<div class="settings-menu">
|
||||
<div v-if="showSubMenu" class="settings-menu col-lg-3 col-md-5">
|
||||
<router-link
|
||||
v-for="(item, key) in subMenus"
|
||||
:key="key"
|
||||
@@ -16,9 +25,17 @@
|
||||
{{ item.title }}
|
||||
</div>
|
||||
</router-link>
|
||||
|
||||
<!-- Logout Button -->
|
||||
<a v-if="$root.isMobile && $root.loggedIn && $root.socket.token !== 'autoLogin'" class="logout" @click.prevent="$root.logout">
|
||||
<div class="menu-item">
|
||||
<font-awesome-icon icon="sign-out-alt" />
|
||||
{{ $t("Logout") }}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="settings-content">
|
||||
<div class="settings-content-header">
|
||||
<div class="settings-content col-lg-9 col-md-7">
|
||||
<div v-if="currentPage" class="settings-content-header">
|
||||
{{ subMenus[currentPage].title }}
|
||||
</div>
|
||||
<div class="mx-3">
|
||||
@@ -41,7 +58,6 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
show: true,
|
||||
|
||||
settings: {},
|
||||
settingsLoaded: false,
|
||||
};
|
||||
@@ -52,11 +68,19 @@ export default {
|
||||
let pathSplit = useRoute().path.split("/");
|
||||
let pathEnd = pathSplit[pathSplit.length - 1];
|
||||
if (!pathEnd || pathEnd === "settings") {
|
||||
return "general";
|
||||
return null;
|
||||
}
|
||||
return pathEnd;
|
||||
},
|
||||
|
||||
showSubMenu() {
|
||||
if (this.$root.isMobile) {
|
||||
return !this.currentPage;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
subMenus() {
|
||||
return {
|
||||
general: {
|
||||
@@ -68,15 +92,33 @@ export default {
|
||||
notifications: {
|
||||
title: this.$t("Notifications"),
|
||||
},
|
||||
"reverse-proxy": {
|
||||
title: this.$t("Reverse Proxy"),
|
||||
},
|
||||
tags: {
|
||||
title: this.$t("Tags"),
|
||||
},
|
||||
"monitor-history": {
|
||||
title: this.$t("Monitor History"),
|
||||
},
|
||||
"docker-hosts": {
|
||||
title: this.$t("Docker Hosts"),
|
||||
},
|
||||
security: {
|
||||
title: this.$t("Security"),
|
||||
},
|
||||
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"),
|
||||
},
|
||||
@@ -84,15 +126,38 @@ export default {
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
"$root.isMobile"() {
|
||||
this.loadGeneralPage();
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.loadSettings();
|
||||
this.loadGeneralPage();
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
/**
|
||||
* Load the general settings page
|
||||
* For desktop only, on mobile do nothing
|
||||
*/
|
||||
loadGeneralPage() {
|
||||
if (!this.currentPage && !this.$root.isMobile) {
|
||||
this.$router.push("/settings/general");
|
||||
}
|
||||
},
|
||||
|
||||
/** Load settings from server */
|
||||
loadSettings() {
|
||||
this.$root.getSocket().emit("getSettings", (res) => {
|
||||
this.settings = res.data;
|
||||
|
||||
if (this.settings.checkUpdate === undefined) {
|
||||
this.settings.checkUpdate = true;
|
||||
}
|
||||
|
||||
if (this.settings.searchEngineIndex === undefined) {
|
||||
this.settings.searchEngineIndex = false;
|
||||
}
|
||||
@@ -101,28 +166,77 @@ export default {
|
||||
this.settings.entryPage = "dashboard";
|
||||
}
|
||||
|
||||
if (this.settings.dnsCache === undefined) {
|
||||
this.settings.dnsCache = false;
|
||||
}
|
||||
|
||||
if (this.settings.keepDataPeriodDays === undefined) {
|
||||
this.settings.keepDataPeriodDays = 180;
|
||||
}
|
||||
|
||||
if (this.settings.tlsExpiryNotifyDays === undefined) {
|
||||
this.settings.tlsExpiryNotifyDays = [ 7, 14, 21 ];
|
||||
}
|
||||
|
||||
if (this.settings.trustProxy === undefined) {
|
||||
this.settings.trustProxy = false;
|
||||
}
|
||||
|
||||
this.settingsLoaded = true;
|
||||
});
|
||||
},
|
||||
|
||||
saveSettings() {
|
||||
this.$root.getSocket().emit("setSettings", this.settings, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
this.loadSettings();
|
||||
});
|
||||
/**
|
||||
* Callback for saving settings
|
||||
* @callback saveSettingsCB
|
||||
* @param {Object} res Result of operation
|
||||
*/
|
||||
|
||||
/**
|
||||
* Save Settings
|
||||
* @param {saveSettingsCB} [callback]
|
||||
* @param {string} [currentPassword] Only need for disableAuth to true
|
||||
*/
|
||||
saveSettings(callback, currentPassword) {
|
||||
let valid = this.validateSettings();
|
||||
if (valid.success) {
|
||||
this.$root.getSocket().emit("setSettings", this.settings, currentPassword, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
this.loadSettings();
|
||||
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.$root.toastError(valid.msg);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensure settings are valid
|
||||
* @returns {Object} Contains success state and error msg
|
||||
*/
|
||||
validateSettings() {
|
||||
if (this.settings.keepDataPeriodDays < 0) {
|
||||
return {
|
||||
success: false,
|
||||
msg: this.$t("dataRetentionTimeError"),
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
msg: "",
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.shadow-box {
|
||||
.shadow-box-settings {
|
||||
padding: 20px;
|
||||
min-height: calc(100vh - 155px);
|
||||
}
|
||||
@@ -136,9 +250,6 @@ footer {
|
||||
}
|
||||
|
||||
.settings-menu {
|
||||
flex: 0 0 auto;
|
||||
width: 300px;
|
||||
|
||||
a {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
@@ -148,6 +259,8 @@ footer {
|
||||
margin: 0.5em;
|
||||
padding: 0.7em 1em;
|
||||
cursor: pointer;
|
||||
border-left-width: 0;
|
||||
transition: all ease-in-out 0.1s;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
@@ -171,9 +284,6 @@ footer {
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
flex: 0 0 auto;
|
||||
width: calc(100% - 300px);
|
||||
|
||||
.settings-content-header {
|
||||
width: calc(100% + 20px);
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
@@ -187,6 +297,18 @@ footer {
|
||||
background: $dark-header-bg;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.mobile & {
|
||||
padding: 15px 0 0 0;
|
||||
|
||||
.dark & {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logout {
|
||||
color: $danger !important;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="form-container">
|
||||
<div class="form-container" data-cy="setup-form">
|
||||
<div class="form">
|
||||
<form @submit.prevent="submit">
|
||||
<div>
|
||||
@@ -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>
|
||||
@@ -23,21 +23,21 @@
|
||||
</div>
|
||||
|
||||
<div class="form-floating mt-3">
|
||||
<input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username" required>
|
||||
<input id="floatingInput" v-model="username" type="text" class="form-control" placeholder="Username" required data-cy="username-input">
|
||||
<label for="floatingInput">{{ $t("Username") }}</label>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mt-3">
|
||||
<input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password" required>
|
||||
<input id="floatingPassword" v-model="password" type="password" class="form-control" placeholder="Password" required data-cy="password-input">
|
||||
<label for="floatingPassword">{{ $t("Password") }}</label>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mt-3">
|
||||
<input id="repeat" v-model="repeatPassword" type="password" class="form-control" placeholder="Repeat Password" required>
|
||||
<input id="repeat" v-model="repeatPassword" type="password" class="form-control" placeholder="Repeat Password" required data-cy="password-repeat-input">
|
||||
<label for="repeat">{{ $t("Repeat Password") }}</label>
|
||||
</div>
|
||||
|
||||
<button class="w-100 btn btn-primary mt-3" type="submit" :disabled="processing">
|
||||
<button class="w-100 btn btn-primary mt-3" type="submit" :disabled="processing" data-cy="submit-setup-form">
|
||||
{{ $t("Create") }}
|
||||
</button>
|
||||
</form>
|
||||
@@ -59,9 +59,7 @@ export default {
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
"$i18n.locale"() {
|
||||
localStorage.locale = this.$i18n.locale;
|
||||
},
|
||||
|
||||
},
|
||||
mounted() {
|
||||
this.$root.getSocket().emit("needSetup", (needSetup) => {
|
||||
@@ -71,6 +69,10 @@ export default {
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Submit form data for processing
|
||||
* @returns {void}
|
||||
*/
|
||||
submit() {
|
||||
this.processing = true;
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user