Merge branch 'master' into 1.23.14-to-2.0.0

# Conflicts:
#	src/lang/en.json
#	src/util.js
#	src/util.ts
This commit is contained in:
Louis Lam
2024-06-26 10:00:30 +08:00
440 changed files with 26491 additions and 16428 deletions

View File

@@ -82,8 +82,6 @@
<script>
import APIKeyDialog from "../../components/APIKeyDialog.vue";
import Confirm from "../Confirm.vue";
import { useToast } from "vue-toastification";
const toast = useToast();
export default {
components: {
@@ -109,6 +107,7 @@ export default {
/**
* Show dialog to confirm deletion
* @param {number} keyID ID of monitor that is being deleted
* @returns {void}
*/
deleteDialog(keyID) {
this.selectedKeyID = keyID;
@@ -117,19 +116,18 @@ export default {
/**
* Delete a key
* @returns {void}
*/
deleteKey() {
this.$root.deleteAPIKey(this.selectedKeyID, (res) => {
if (res.ok) {
toast.success(res.msg);
} else {
toast.error(res.msg);
}
this.$root.toastRes(res);
});
},
/**
* Show dialog to confirm pause
* @param {number} keyID ID of key to pause
* @returns {void}
*/
disableDialog(keyID) {
this.selectedKeyID = keyID;
@@ -137,7 +135,8 @@ export default {
},
/**
* Pause maintenance
* Pause API key
* @returns {void}
*/
disableKey() {
this.$root
@@ -148,7 +147,9 @@ export default {
},
/**
* Resume maintenance
* Resume API key
* @param {number} id Key to resume
* @returns {void}
*/
enableKey(id) {
this.$root.getSocket().emit("enableAPIKey", id, (res) => {

View File

@@ -1,228 +0,0 @@
<template>
<div>
<div class="my-4">
<div class="alert alert-warning" role="alert" style="border-radius: 15px;">
{{ $t("backupOutdatedWarning") }}<br />
<br />
{{ $t("backupRecommend") }}
</div>
<h4 class="mt-4 mb-2">{{ $t("Export Backup") }}</h4>
<p>
{{ $t("backupDescription") }} <br />
({{ $t("backupDescription2") }}) <br />
</p>
<div class="mb-2">
<button class="btn btn-primary" @click="downloadBackup">
{{ $t("Export") }}
</button>
</div>
<p>
<strong>{{ $t("backupDescription3") }}</strong>
</p>
</div>
<div class="my-4">
<h4 class="mt-4 mb-2">{{ $t("Import Backup") }}</h4>
<label class="form-label">{{ $t("Options") }}:</label>
<br />
<div class="form-check form-check-inline">
<input
id="radioKeep"
v-model="importHandle"
class="form-check-input"
type="radio"
name="radioImportHandle"
value="keep"
/>
<label class="form-check-label" for="radioKeep">
{{ $t("Keep both") }}
</label>
</div>
<div class="form-check form-check-inline">
<input
id="radioSkip"
v-model="importHandle"
class="form-check-input"
type="radio"
name="radioImportHandle"
value="skip"
/>
<label class="form-check-label" for="radioSkip">
{{ $t("Skip existing") }}
</label>
</div>
<div class="form-check form-check-inline">
<input
id="radioOverwrite"
v-model="importHandle"
class="form-check-input"
type="radio"
name="radioImportHandle"
value="overwrite"
/>
<label class="form-check-label" for="radioOverwrite">
{{ $t("Overwrite") }}
</label>
</div>
<div class="form-text mb-2">
{{ $t("importHandleDescription") }}
</div>
<div class="mb-2">
<input
id="import-backend"
type="file"
class="form-control"
accept="application/json"
/>
</div>
<div class="input-group mb-2 justify-content-end">
<button
type="button"
class="btn btn-outline-primary"
:disabled="processing"
@click="confirmImport"
>
<div
v-if="processing"
class="spinner-border spinner-border-sm me-1"
></div>
{{ $t("Import") }}
</button>
</div>
<div
v-if="importAlert"
class="alert alert-danger mt-3"
style="padding: 6px 16px;"
>
{{ importAlert }}
</div>
</div>
<Confirm
ref="confirmImport"
btn-style="btn-danger"
:yes-text="$t('Yes')"
:no-text="$t('No')"
@yes="importBackup"
>
{{ $t("confirmImportMsg") }}
</Confirm>
</div>
</template>
<script>
import Confirm from "../../components/Confirm.vue";
import dayjs from "dayjs";
import { useToast } from "vue-toastification";
const toast = useToast();
export default {
components: {
Confirm,
},
data() {
return {
processing: false,
importHandle: "skip",
importAlert: null,
};
},
methods: {
/**
* Show the confimation dialog confirming the configuration
* be imported
*/
confirmImport() {
this.$refs.confirmImport.show();
},
/** Download a backup of the configuration */
downloadBackup() {
let time = dayjs().format("YYYY_MM_DD-hh_mm_ss");
let fileName = `Uptime_Kuma_Backup_${time}.json`;
let monitorList = Object.values(this.$root.monitorList);
let exportData = {
version: this.$root.info.version,
notificationList: this.$root.notificationList,
monitorList: monitorList,
};
exportData = JSON.stringify(exportData, null, 4);
let downloadItem = document.createElement("a");
downloadItem.setAttribute(
"href",
"data:application/json;charset=utf-8," +
encodeURIComponent(exportData)
);
downloadItem.setAttribute("download", fileName);
downloadItem.click();
},
/**
* Import the specified backup file
* @returns {?string}
*/
importBackup() {
this.processing = true;
let uploadItem = document.getElementById("import-backend").files;
if (uploadItem.length <= 0) {
this.processing = false;
return (this.importAlert = this.$t("alertNoFile"));
}
if (uploadItem.item(0).type !== "application/json") {
this.processing = false;
return (this.importAlert = this.$t("alertWrongFileType"));
}
let fileReader = new FileReader();
fileReader.readAsText(uploadItem.item(0));
fileReader.onload = (item) => {
this.$root.uploadBackup(
item.target.result,
this.importHandle,
(res) => {
this.processing = false;
if (res.ok) {
toast.success(res.msg);
} else {
toast.error(res.msg);
}
}
);
};
},
},
};
</script>
<style lang="scss" scoped>
@import "../../assets/vars.scss";
.dark {
#import-backend {
&::file-selector-button {
color: $primary;
background-color: $dark-bg;
}
&:hover:not(:disabled):not([readonly])::file-selector-button {
color: $dark-font-color2;
background-color: $primary;
}
}
}
</style>

View File

@@ -187,46 +187,6 @@
</div>
</div>
<!-- DNS Cache -->
<div class="mb-4">
<label class="form-label">
{{ $t("Enable DNS Cache") }}
<div class="form-text">
{{ $t("dnsCacheDescription") }}
</div>
</label>
<div class="form-check">
<input
id="dnsCacheEnable"
v-model="settings.dnsCache"
class="form-check-input"
type="radio"
name="dnsCache"
:value="true"
required
/>
<label class="form-check-label" for="dnsCacheEnable">
{{ $t("Enable") }}
</label>
</div>
<div class="form-check">
<input
id="dnsCacheDisable"
v-model="settings.dnsCache"
class="form-check-input"
type="radio"
name="dnsCache"
:value="false"
required
/>
<label class="form-check-label" for="dnsCacheDisable">
{{ $t("Disable") }}
</label>
</div>
</div>
<!-- Chrome Executable -->
<div class="mb-4">
<label class="form-label" for="primaryBaseURL">
@@ -293,16 +253,25 @@ export default {
},
methods: {
/** Save the settings */
/**
* Save the settings
* @returns {void}
*/
saveGeneral() {
localStorage.timezone = this.$root.userTimezone;
this.saveSettings();
},
/** Get the base URL of the application */
/**
* Get the base URL of the application
* @returns {void}
*/
autoGetPrimaryBaseURL() {
this.settings.primaryBaseURL = location.protocol + "//" + location.host;
},
/**
* Test the chrome executable
* @returns {void}
*/
testChrome() {
this.$root.getSocket().emit("testChrome", this.settings.chromeExecutable, (res) => {
this.$root.toastRes(res);

View File

@@ -28,7 +28,7 @@
</button>
</div>
<div class="my-4">
<div class="my-3">
<div v-if="$root.info.dbType === 'sqlite'" class="my-3">
<button class="btn btn-outline-info me-2" @click="shrinkDatabase">
{{ $t("Shrink Database") }} ({{ databaseSizeDisplay }})
</button>
@@ -57,9 +57,6 @@
<script>
import Confirm from "../../components/Confirm.vue";
import { log } from "../../util.ts";
import { useToast } from "vue-toastification";
const toast = useToast();
export default {
components: {
@@ -94,7 +91,10 @@ export default {
},
methods: {
/** Get the current size of the database */
/**
* Get the current size of the database
* @returns {void}
*/
loadDatabaseSize() {
log.debug("monitorhistory", "load database size");
this.$root.getSocket().emit("getDatabaseSize", (res) => {
@@ -107,30 +107,39 @@ export default {
});
},
/** Request that the database is shrunk */
/**
* Request that the database is shrunk
* @returns {void}
*/
shrinkDatabase() {
this.$root.getSocket().emit("shrinkDatabase", (res) => {
if (res.ok) {
this.loadDatabaseSize();
toast.success("Done");
this.$root.toastSuccess("Done");
} else {
log.debug("monitorhistory", res);
}
});
},
/** Show the dialog to confirm clearing stats */
/**
* Show the dialog to confirm clearing stats
* @returns {void}
*/
confirmClearStatistics() {
this.$refs.confirmClearStatistics.show();
},
/** Send the request to clear stats */
/**
* Send the request to clear stats
* @returns {void}
*/
clearStatistics() {
this.$root.clearStatistics((res) => {
if (res.ok) {
this.$router.go();
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
},

View File

@@ -20,6 +20,39 @@
</button>
</div>
<div class="my-4 pt-4">
<h5 class="my-4 settings-subheading">{{ $t("monitorToastMessagesLabel") }}</h5>
<p>{{ $t("monitorToastMessagesDescription") }}</p>
<div class="my-4">
<label for="toastErrorTimeoutSecs" class="form-label">
{{ $t("toastErrorTimeout") }}
</label>
<input
id="toastErrorTimeoutSecs"
v-model="toastErrorTimeoutSecs"
type="number"
class="form-control"
min="-1"
step="1"
/>
</div>
<div class="my-4">
<label for="toastSuccessTimeoutSecs" class="form-label">
{{ $t("toastSuccessTimeout") }}
</label>
<input
id="toastSuccessTimeoutSecs"
v-model="toastSuccessTimeoutSecs"
type="number"
class="form-control"
min="-1"
step="1"
/>
</div>
</div>
<div class="my-4 pt-4">
<h5 class="my-4 settings-subheading">{{ $t("settingsCertificateExpiry") }}</h5>
<p>{{ $t("certificationExpiryDescription") }}</p>
@@ -58,6 +91,8 @@ export default {
data() {
return {
toastSuccessTimeoutSecs: 20,
toastErrorTimeoutSecs: -1,
/**
* Variable to store the input for new certificate expiry day.
*/
@@ -77,10 +112,31 @@ export default {
},
},
watch: {
// Parse, store and apply new timeout settings.
toastSuccessTimeoutSecs(newTimeout) {
const parsedTimeout = parseInt(newTimeout);
if (parsedTimeout != null && !Number.isNaN(parsedTimeout)) {
localStorage.toastSuccessTimeout = newTimeout > 0 ? newTimeout * 1000 : newTimeout;
}
},
toastErrorTimeoutSecs(newTimeout) {
const parsedTimeout = parseInt(newTimeout);
if (parsedTimeout != null && !Number.isNaN(parsedTimeout)) {
localStorage.toastErrorTimeout = newTimeout > 0 ? newTimeout * 1000 : newTimeout;
}
}
},
mounted() {
this.loadToastTimeoutSettings();
},
methods: {
/**
* Remove a day from expiry notification days.
* @param {number} day The day to remove.
* @returns {void}
*/
removeExpiryNotifDay(day) {
this.settings.tlsExpiryNotifyDays = this.settings.tlsExpiryNotifyDays.filter(d => d !== day);
@@ -93,6 +149,7 @@ export default {
* - day is > 0.
* - The day is not already in the list.
* @param {number} day The day number to add.
* @returns {void}
*/
addExpiryNotifDay(day) {
if (day != null && day !== "") {
@@ -106,6 +163,28 @@ export default {
}
}
},
/**
* Loads toast timeout settings from storage to component data.
* @returns {void}
*/
loadToastTimeoutSettings() {
const successTimeout = localStorage.toastSuccessTimeout;
if (successTimeout !== undefined) {
const parsedTimeout = parseInt(successTimeout);
if (parsedTimeout != null && !Number.isNaN(parsedTimeout)) {
this.toastSuccessTimeoutSecs = parsedTimeout > 0 ? parsedTimeout / 1000 : parsedTimeout;
}
}
const errorTimeout = localStorage.toastErrorTimeout;
if (errorTimeout !== undefined) {
const parsedTimeout = parseInt(errorTimeout);
if (parsedTimeout != null && !Number.isNaN(parsedTimeout)) {
this.toastErrorTimeoutSecs = parsedTimeout > 0 ? parsedTimeout / 1000 : parsedTimeout;
}
}
},
},
};
</script>

View File

@@ -0,0 +1,53 @@
<template>
<div>
<div class="dockerHost-list my-4">
<p v-if="$root.remoteBrowserList.length === 0">
{{ $t("Not available, please setup.") }}
</p>
<ul class="list-group mb-3" style="border-radius: 1rem;">
<li v-for="(remoteBrowser, index) in $root.remoteBrowserList" :key="index" class="list-group-item">
{{ remoteBrowser.name }}<br>
<a href="#" @click="$refs.remoteBrowserDialog.show(remoteBrowser.id)">{{ $t("Edit") }}</a>
</li>
</ul>
<button class="btn btn-primary me-2" type="button" @click="$refs.remoteBrowserDialog.show()">
<font-awesome-icon icon="plus" /> {{ $t("Add Remote Browser") }}
</button>
</div>
<div class="my-4 pt-4">
<h5 class="my-4 settings-subheading">{{ $t("What is a Remote Browser?") }}</h5>
<p>{{ $t("remoteBrowsersDescription") }} <a href="https://hub.docker.com/r/browserless/chrome">{{ $t("self-hosted container") }}</a></p>
</div>
<RemoteBrowserDialog ref="remoteBrowserDialog" />
</div>
</template>
<script>
import RemoteBrowserDialog from "../../components/RemoteBrowserDialog.vue";
export default {
components: {
RemoteBrowserDialog,
},
data() {
return {};
},
computed: {
settings() {
return this.$parent.$parent.$parent.settings;
},
saveSettings() {
return this.$parent.$parent.$parent.saveSettings;
},
settingsLoaded() {
return this.$parent.$parent.$parent.settingsLoaded;
},
}
};
</script>

View File

@@ -175,17 +175,26 @@ export default {
this.$root.getSocket().emit(prefix + "leave");
},
methods: {
/** Start the Cloudflare tunnel */
/**
* Start the Cloudflare tunnel
* @returns {void}
*/
start() {
this.$root.getSocket().emit(prefix + "start", this.cloudflareTunnelToken);
},
/** Stop the Cloudflare tunnel */
/**
* Stop the Cloudflare tunnel
* @returns {void}
*/
stop() {
this.$root.getSocket().emit(prefix + "stop", this.currentPassword, (res) => {
this.$root.toastRes(res);
});
},
/** Remove the token for the Cloudflare tunnel */
/**
* Remove the token for the Cloudflare tunnel
* @returns {void}
*/
removeToken() {
this.$root.getSocket().emit(prefix + "removeToken");
this.cloudflareTunnelToken = "";

View File

@@ -93,10 +93,16 @@
<TwoFADialog ref="TwoFADialog" />
<Confirm ref="confirmDisableAuth" btn-style="btn-danger" :yes-text="$t('I understand, please disable')" :no-text="$t('Leave')" @yes="disableAuth">
<!-- eslint-disable-next-line vue/no-v-html -->
<p v-html="$t('disableauth.message1')"></p>
<!-- eslint-disable-next-line vue/no-v-html -->
<p v-html="$t('disableauth.message2')"></p>
<i18n-t tag="p" keypath="disableauth.message1">
<template #disableAuth>
<strong>{{ $t('disable authentication') }}</strong>
</template>
</i18n-t>
<i18n-t tag="p" keypath="disableauth.message2">
<template #intendThirdPartyAuth>
<strong>{{ $t('intend to implement third-party authentication') }}</strong>
</template>
</i18n-t>
<p>{{ $t("Please use this option carefully!") }}</p>
<div class="mb-3">
@@ -155,7 +161,10 @@ export default {
},
methods: {
/** Check new passwords match before saving them */
/**
* Check new passwords match before saving them
* @returns {void}
*/
savePassword() {
if (this.password.newPassword !== this.password.repeatNewPassword) {
this.invalidPassword = true;
@@ -168,12 +177,21 @@ export default {
this.password.currentPassword = "";
this.password.newPassword = "";
this.password.repeatNewPassword = "";
// Update token of the current session
if (res.token) {
this.$root.storage().token = res.token;
this.$root.socket.token = res.token;
}
}
});
}
},
/** Disable authentication for web app access */
/**
* Disable authentication for web app access
* @returns {void}
*/
disableAuth() {
this.settings.disableAuth = true;
@@ -186,7 +204,10 @@ export default {
}, this.password.currentPassword);
},
/** Enable authentication for web app access */
/**
* Enable authentication for web app access
* @returns {void}
*/
enableAuth() {
this.settings.disableAuth = false;
this.saveSettings();
@@ -194,7 +215,10 @@ export default {
location.reload();
},
/** Show confirmation dialog for disable auth */
/**
* Show confirmation dialog for disable auth
* @returns {void}
*/
confirmDisableAuth() {
this.$refs.confirmDisableAuth.show();
},

View File

@@ -28,11 +28,9 @@
</template>
<script>
import { useToast } from "vue-toastification";
import TagEditDialog from "../../components/TagEditDialog.vue";
import Tag from "../Tag.vue";
import Confirm from "../Confirm.vue";
const toast = useToast();
export default {
components: {
@@ -86,7 +84,7 @@ export default {
if (res.ok) {
this.tagsList = res.tags;
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
},
@@ -138,7 +136,7 @@ export default {
/**
* Get monitors which has a specific tag locally
* @param {number} tagId id of the tag to filter
* @returns {Object[]} list of monitors which has a specific tag
* @returns {object[]} list of monitors which has a specific tag
*/
monitorsByTag(tagId) {
return Object.values(this.$root.monitorList).filter((monitor) => {