diff options
Diffstat (limited to 'src/components/settings')
-rw-r--r-- | src/components/settings/APIKeys.vue | 273 | ||||
-rw-r--r-- | src/components/settings/About.vue | 66 | ||||
-rw-r--r-- | src/components/settings/Appearance.vue | 190 | ||||
-rw-r--r-- | src/components/settings/Docker.vue | 48 | ||||
-rw-r--r-- | src/components/settings/General.vue | 283 | ||||
-rw-r--r-- | src/components/settings/MonitorHistory.vue | 155 | ||||
-rw-r--r-- | src/components/settings/Notifications.vue | 218 | ||||
-rw-r--r-- | src/components/settings/Proxies.vue | 48 | ||||
-rw-r--r-- | src/components/settings/RemoteBrowsers.vue | 53 | ||||
-rw-r--r-- | src/components/settings/ReverseProxy.vue | 211 | ||||
-rw-r--r-- | src/components/settings/Security.vue | 228 | ||||
-rw-r--r-- | src/components/settings/Tags.vue | 175 |
12 files changed, 1948 insertions, 0 deletions
diff --git a/src/components/settings/APIKeys.vue b/src/components/settings/APIKeys.vue new file mode 100644 index 0000000..d31645b --- /dev/null +++ b/src/components/settings/APIKeys.vue @@ -0,0 +1,273 @@ +<template> + <div> + <div + v-if="settings.disableAuth" + class="mt-5 d-flex align-items-center justify-content-center my-3" + > + {{ $t("apiKeysDisabledMsg") }} + </div> + <div v-else> + <div class="add-btn"> + <button class="btn btn-primary me-2" type="button" @click="$refs.apiKeyDialog.show()"> + <font-awesome-icon icon="plus" /> {{ $t("Add API Key") }} + </button> + </div> + + <div> + <span + v-if="Object.keys(keyList).length === 0" + class="d-flex align-items-center justify-content-center my-3" + > + {{ $t("No API Keys") }} + </span> + + <div + v-for="(item, index) in keyList" + :key="index" + class="item" + :class="item.status" + > + <div class="left-part"> + <div class="circle"></div> + <div class="info"> + <div class="title">{{ item.name }}</div> + <div class="status"> + {{ $t("apiKey-" + item.status) }} + </div> + <div class="date"> + {{ $t("Created") }}: {{ item.createdDate }} + </div> + <div class="date"> + {{ $t("Expires") }}: + {{ item.expires || $t("Never") }} + </div> + </div> + </div> + + <div class="buttons"> + <div class="btn-group" role="group"> + <button v-if="item.active" class="btn btn-normal" @click="disableDialog(item.id)"> + <font-awesome-icon icon="pause" /> {{ $t("Disable") }} + </button> + + <button v-if="!item.active" class="btn btn-primary" @click="enableKey(item.id)"> + <font-awesome-icon icon="play" /> {{ $t("Enable") }} + </button> + + <button class="btn btn-danger" @click="deleteDialog(item.id)"> + <font-awesome-icon icon="trash" /> {{ $t("Delete") }} + </button> + </div> + </div> + </div> + </div> + </div> + + <div class="text-center mt-3" style="font-size: 13px;"> + <a href="https://github.com/louislam/uptime-kuma/wiki/API-Keys" target="_blank">{{ $t("Learn More") }}</a> + </div> + + <Confirm ref="confirmPause" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="disableKey"> + {{ $t("disableAPIKeyMsg") }} + </Confirm> + + <Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteKey"> + {{ $t("deleteAPIKeyMsg") }} + </Confirm> + + <APIKeyDialog ref="apiKeyDialog" /> + </div> +</template> + +<script> +import APIKeyDialog from "../../components/APIKeyDialog.vue"; +import Confirm from "../Confirm.vue"; + +export default { + components: { + APIKeyDialog, + Confirm, + }, + data() { + return { + selectedKeyID: null, + }; + }, + computed: { + keyList() { + let result = Object.values(this.$root.apiKeyList); + return result; + }, + settings() { + return this.$parent.$parent.$parent.settings; + }, + }, + + methods: { + /** + * Show dialog to confirm deletion + * @param {number} keyID ID of monitor that is being deleted + * @returns {void} + */ + deleteDialog(keyID) { + this.selectedKeyID = keyID; + this.$refs.confirmDelete.show(); + }, + + /** + * Delete a key + * @returns {void} + */ + deleteKey() { + this.$root.deleteAPIKey(this.selectedKeyID, (res) => { + this.$root.toastRes(res); + }); + }, + + /** + * Show dialog to confirm pause + * @param {number} keyID ID of key to pause + * @returns {void} + */ + disableDialog(keyID) { + this.selectedKeyID = keyID; + this.$refs.confirmPause.show(); + }, + + /** + * Pause API key + * @returns {void} + */ + disableKey() { + this.$root + .getSocket() + .emit("disableAPIKey", this.selectedKeyID, (res) => { + this.$root.toastRes(res); + }); + }, + + /** + * Resume API key + * @param {number} id Key to resume + * @returns {void} + */ + enableKey(id) { + this.$root.getSocket().emit("enableAPIKey", 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; + } +} + +.add-btn { + padding-top: 20px; + padding-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; + } + + &.active { + .circle { + background-color: $primary; + } + } + + &.inactive { + .circle { + background-color: $danger; + } + } + + &.expired { + .left-part { + opacity: 0.3; + } + + .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; + } + } +} + +.date { + margin-top: 5px; + display: block; + font-size: 14px; + background-color: rgba(255, 255, 255, 0.5); + border-radius: 20px; + padding: 0 10px; + width: fit-content; + + .dark & { + color: white; + background-color: rgba(255, 255, 255, 0.1); + } +} + +.dark { + .item { + &:hover { + background-color: $dark-bg2; + } + } +} +</style> diff --git a/src/components/settings/About.vue b/src/components/settings/About.vue new file mode 100644 index 0000000..3ef9e6d --- /dev/null +++ b/src/components/settings/About.vue @@ -0,0 +1,66 @@ +<template> + <div class="d-flex justify-content-center align-items-center"> + <div class="logo d-flex flex-column justify-content-center align-items-center"> + <object class="my-4" width="200" height="200" data="/icon.svg" /> + <div class="fs-4 fw-bold">Uptime Kuma</div> + <div>{{ $t("Version") }}: {{ $root.info.version }}</div> + <div class="frontend-version">{{ $t("Frontend Version") }}: {{ $root.frontendVersion }}</div> + + <div v-if="!$root.isFrontendBackendVersionMatched" class="alert alert-warning mt-4" role="alert"> + ⚠️ {{ $t("Frontend Version do not match backend version!") }} + </div> + + <div class="my-3 update-link"><a href="https://github.com/louislam/uptime-kuma/releases" target="_blank" rel="noopener">{{ $t("Check Update On GitHub") }}</a></div> + + <div class="mt-1"> + <div class="form-check"> + <label><input v-model="settings.checkUpdate" type="checkbox" @change="saveSettings()" /> {{ $t("Show update if available") }}</label> + </div> + + <div class="form-check"> + <label><input v-model="settings.checkBeta" type="checkbox" :disabled="!settings.checkUpdate" @change="saveSettings()" /> {{ $t("Also check beta release") }}</label> + </div> + </div> + </div> + </div> +</template> + +<script> +export default { + computed: { + settings() { + return this.$parent.$parent.$parent.settings; + }, + saveSettings() { + return this.$parent.$parent.$parent.saveSettings; + }, + settingsLoaded() { + return this.$parent.$parent.$parent.settingsLoaded; + }, + }, + + watch: { + + } +}; +</script> + +<style lang="scss" scoped> +.logo { + margin: 4em 1em; +} + +.update-link { + font-size: 0.8em; +} + +.frontend-version { + font-size: 0.9em; + color: #cccccc; + + .dark & { + color: #333333; + } +} + +</style> diff --git a/src/components/settings/Appearance.vue b/src/components/settings/Appearance.vue new file mode 100644 index 0000000..a1391d6 --- /dev/null +++ b/src/components/settings/Appearance.vue @@ -0,0 +1,190 @@ +<template> + <div> + <div class="my-4"> + <label for="language" class="form-label"> + {{ $t("Language") }} + </label> + <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> + </select> + </div> + <div class="my-4"> + <label for="timezone" class="form-label">{{ $t("Theme") }}</label> + <div> + <div + class="btn-group" + role="group" + aria-label="Basic checkbox toggle button group" + > + <input + id="btncheck1" + v-model="$root.userTheme" + type="radio" + class="btn-check" + name="theme" + autocomplete="off" + value="light" + /> + <label class="btn btn-outline-primary" for="btncheck1"> + {{ $t("Light") }} + </label> + + <input + id="btncheck2" + v-model="$root.userTheme" + type="radio" + class="btn-check" + name="theme" + autocomplete="off" + value="dark" + /> + <label class="btn btn-outline-primary" for="btncheck2"> + {{ $t("Dark") }} + </label> + + <input + id="btncheck3" + v-model="$root.userTheme" + type="radio" + class="btn-check" + name="theme" + autocomplete="off" + value="auto" + /> + <label class="btn btn-outline-primary" for="btncheck3"> + {{ $t("Auto") }} + </label> + </div> + </div> + </div> + <div class="my-4"> + <label class="form-label">{{ $t("Theme - Heartbeat Bar") }}</label> + <div> + <div + class="btn-group" + role="group" + aria-label="Basic checkbox toggle button group" + > + <input + id="btncheck4" + v-model="$root.userHeartbeatBar" + type="radio" + class="btn-check" + name="heartbeatBarTheme" + autocomplete="off" + value="normal" + /> + <label class="btn btn-outline-primary" for="btncheck4"> + {{ $t("Normal") }} + </label> + + <input + id="btncheck5" + v-model="$root.userHeartbeatBar" + type="radio" + class="btn-check" + name="heartbeatBarTheme" + autocomplete="off" + value="bottom" + /> + <label class="btn btn-outline-primary" for="btncheck5"> + {{ $t("Bottom") }} + </label> + + <input + id="btncheck6" + v-model="$root.userHeartbeatBar" + type="radio" + class="btn-check" + name="heartbeatBarTheme" + autocomplete="off" + value="none" + /> + <label class="btn btn-outline-primary" for="btncheck6"> + {{ $t("None") }} + </label> + </div> + </div> + </div> + + <!-- Timeline --> + <div class="my-4"> + <label class="form-label">{{ $t("styleElapsedTime") }}</label> + <div> + <div class="btn-group" role="group"> + <input + id="styleElapsedTimeShowNoLine" + v-model="$root.styleElapsedTime" + type="radio" + class="btn-check" + name="styleElapsedTime" + autocomplete="off" + value="no-line" + /> + <label class="btn btn-outline-primary" for="styleElapsedTimeShowNoLine"> + {{ $t("styleElapsedTimeShowNoLine") }} + </label> + + <input + id="styleElapsedTimeShowWithLine" + v-model="$root.styleElapsedTime" + type="radio" + class="btn-check" + name="styleElapsedTime" + autocomplete="off" + value="with-line" + /> + <label class="btn btn-outline-primary" for="styleElapsedTimeShowWithLine"> + {{ $t("styleElapsedTimeShowWithLine") }} + </label> + + <input + id="styleElapsedTimeNone" + v-model="$root.styleElapsedTime" + type="radio" + class="btn-check" + name="styleElapsedTime" + autocomplete="off" + value="none" + /> + <label class="btn btn-outline-primary" for="styleElapsedTimeNone"> + {{ $t("None") }} + </label> + </div> + </div> + </div> + </div> +</template> + +<script> +export default { + +}; +</script> + +<style lang="scss" scoped> +@import "../../assets/vars.scss"; + +.btn-check:active + .btn-outline-primary, +.btn-check:checked + .btn-outline-primary, +.btn-check:hover + .btn-outline-primary { + color: #fff; + + .dark & { + color: #000; + } +} + +.dark { + .list-group-item { + background-color: $dark-bg2; + color: $dark-font-color; + } +} +</style> diff --git a/src/components/settings/Docker.vue b/src/components/settings/Docker.vue new file mode 100644 index 0000000..c411c30 --- /dev/null +++ b/src/components/settings/Docker.vue @@ -0,0 +1,48 @@ +<template> + <div> + <div class="dockerHost-list my-4"> + <p v-if="$root.dockerHostList.length === 0"> + {{ $t("Not available, please setup.") }} + </p> + + <ul class="list-group mb-3" style="border-radius: 1rem;"> + <li v-for="(dockerHost, index) in $root.dockerHostList" :key="index" class="list-group-item"> + {{ dockerHost.name }}<br> + <a href="#" @click="$refs.dockerHostDialog.show(dockerHost.id)">{{ $t("Edit") }}</a> + </li> + </ul> + + <button class="btn btn-primary me-2" type="button" @click="$refs.dockerHostDialog.show()"> + {{ $t("Setup Docker Host") }} + </button> + </div> + + <DockerHostDialog ref="dockerHostDialog" /> + </div> +</template> + +<script> +import DockerHostDialog from "../../components/DockerHostDialog.vue"; + +export default { + components: { + DockerHostDialog, + }, + + 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> diff --git a/src/components/settings/General.vue b/src/components/settings/General.vue new file mode 100644 index 0000000..487c3ba --- /dev/null +++ b/src/components/settings/General.vue @@ -0,0 +1,283 @@ +<template> + <div> + <form class="my-4" autocomplete="off" @submit.prevent="saveGeneral"> + <!-- Client side Timezone --> + <div class="mb-4"> + <label for="timezone" class="form-label"> + {{ $t("Display Timezone") }} + </label> + <select id="timezone" v-model="$root.userTimezone" class="form-select"> + <option value="auto"> + {{ $t("Auto") }}: {{ guessTimezone }} + </option> + <option + v-for="(timezone, index) in timezoneList" + :key="index" + :value="timezone.value" + > + {{ timezone.name }} + </option> + </select> + </div> + + <!-- Server Timezone --> + <div class="mb-4"> + <label for="timezone" class="form-label"> + {{ $t("Server Timezone") }} + </label> + <select id="timezone" v-model="settings.serverTimezone" class="form-select"> + <option value="UTC">UTC</option> + <option + v-for="(timezone, index) in timezoneList" + :key="index" + :value="timezone.value" + > + {{ timezone.name }} + </option> + </select> + </div> + + <!-- Search Engine --> + <div class="mb-4"> + <label class="form-label"> + {{ $t("Search Engine Visibility") }} + </label> + + <div class="form-check"> + <input + id="searchEngineIndexYes" + v-model="settings.searchEngineIndex" + class="form-check-input" + type="radio" + name="searchEngineIndex" + :value="true" + required + /> + <label class="form-check-label" for="searchEngineIndexYes"> + {{ $t("Allow indexing") }} + </label> + </div> + <div class="form-check"> + <input + id="searchEngineIndexNo" + v-model="settings.searchEngineIndex" + class="form-check-input" + type="radio" + name="searchEngineIndex" + :value="false" + required + /> + <label class="form-check-label" for="searchEngineIndexNo"> + {{ $t("Discourage search engines from indexing site") }} + </label> + </div> + </div> + + <!-- Entry Page --> + <div class="mb-4"> + <label class="form-label">{{ $t("Entry Page") }}</label> + + <div class="form-check"> + <input + id="entryPageDashboard" + v-model="settings.entryPage" + class="form-check-input" + type="radio" + name="entryPage" + value="dashboard" + required + /> + <label class="form-check-label" for="entryPageDashboard"> + {{ $t("Dashboard") }} + </label> + </div> + + <div v-for="statusPage in $root.statusPageList" :key="statusPage.id" class="form-check"> + <input + :id="'status-page-' + statusPage.id" + v-model="settings.entryPage" + class="form-check-input" + type="radio" + name="entryPage" + :value="'statusPage-' + statusPage.slug" + required + /> + <label class="form-check-label" :for="'status-page-' + statusPage.id"> + {{ $t("Status Page") }} - {{ statusPage.title }} + </label> + </div> + </div> + + <!-- Primary Base URL --> + <div class="mb-4"> + <label class="form-label" for="primaryBaseURL"> + {{ $t("Primary Base URL") }} + </label> + + <div class="input-group mb-3"> + <input + id="primaryBaseURL" + v-model="settings.primaryBaseURL" + class="form-control" + name="primaryBaseURL" + placeholder="https://" + pattern="https?://.+" + autocomplete="new-password" + /> + <button class="btn btn-outline-primary" type="button" @click="autoGetPrimaryBaseURL"> + {{ $t("Auto Get") }} + </button> + </div> + + <div class="form-text"></div> + </div> + + <!-- Steam API Key --> + <div class="mb-4"> + <label class="form-label" for="steamAPIKey"> + {{ $t("Steam API Key") }} + </label> + <HiddenInput + id="steamAPIKey" + v-model="settings.steamAPIKey" + autocomplete="new-password" + /> + <div class="form-text"> + {{ $t("steamApiKeyDescription") }} + <a href="https://steamcommunity.com/dev" target="_blank"> + https://steamcommunity.com/dev + </a> + </div> + </div> + + <!-- DNS Cache (nscd) --> + <div v-if="$root.info.isContainer" class="mb-4"> + <label class="form-label"> + {{ $t("enableNSCD") }} + </label> + + <div class="form-check"> + <input + id="nscdEnable" + v-model="settings.nscd" + class="form-check-input" + type="radio" + name="nscd" + :value="true" + required + /> + <label class="form-check-label" for="nscdEnable"> + {{ $t("Enable") }} + </label> + </div> + + <div class="form-check"> + <input + id="nscdDisable" + v-model="settings.nscd" + class="form-check-input" + type="radio" + name="nscd" + :value="false" + required + /> + <label class="form-check-label" for="nscdDisable"> + {{ $t("Disable") }} + </label> + </div> + </div> + + <!-- Chrome Executable --> + <div class="mb-4"> + <label class="form-label" for="primaryBaseURL"> + {{ $t("chromeExecutable") }} + </label> + + <div class="input-group mb-3"> + <input + id="primaryBaseURL" + v-model="settings.chromeExecutable" + class="form-control" + name="primaryBaseURL" + :placeholder="$t('chromeExecutableAutoDetect')" + /> + <button class="btn btn-outline-primary" type="button" @click="testChrome"> + {{ $t("Test") }} + </button> + </div> + + <div class="form-text"> + {{ $t("chromeExecutableDescription") }} + </div> + </div> + + <!-- Save Button --> + <div> + <button class="btn btn-primary" type="submit"> + {{ $t("Save") }} + </button> + </div> + </form> + </div> +</template> + +<script> +import HiddenInput from "../../components/HiddenInput.vue"; +import dayjs from "dayjs"; +import { timezoneList } from "../../util-frontend"; + +export default { + components: { + HiddenInput, + }, + + data() { + return { + timezoneList: timezoneList(), + }; + }, + + computed: { + settings() { + return this.$parent.$parent.$parent.settings; + }, + saveSettings() { + return this.$parent.$parent.$parent.saveSettings; + }, + settingsLoaded() { + return this.$parent.$parent.$parent.settingsLoaded; + }, + guessTimezone() { + return dayjs.tz.guess(); + } + }, + + methods: { + /** + * Save the settings + * @returns {void} + */ + saveGeneral() { + localStorage.timezone = this.$root.userTimezone; + this.saveSettings(); + }, + /** + * 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); + }); + }, + }, +}; +</script> + diff --git a/src/components/settings/MonitorHistory.vue b/src/components/settings/MonitorHistory.vue new file mode 100644 index 0000000..25e3e15 --- /dev/null +++ b/src/components/settings/MonitorHistory.vue @@ -0,0 +1,155 @@ +<template> + <div> + <div class="my-4"> + <label for="keepDataPeriodDays" class="form-label"> + {{ + $t("clearDataOlderThan", [ + settings.keepDataPeriodDays, + ]) + }} + {{ $t("infiniteRetention") }} + </label> + <input + id="keepDataPeriodDays" + v-model="settings.keepDataPeriodDays" + type="number" + class="form-control" + required + min="0" + step="1" + /> + <div v-if="settings.keepDataPeriodDays < 0" class="form-text"> + {{ $t("dataRetentionTimeError") }} + </div> + </div> + <div class="my-4"> + <button class="btn btn-primary" type="button" @click="saveSettings()"> + {{ $t("Save") }} + </button> + </div> + <div class="my-4"> + <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> + <i18n-t tag="div" keypath="shrinkDatabaseDescriptionSqlite" class="form-text mt-2 mb-4 ms-2"> + <template #vacuum> + <code>VACUUM</code> + </template> + <template #auto_vacuum> + <code>AUTO_VACUUM</code> + </template> + </i18n-t> + </div> + <button + id="clearAllStats-btn" + class="btn btn-outline-danger me-2 mb-2" + @click="confirmClearStatistics" + > + {{ $t("Clear all statistics") }} + </button> + </div> + <Confirm + ref="confirmClearStatistics" + btn-style="btn-danger" + :yes-text="$t('Yes')" + :no-text="$t('No')" + @yes="clearStatistics" + > + {{ $t("confirmClearStatisticsMsg") }} + </Confirm> + </div> +</template> + +<script> +import Confirm from "../../components/Confirm.vue"; +import { log } from "../../util.ts"; + +export default { + components: { + Confirm, + }, + + data() { + return { + databaseSize: 0, + }; + }, + + computed: { + settings() { + return this.$parent.$parent.$parent.settings; + }, + saveSettings() { + return this.$parent.$parent.$parent.saveSettings; + }, + settingsLoaded() { + return this.$parent.$parent.$parent.settingsLoaded; + }, + databaseSizeDisplay() { + return ( + Math.round((this.databaseSize / 1024 / 1024) * 10) / 10 + " MB" + ); + }, + }, + + mounted() { + this.loadDatabaseSize(); + }, + + methods: { + /** + * Get the current size of the database + * @returns {void} + */ + loadDatabaseSize() { + log.debug("monitorhistory", "load database size"); + this.$root.getSocket().emit("getDatabaseSize", (res) => { + if (res.ok) { + this.databaseSize = res.size; + log.debug("monitorhistory", "database size: " + res.size); + } else { + log.debug("monitorhistory", res); + } + }); + }, + + /** + * Request that the database is shrunk + * @returns {void} + */ + shrinkDatabase() { + this.$root.getSocket().emit("shrinkDatabase", (res) => { + if (res.ok) { + this.loadDatabaseSize(); + this.$root.toastSuccess("Done"); + } else { + log.debug("monitorhistory", res); + } + }); + }, + + /** + * Show the dialog to confirm clearing stats + * @returns {void} + */ + confirmClearStatistics() { + this.$refs.confirmClearStatistics.show(); + }, + + /** + * Send the request to clear stats + * @returns {void} + */ + clearStatistics() { + this.$root.clearStatistics((res) => { + if (res.ok) { + this.$router.go(); + } else { + this.$root.toastError(res.msg); + } + }); + }, + }, +}; +</script> diff --git a/src/components/settings/Notifications.vue b/src/components/settings/Notifications.vue new file mode 100644 index 0000000..2a65d79 --- /dev/null +++ b/src/components/settings/Notifications.vue @@ -0,0 +1,218 @@ +<template> + <div> + <div class="notification-list my-4"> + <p v-if="$root.notificationList.length === 0"> + {{ $t("Not available, please setup.") }} + </p> + <p v-else> + {{ $t("notificationDescription") }} + </p> + + <ul class="list-group mb-3" style="border-radius: 1rem;"> + <li v-for="(notification, index) in $root.notificationList" :key="index" class="list-group-item"> + {{ notification.name }}<br> + <a href="#" @click="$refs.notificationDialog.show(notification.id)">{{ $t("Edit") }}</a> + </li> + </ul> + + <button class="btn btn-primary me-2" type="button" @click="$refs.notificationDialog.show()"> + {{ $t("Setup Notification") }} + </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> + <p>{{ $t("notificationDescription") }}</p> + <div class="mt-1 mb-3 ps-2 cert-exp-days col-12 col-xl-6"> + <div v-for="day in settings.tlsExpiryNotifyDays" :key="day" class="d-flex align-items-center justify-content-between cert-exp-day-row py-2"> + <span>{{ day }} {{ $tc("day", day) }}</span> + <button type="button" class="btn-rm-expiry btn btn-outline-danger ms-2 py-1" :aria-label="$t('Remove the expiry notification')" @click="removeExpiryNotifDay(day)"> + <font-awesome-icon icon="times" /> + </button> + </div> + </div> + <div class="col-12 col-xl-6"> + <ActionInput v-model="expiryNotifInput" :type="'number'" :placeholder="$t('day')" :icon="'plus'" :action="() => addExpiryNotifDay(expiryNotifInput)" :action-aria-label="$t('Add a new expiry notification day')" /> + </div> + <div> + <button class="btn btn-primary" type="button" @click="saveSettings()"> + {{ $t("Save") }} + </button> + </div> + </div> + + <NotificationDialog ref="notificationDialog" /> + </div> +</template> + +<script> +import NotificationDialog from "../../components/NotificationDialog.vue"; +import ActionInput from "../ActionInput.vue"; + +export default { + components: { + NotificationDialog, + ActionInput, + }, + + data() { + return { + toastSuccessTimeoutSecs: 20, + toastErrorTimeoutSecs: -1, + /** + * Variable to store the input for new certificate expiry day. + */ + expiryNotifInput: null, + }; + }, + + computed: { + settings() { + return this.$parent.$parent.$parent.settings; + }, + saveSettings() { + return this.$parent.$parent.$parent.saveSettings; + }, + settingsLoaded() { + return this.$parent.$parent.$parent.settingsLoaded; + }, + }, + + 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); + }, + /** + * Add a new expiry notification day. + * Will verify: + * - day is not null or empty string. + * - day is a number. + * - 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 !== "") { + const parsedDay = parseInt(day); + if (parsedDay != null && !isNaN(parsedDay) && parsedDay > 0) { + if (!this.settings.tlsExpiryNotifyDays.includes(parsedDay)) { + this.settings.tlsExpiryNotifyDays.push(parseInt(day)); + this.settings.tlsExpiryNotifyDays.sort((a, b) => a - b); + this.expiryNotifInput = null; + } + } + } + }, + + /** + * 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> + +<style lang="scss" scoped> +@import "../../assets/vars.scss"; + +.btn-rm-expiry { + padding-left: 11px; + padding-right: 11px; +} + +.dark { + .list-group-item { + background-color: $dark-bg2; + color: $dark-font-color; + } +} + +.cert-exp-days .cert-exp-day-row { + border-bottom: 1px solid rgba(0, 0, 0, 0.125); + + .dark & { + border-bottom: 1px solid $dark-border-color; + } +} + +.cert-exp-days .cert-exp-day-row:last-child { + border: none; +} +</style> diff --git a/src/components/settings/Proxies.vue b/src/components/settings/Proxies.vue new file mode 100644 index 0000000..4608f3a --- /dev/null +++ b/src/components/settings/Proxies.vue @@ -0,0 +1,48 @@ +<template> + <div> + <!-- Proxies --> + <div class="proxy-list my-4"> + <p v-if="$root.proxyList.length === 0"> + {{ $t("Not available, please setup.") }} + </p> + <p v-else> + {{ $t("proxyDescription") }} + </p> + + <ul class="list-group mb-3" style="border-radius: 1rem;"> + <li v-for="(proxy, index) in $root.proxyList" :key="index" class="list-group-item"> + {{ proxy.host }}:{{ proxy.port }} ({{ proxy.protocol }}) + <span v-if="proxy.default === true" class="badge bg-primary ms-2">{{ $t("Default") }}</span><br> + <a href="#" @click="$refs.proxyDialog.show(proxy.id)">{{ $t("Edit") }}</a> + </li> + </ul> + + <button class="btn btn-primary me-2" type="button" @click="$refs.proxyDialog.show()"> + {{ $t("Setup Proxy") }} + </button> + </div> + + <ProxyDialog ref="proxyDialog" /> + </div> +</template> + +<script> +import ProxyDialog from "../../components/ProxyDialog.vue"; + +export default { + components: { + ProxyDialog + }, +}; +</script> + +<style lang="scss" scoped> +@import "../../assets/vars.scss"; + +.dark { + .list-group-item { + background-color: $dark-bg2; + color: $dark-font-color; + } +} +</style> diff --git a/src/components/settings/RemoteBrowsers.vue b/src/components/settings/RemoteBrowsers.vue new file mode 100644 index 0000000..b449ac6 --- /dev/null +++ b/src/components/settings/RemoteBrowsers.vue @@ -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> diff --git a/src/components/settings/ReverseProxy.vue b/src/components/settings/ReverseProxy.vue new file mode 100644 index 0000000..0f0d493 --- /dev/null +++ b/src/components/settings/ReverseProxy.vue @@ -0,0 +1,211 @@ +<template> + <div> + <h4 class="mt-4">Cloudflare Tunnel</h4> + + <div class="my-3"> + <div> + cloudflared: + <span v-if="installed === true" class="text-primary">{{ $t("Installed") }}</span> + <span v-else-if="installed === false" class="text-danger">{{ $t("Not installed") }}</span> + </div> + + <div> + {{ $t("Status") }}: + <span v-if="running" class="text-primary">{{ $t("Running") }}</span> + <span v-else-if="!running" class="text-danger">{{ $t("Not running") }}</span> + </div> + + <div v-if="false"> + {{ message }} + </div> + + <div v-if="errorMessage" class="mt-3"> + {{ $t("Message:") }} + <textarea v-model="errorMessage" class="form-control" readonly></textarea> + </div> + + <i18n-t v-if="installed === false" tag="p" keypath="wayToGetCloudflaredURL"> + <a + href="https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation/" + target="_blank" + >{{ $t("cloudflareWebsite") }}</a> + </i18n-t> + </div> + + <!-- If installed show token input --> + <div v-if="installed" class="mb-2"> + <div class="mb-4"> + <label class="form-label" for="cloudflareTunnelToken"> + Cloudflare Tunnel {{ $t("Token") }} + </label> + <HiddenInput + id="cloudflareTunnelToken" + v-model="cloudflareTunnelToken" + autocomplete="new-password" + :readonly="running" + /> + <div class="form-text"> + <div v-if="cloudflareTunnelToken" class="mb-3"> + <span v-if="!running" class="remove-token" @click="removeToken">{{ $t("Remove Token") }}</span> + </div> + + {{ $t("Don't know how to get the token? Please read the guide:") }}<br /> + <a href="https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy-with-Cloudflare-Tunnel" target="_blank"> + https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy-with-Cloudflare-Tunnel + </a> + </div> + </div> + + <div> + <button v-if="!running" class="btn btn-primary" type="submit" @click="start"> + {{ $t("Start") }} cloudflared + </button> + + <button v-if="running" class="btn btn-danger" type="submit" @click="$refs.confirmStop.show();"> + {{ $t("Stop") }} cloudflared + </button> + + <Confirm ref="confirmStop" btn-style="btn-danger" :yes-text="$t('Stop') + ' cloudflared'" :no-text="$t('Cancel')" @yes="stop"> + {{ $t("The current connection may be lost if you are currently connecting via Cloudflare Tunnel. Are you sure want to stop it? Type your current password to confirm it.") }} + + <p class="mt-2">{{ $t("disableCloudflaredNoAuthMsg") }}</p> + + <div v-if="!settings.disableAuth" class="mt-3"> + <label for="current-password2" class="form-label"> + {{ $t("Current Password") }} + </label> + <input + id="current-password2" + v-model="currentPassword" + type="password" + class="form-control" + required + /> + </div> + </Confirm> + </div> + </div> + + <h4 class="mt-4">{{ $t("Other Software") }}</h4> + <div> + {{ $t("For example: nginx, Apache and Traefik.") }} <br /> + {{ $t("Please read") }} <a href="https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy" target="_blank">https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy</a>. + </div> + + <h4 class="my-4">{{ $t("HTTP Headers") }}</h4> + <div class="my-3"> + <label class="form-label"> + {{ $t("Trust Proxy") }} + </label> + <div class="form-check"> + <input + id="trustProxyYes" + v-model="settings.trustProxy" + class="form-check-input" + type="radio" + name="trustProxyYes" + :value="true" + required + /> + <label class="form-check-label" for="trustProxyYes"> + {{ $t("Yes") }} + </label> + </div> + <div class="form-check"> + <input + id="trustProxyNo" + v-model="settings.trustProxy" + class="form-check-input" + type="radio" + name="flexRadioDefault" + :value="false" + required + /> + <label class="form-check-label" for="trustProxyNo"> + {{ $t("No") }} + </label> + </div> + + <div class="form-text"> + {{ $t("trustProxyDescription") }} + </div> + </div> + + <div> + <button class="btn btn-primary" type="submit" @click="saveSettings()"> + {{ $t("Save") }} + </button> + </div> + </div> +</template> + +<script> +import HiddenInput from "../../components/HiddenInput.vue"; +import Confirm from "../Confirm.vue"; + +const prefix = "cloudflared_"; + +export default { + components: { + HiddenInput, + Confirm + }, + data() { + // See /src/mixins/socket.js + return this.$root.cloudflared; + }, + computed: { + settings() { + return this.$parent.$parent.$parent.settings; + }, + saveSettings() { + return this.$parent.$parent.$parent.saveSettings; + }, + settingsLoaded() { + return this.$parent.$parent.$parent.settingsLoaded; + }, + }, + watch: { + + }, + created() { + this.$root.getSocket().emit(prefix + "join"); + }, + unmounted() { + this.$root.getSocket().emit(prefix + "leave"); + }, + methods: { + /** + * Start the Cloudflare tunnel + * @returns {void} + */ + start() { + this.$root.getSocket().emit(prefix + "start", this.cloudflareTunnelToken); + }, + /** + * 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 + * @returns {void} + */ + removeToken() { + this.$root.getSocket().emit(prefix + "removeToken"); + this.cloudflareTunnelToken = ""; + } + } +}; +</script> + +<style lang="scss" scoped> +.remove-token { + text-decoration: underline; + cursor: pointer; +} +</style> diff --git a/src/components/settings/Security.vue b/src/components/settings/Security.vue new file mode 100644 index 0000000..5d8aed8 --- /dev/null +++ b/src/components/settings/Security.vue @@ -0,0 +1,228 @@ +<template> + <div> + <div v-if="settingsLoaded" class="my-4"> + <!-- Change Password --> + <template v-if="!settings.disableAuth"> + <p> + {{ $t("Current User") }}: <strong>{{ $root.username }}</strong> + <button v-if="! settings.disableAuth" id="logout-btn" class="btn btn-danger ms-4 me-2 mb-2" @click="$root.logout">{{ $t("Logout") }}</button> + </p> + + <h5 class="my-4 settings-subheading">{{ $t("Change Password") }}</h5> + <form class="mb-3" @submit.prevent="savePassword"> + <div class="mb-3"> + <label for="current-password" class="form-label"> + {{ $t("Current Password") }} + </label> + <input + id="current-password" + v-model="password.currentPassword" + type="password" + class="form-control" + autocomplete="current-password" + required + /> + </div> + + <div class="mb-3"> + <label for="new-password" class="form-label"> + {{ $t("New Password") }} + </label> + <input + id="new-password" + v-model="password.newPassword" + type="password" + class="form-control" + autocomplete="new-password" + required + /> + </div> + + <div class="mb-3"> + <label for="repeat-new-password" class="form-label"> + {{ $t("Repeat New Password") }} + </label> + <input + id="repeat-new-password" + v-model="password.repeatNewPassword" + type="password" + class="form-control" + :class="{ 'is-invalid': invalidPassword }" + autocomplete="new-password" + required + /> + <div class="invalid-feedback"> + {{ $t("passwordNotMatchMsg") }} + </div> + </div> + + <div> + <button class="btn btn-primary" type="submit"> + {{ $t("Update Password") }} + </button> + </div> + </form> + </template> + + <div v-if="! settings.disableAuth" class="mt-5 mb-3"> + <h5 class="my-4 settings-subheading"> + {{ $t("Two Factor Authentication") }} + </h5> + <div class="mb-4"> + <button + class="btn btn-primary me-2" + type="button" + @click="$refs.TwoFADialog.show()" + > + {{ $t("2FA Settings") }} + </button> + </div> + </div> + + <div class="my-4"> + <!-- Advanced --> + <h5 class="my-4 settings-subheading">{{ $t("Advanced") }}</h5> + + <div class="mb-4"> + <button v-if="settings.disableAuth" id="enableAuth-btn" class="btn btn-outline-primary me-2 mb-2" @click="enableAuth">{{ $t("Enable Auth") }}</button> + <button v-if="! settings.disableAuth" id="disableAuth-btn" class="btn btn-primary me-2 mb-2" @click="confirmDisableAuth">{{ $t("Disable Auth") }}</button> + </div> + </div> + </div> + + <TwoFADialog ref="TwoFADialog" /> + + <Confirm ref="confirmDisableAuth" btn-style="btn-danger" :yes-text="$t('I understand, please disable')" :no-text="$t('Leave')" @yes="disableAuth"> + <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"> + <label for="current-password2" class="form-label"> + {{ $t("Current Password") }} + </label> + <input + id="current-password2" + v-model="password.currentPassword" + type="password" + class="form-control" + required + /> + </div> + </Confirm> + </div> +</template> + +<script> +import Confirm from "../../components/Confirm.vue"; +import TwoFADialog from "../../components/TwoFADialog.vue"; + +export default { + components: { + Confirm, + TwoFADialog + }, + + data() { + return { + invalidPassword: false, + password: { + currentPassword: "", + newPassword: "", + repeatNewPassword: "", + } + }; + }, + + computed: { + settings() { + return this.$parent.$parent.$parent.settings; + }, + saveSettings() { + return this.$parent.$parent.$parent.saveSettings; + }, + settingsLoaded() { + return this.$parent.$parent.$parent.settingsLoaded; + } + }, + + watch: { + "password.repeatNewPassword"() { + this.invalidPassword = false; + }, + }, + + methods: { + /** + * Check new passwords match before saving them + * @returns {void} + */ + savePassword() { + if (this.password.newPassword !== this.password.repeatNewPassword) { + this.invalidPassword = true; + } else { + this.$root + .getSocket() + .emit("changePassword", this.password, (res) => { + this.$root.toastRes(res); + if (res.ok) { + 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 + * @returns {void} + */ + disableAuth() { + this.settings.disableAuth = true; + + // Need current password to disable auth + // Set it to empty if done + this.saveSettings(() => { + this.password.currentPassword = ""; + this.$root.username = null; + this.$root.socket.token = "autoLogin"; + }, this.password.currentPassword); + }, + + /** + * Enable authentication for web app access + * @returns {void} + */ + enableAuth() { + this.settings.disableAuth = false; + this.saveSettings(); + this.$root.storage().removeItem("token"); + location.reload(); + }, + + /** + * Show confirmation dialog for disable auth + * @returns {void} + */ + confirmDisableAuth() { + this.$refs.confirmDisableAuth.show(); + }, + + }, +}; +</script> diff --git a/src/components/settings/Tags.vue b/src/components/settings/Tags.vue new file mode 100644 index 0000000..75ac37c --- /dev/null +++ b/src/components/settings/Tags.vue @@ -0,0 +1,175 @@ +<template> + <div class="my-4"> + <div class="mx-0 mx-lg-4 pt-1 mb-4"> + <button class="btn btn-primary" @click.stop="addTag"><font-awesome-icon icon="plus" /> {{ $t("Add New Tag") }}</button> + </div> + + <div class="tags-list my-3"> + <div v-for="(tag, index) in tagsList" :key="tag.id" class="d-flex align-items-center mx-0 mx-lg-4 py-1 tags-list-row" :disabled="processing" @click="editTag(index)"> + <div class="col-10 col-sm-5"> + <Tag :item="tag" /> + </div> + <div class="col-5 px-1 d-none d-sm-block"> + <div>{{ monitorsByTag(tag.id).length }} {{ $tc("Monitor", monitorsByTag(tag.id).length) }}</div> + </div> + <div class="col-2 pe-2 pe-lg-3 d-flex justify-content-end"> + <button type="button" class="btn-rm-tag btn btn-outline-danger ms-2 py-1" :disabled="processing" @click.stop="deleteConfirm(index)"> + <font-awesome-icon class="" icon="trash" /> + </button> + </div> + </div> + </div> + + <TagEditDialog ref="tagEditDialog" :updated="tagsUpdated" :existing-tags="tagsList" /> + <Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteTag"> + {{ $t("confirmDeleteTagMsg") }} + </Confirm> + </div> +</template> + +<script> +import TagEditDialog from "../../components/TagEditDialog.vue"; +import Tag from "../Tag.vue"; +import Confirm from "../Confirm.vue"; + +export default { + components: { + Confirm, + TagEditDialog, + Tag, + }, + + data() { + return { + processing: false, + tagsList: null, + deletingTag: null, + }; + }, + + computed: { + settings() { + return this.$parent.$parent.$parent.settings; + }, + saveSettings() { + return this.$parent.$parent.$parent.saveSettings; + }, + settingsLoaded() { + return this.$parent.$parent.$parent.settingsLoaded; + }, + }, + + mounted() { + this.getExistingTags(); + }, + + methods: { + /** + * Reflect tag changes in the UI by fetching data. Callback for the edit tag dialog. + * @returns {void} + */ + tagsUpdated() { + this.getExistingTags(); + this.$root.getMonitorList(); + }, + + /** + * Get list of tags from server + * @returns {void} + */ + getExistingTags() { + this.processing = true; + this.$root.getSocket().emit("getTags", (res) => { + this.processing = false; + if (res.ok) { + this.tagsList = res.tags; + } else { + this.$root.toastError(res.msg); + } + }); + }, + + /** + * Show confirmation for deleting a tag + * @param {number} index index of the tag to delete in the local tagsList + * @returns {void} + */ + deleteConfirm(index) { + this.deletingTag = this.tagsList[index]; + this.$refs.confirmDelete.show(); + }, + + /** + * Show dialog for adding a new tag + * @returns {void} + */ + addTag() { + this.$refs.tagEditDialog.reset(); + this.$refs.tagEditDialog.show(); + }, + + /** + * Show dialog for editing a tag + * @param {number} index index of the tag to edit in the local tagsList + * @returns {void} + */ + editTag(index) { + this.$refs.tagEditDialog.show(this.tagsList[index]); + }, + + /** + * Delete the tag "deletingTag" from server + * @returns {void} + */ + deleteTag() { + this.processing = true; + this.$root.getSocket().emit("deleteTag", this.deletingTag.id, (res) => { + this.$root.toastRes(res); + this.processing = false; + + if (res.ok) { + this.tagsUpdated(); + } + }); + }, + + /** + * 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 + */ + monitorsByTag(tagId) { + return Object.values(this.$root.monitorList).filter((monitor) => { + return monitor.tags.find(monitorTag => monitorTag.tag_id === tagId); + }); + }, + }, +}; +</script> + +<style lang="scss" scoped> +@import "../../assets/vars.scss"; + +.btn-rm-tag { + padding-left: 9px; + padding-right: 9px; +} + +.tags-list .tags-list-row { + cursor: pointer; + border-top: 1px solid rgba(0, 0, 0, 0.125); + + .dark & { + border-top: 1px solid $dark-border-color; + } + + &:hover { + background-color: $highlight-white; + } + + .dark &:hover { + background-color: $dark-bg2; + } +} + +</style> |