diff options
author | Daniel Baumann <daniel@debian.org> | 2024-11-26 09:28:28 +0100 |
---|---|---|
committer | Daniel Baumann <daniel@debian.org> | 2024-11-26 12:25:58 +0100 |
commit | a1882b67c41fe9901a0cd8059b5cc78a5beadec0 (patch) | |
tree | 2a24507c67aa99a15416707b2f7e645142230ed8 /src/mixins | |
parent | Initial commit. (diff) | |
download | uptime-kuma-a1882b67c41fe9901a0cd8059b5cc78a5beadec0.tar.xz uptime-kuma-a1882b67c41fe9901a0cd8059b5cc78a5beadec0.zip |
Adding upstream version 2.0.0~beta.0+dfsg.upstream/2.0.0_beta.0+dfsgupstream
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to 'src/mixins')
-rw-r--r-- | src/mixins/datetime.js | 138 | ||||
-rw-r--r-- | src/mixins/lang.js | 38 | ||||
-rw-r--r-- | src/mixins/mobile.js | 44 | ||||
-rw-r--r-- | src/mixins/public.js | 55 | ||||
-rw-r--r-- | src/mixins/socket.js | 879 | ||||
-rw-r--r-- | src/mixins/theme.js | 111 |
6 files changed, 1265 insertions, 0 deletions
diff --git a/src/mixins/datetime.js b/src/mixins/datetime.js new file mode 100644 index 0000000..ca2d8f0 --- /dev/null +++ b/src/mixins/datetime.js @@ -0,0 +1,138 @@ +import dayjs from "dayjs"; + +/** + * DateTime Mixin + * Handled timezone and format + */ +export default { + data() { + return { + userTimezone: localStorage.timezone || "auto", + }; + }, + + methods: { + /** + * Convert value to UTC + * @param {string | number | Date | dayjs.Dayjs} value Time + * value to convert + * @returns {dayjs.Dayjs} Converted time + */ + toUTC(value) { + return dayjs.tz(value, this.timezone).utc().format(); + }, + + /** + * Used for <input type="datetime" /> + * @param {string | number | Date | dayjs.Dayjs} value Value to + * convert + * @returns {string} Datetime string + */ + toDateTimeInputFormat(value) { + return this.datetimeFormat(value, "YYYY-MM-DDTHH:mm"); + }, + + /** + * Return a given value in the format YYYY-MM-DD HH:mm:ss + * @param {any} value Value to format as date time + * @returns {string} Formatted string + */ + datetime(value) { + return this.datetimeFormat(value, "YYYY-MM-DD HH:mm:ss"); + }, + + /** + * Converts a Unix timestamp to a formatted date and time string. + * @param {number} value - The Unix timestamp to convert. + * @returns {string} The formatted date and time string. + */ + unixToDateTime(value) { + return dayjs.unix(value).tz(this.timezone).format("YYYY-MM-DD HH:mm:ss"); + }, + + /** + * Converts a Unix timestamp to a dayjs object. + * @param {number} value - The Unix timestamp to convert. + * @returns {dayjs.Dayjs} The dayjs object representing the given timestamp. + */ + unixToDayjs(value) { + return dayjs.unix(value).tz(this.timezone); + }, + + /** + * Converts the given value to a dayjs object. + * @param {string} value - the value to be converted + * @returns {dayjs.Dayjs} a dayjs object in the timezone of this instance + */ + toDayjs(value) { + return dayjs.utc(value).tz(this.timezone); + }, + + /** + * Get time for maintenance + * @param {string | number | Date | dayjs.Dayjs} value Time to + * format + * @returns {string} Formatted string + */ + datetimeMaintenance(value) { + const inputDate = new Date(value); + const now = new Date(Date.now()); + + if (inputDate.getFullYear() === now.getUTCFullYear() && inputDate.getMonth() === now.getUTCMonth() && inputDate.getDay() === now.getUTCDay()) { + return this.datetimeFormat(value, "HH:mm"); + } else { + return this.datetimeFormat(value, "YYYY-MM-DD HH:mm"); + } + }, + + /** + * Return a given value in the format YYYY-MM-DD + * @param {any} value Value to format as date + * @returns {string} Formatted string + */ + date(value) { + return this.datetimeFormat(value, "YYYY-MM-DD"); + }, + + /** + * Return a given value in the format HH:mm or if second is set + * to true, HH:mm:ss + * @param {any} value Value to format + * @param {boolean} second Should seconds be included? + * @returns {string} Formatted string + */ + time(value, second = true) { + let secondString; + if (second) { + secondString = ":ss"; + } else { + secondString = ""; + } + return this.datetimeFormat(value, "HH:mm" + secondString); + }, + + /** + * Return a value in a custom format + * @param {any} value Value to format + * @param {any} format Format to return value in + * @returns {string} Formatted string + */ + datetimeFormat(value, format) { + if (value !== undefined && value !== "") { + return dayjs.utc(value).tz(this.timezone).format(format); + } + return ""; + }, + }, + + computed: { + timezone() { + if (this.userTimezone === "auto") { + return dayjs.tz.guess(); + } + + return this.userTimezone; + }, + } + +}; diff --git a/src/mixins/lang.js b/src/mixins/lang.js new file mode 100644 index 0000000..9061e7d --- /dev/null +++ b/src/mixins/lang.js @@ -0,0 +1,38 @@ +import { currentLocale } from "../i18n"; +import { setPageLocale } from "../util-frontend"; +const langModules = import.meta.glob("../lang/*.json"); + +export default { + data() { + return { + language: currentLocale(), + }; + }, + + async created() { + if (this.language !== "en") { + await this.changeLang(this.language); + } + }, + + watch: { + async language(lang) { + await this.changeLang(lang); + }, + }, + + methods: { + /** + * Change the application language + * @param {string} lang Language code to switch to + * @returns {Promise<void>} + */ + async changeLang(lang) { + let message = (await langModules["../lang/" + lang + ".json"]()).default; + this.$i18n.setLocaleMessage(lang, message); + this.$i18n.locale = lang; + localStorage.locale = lang; + setPageLocale(); + } + } +}; diff --git a/src/mixins/mobile.js b/src/mixins/mobile.js new file mode 100644 index 0000000..c44edcf --- /dev/null +++ b/src/mixins/mobile.js @@ -0,0 +1,44 @@ +export default { + + data() { + return { + windowWidth: window.innerWidth, + }; + }, + + created() { + window.addEventListener("resize", this.onResize); + this.updateBody(); + }, + + methods: { + /** + * Handle screen resize + * @returns {void} + */ + onResize() { + this.windowWidth = window.innerWidth; + this.updateBody(); + }, + + /** + * Add css-class "mobile" to body if needed + * @returns {void} + */ + updateBody() { + if (this.isMobile) { + document.body.classList.add("mobile"); + } else { + document.body.classList.remove("mobile"); + } + } + + }, + + computed: { + isMobile() { + return this.windowWidth <= 767.98; + }, + }, + +}; diff --git a/src/mixins/public.js b/src/mixins/public.js new file mode 100644 index 0000000..c87bfb3 --- /dev/null +++ b/src/mixins/public.js @@ -0,0 +1,55 @@ +import axios from "axios"; +import { getDevContainerServerHostname, isDevContainer } from "../util-frontend"; + +const env = process.env.NODE_ENV || "production"; + +// change the axios base url for development +if (env === "development" && isDevContainer()) { + axios.defaults.baseURL = location.protocol + "//" + getDevContainerServerHostname(); +} else if (env === "development" || localStorage.dev === "dev") { + axios.defaults.baseURL = location.protocol + "//" + location.hostname + ":3001"; +} + +export default { + data() { + return { + publicGroupList: [], + }; + }, + computed: { + publicMonitorList() { + let result = {}; + + for (let group of this.publicGroupList) { + for (let monitor of group.monitorList) { + result[monitor.id] = monitor; + } + } + return result; + }, + + publicLastHeartbeatList() { + let result = {}; + + for (let monitorID in this.publicMonitorList) { + if (this.lastHeartbeatList[monitorID]) { + result[monitorID] = this.lastHeartbeatList[monitorID]; + } + } + + return result; + }, + + baseURL() { + if (this.$root.info.primaryBaseURL) { + return this.$root.info.primaryBaseURL; + } + + if (env === "development" || localStorage.dev === "dev") { + return axios.defaults.baseURL; + } else { + return location.protocol + "//" + location.host; + } + }, + } +}; diff --git a/src/mixins/socket.js b/src/mixins/socket.js new file mode 100644 index 0000000..3272e04 --- /dev/null +++ b/src/mixins/socket.js @@ -0,0 +1,879 @@ +import { io } from "socket.io-client"; +import { useToast } from "vue-toastification"; +import jwtDecode from "jwt-decode"; +import Favico from "favico.js"; +import dayjs from "dayjs"; +import mitt from "mitt"; + +import { DOWN, MAINTENANCE, PENDING, UP } from "../util.ts"; +import { getDevContainerServerHostname, isDevContainer, getToastSuccessTimeout, getToastErrorTimeout } from "../util-frontend.js"; +const toast = useToast(); + +let socket; + +const noSocketIOPages = [ + /^\/status-page$/, // /status-page + /^\/status/, // /status** + /^\/$/ // / +]; + +const favicon = new Favico({ + animation: "none" +}); + +export default { + + data() { + return { + info: { }, + socket: { + token: null, + firstConnect: true, + connected: false, + connectCount: 0, + initedSocketIO: false, + }, + username: null, + remember: (localStorage.remember !== "0"), + allowLoginDialog: false, // Allowed to show login dialog, but "loggedIn" have to be true too. This exists because prevent the login dialog show 0.1s in first before the socket server auth-ed. + loggedIn: false, + monitorList: { }, + monitorTypeList: {}, + maintenanceList: {}, + apiKeyList: {}, + heartbeatList: { }, + avgPingList: { }, + uptimeList: { }, + tlsInfoList: {}, + notificationList: [], + dockerHostList: [], + remoteBrowserList: [], + statusPageListLoaded: false, + statusPageList: [], + proxyList: [], + connectionErrorMsg: `${this.$t("Cannot connect to the socket server.")} ${this.$t("Reconnecting...")}`, + showReverseProxyGuide: true, + cloudflared: { + cloudflareTunnelToken: "", + installed: null, + running: false, + message: "", + errorMessage: "", + currentPassword: "", + }, + faviconUpdateDebounce: null, + emitter: mitt(), + }; + }, + + created() { + this.initSocketIO(); + }, + + methods: { + + /** + * Initialize connection to socket server + * @param {boolean} bypass Should the check for if we + * are on a status page be bypassed? + * @returns {void} + */ + initSocketIO(bypass = false) { + // No need to re-init + if (this.socket.initedSocketIO) { + return; + } + + // No need to connect to the socket.io for status page + if (! bypass && location.pathname) { + for (let page of noSocketIOPages) { + if (location.pathname.match(page)) { + return; + } + } + } + + // Also don't need to connect to the socket.io for setup database page + if (location.pathname === "/setup-database") { + return; + } + + this.socket.initedSocketIO = true; + + let protocol = location.protocol + "//"; + + let url; + const env = process.env.NODE_ENV || "production"; + if (env === "development" && isDevContainer()) { + url = protocol + getDevContainerServerHostname(); + } else if (env === "development" || localStorage.dev === "dev") { + url = protocol + location.hostname + ":3001"; + } else { + // Connect to the current url + url = undefined; + } + + socket = io(url); + + socket.on("info", (info) => { + this.info = info; + }); + + socket.on("setup", (monitorID, data) => { + this.$router.push("/setup"); + }); + + socket.on("autoLogin", (monitorID, data) => { + this.loggedIn = true; + this.storage().token = "autoLogin"; + this.socket.token = "autoLogin"; + this.allowLoginDialog = false; + }); + + socket.on("loginRequired", () => { + let token = this.storage().token; + if (token && token !== "autoLogin") { + this.loginByToken(token); + } else { + this.$root.storage().removeItem("token"); + this.allowLoginDialog = true; + } + }); + + socket.on("monitorList", (data) => { + this.assignMonitorUrlParser(data); + this.monitorList = data; + }); + + socket.on("updateMonitorIntoList", (data) => { + this.assignMonitorUrlParser(data); + Object.entries(data).forEach(([ monitorID, updatedMonitor ]) => { + this.monitorList[monitorID] = updatedMonitor; + }); + }); + + socket.on("deleteMonitorFromList", (monitorID) => { + if (this.monitorList[monitorID]) { + delete this.monitorList[monitorID]; + } + }); + + socket.on("monitorTypeList", (data) => { + this.monitorTypeList = data; + }); + + socket.on("maintenanceList", (data) => { + this.maintenanceList = data; + }); + + socket.on("apiKeyList", (data) => { + this.apiKeyList = data; + }); + + socket.on("notificationList", (data) => { + this.notificationList = data; + }); + + socket.on("statusPageList", (data) => { + this.statusPageListLoaded = true; + this.statusPageList = data; + }); + + socket.on("proxyList", (data) => { + this.proxyList = data.map(item => { + item.auth = !!item.auth; + item.active = !!item.active; + item.default = !!item.default; + + return item; + }); + }); + + socket.on("dockerHostList", (data) => { + this.dockerHostList = data; + }); + + socket.on("remoteBrowserList", (data) => { + this.remoteBrowserList = data; + }); + + socket.on("heartbeat", (data) => { + if (! (data.monitorID in this.heartbeatList)) { + this.heartbeatList[data.monitorID] = []; + } + + this.heartbeatList[data.monitorID].push(data); + + if (this.heartbeatList[data.monitorID].length >= 150) { + this.heartbeatList[data.monitorID].shift(); + } + + // Add to important list if it is important + // Also toast + if (data.important) { + + if (this.monitorList[data.monitorID] !== undefined) { + if (data.status === 0) { + toast.error(`[${this.monitorList[data.monitorID].name}] [DOWN] ${data.msg}`, { + timeout: getToastErrorTimeout(), + }); + } else if (data.status === 1) { + toast.success(`[${this.monitorList[data.monitorID].name}] [Up] ${data.msg}`, { + timeout: getToastSuccessTimeout(), + }); + } else { + toast(`[${this.monitorList[data.monitorID].name}] ${data.msg}`); + } + } + + this.emitter.emit("newImportantHeartbeat", data); + } + }); + + socket.on("heartbeatList", (monitorID, data, overwrite = false) => { + if (! (monitorID in this.heartbeatList) || overwrite) { + this.heartbeatList[monitorID] = data; + } else { + this.heartbeatList[monitorID] = data.concat(this.heartbeatList[monitorID]); + } + }); + + socket.on("avgPing", (monitorID, data) => { + this.avgPingList[monitorID] = data; + }); + + socket.on("uptime", (monitorID, type, data) => { + this.uptimeList[`${monitorID}_${type}`] = data; + }); + + socket.on("certInfo", (monitorID, data) => { + this.tlsInfoList[monitorID] = JSON.parse(data); + }); + + socket.on("connect_error", (err) => { + console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`); + this.connectionErrorMsg = `${this.$t("Cannot connect to the socket server.")} [${err}] ${this.$t("Reconnecting...")}`; + this.showReverseProxyGuide = true; + this.socket.connected = false; + this.socket.firstConnect = false; + }); + + socket.on("disconnect", () => { + console.log("disconnect"); + this.connectionErrorMsg = `${this.$t("Lost connection to the socket server.")} ${this.$t("Reconnecting...")}`; + this.socket.connected = false; + }); + + socket.on("connect", () => { + console.log("Connected to the socket server"); + this.socket.connectCount++; + this.socket.connected = true; + this.showReverseProxyGuide = false; + + // Reset Heartbeat list if it is re-connect + if (this.socket.connectCount >= 2) { + this.clearData(); + } + + this.socket.firstConnect = false; + }); + + // cloudflared + socket.on("cloudflared_installed", (res) => this.cloudflared.installed = res); + socket.on("cloudflared_running", (res) => this.cloudflared.running = res); + socket.on("cloudflared_message", (res) => this.cloudflared.message = res); + socket.on("cloudflared_errorMessage", (res) => this.cloudflared.errorMessage = res); + socket.on("cloudflared_token", (res) => this.cloudflared.cloudflareTunnelToken = res); + + socket.on("initServerTimezone", () => { + socket.emit("initServerTimezone", dayjs.tz.guess()); + }); + + socket.on("refresh", () => { + location.reload(); + }); + }, + /** + * parse all urls from list. + * @param {object} data Monitor data to modify + * @returns {object} list + */ + assignMonitorUrlParser(data) { + Object.entries(data).forEach(([ monitorID, monitor ]) => { + monitor.getUrl = () => { + try { + return new URL(monitor.url); + } catch (_) { + return null; + } + }; + }); + return data; + }, + + /** + * The storage currently in use + * @returns {Storage} Current storage + */ + storage() { + return (this.remember) ? localStorage : sessionStorage; + }, + + /** + * Get payload of JWT cookie + * @returns {(object | undefined)} JWT payload + */ + getJWTPayload() { + const jwtToken = this.$root.storage().token; + + if (jwtToken && jwtToken !== "autoLogin") { + return jwtDecode(jwtToken); + } + return undefined; + }, + + /** + * Get current socket + * @returns {Socket} Current socket + */ + getSocket() { + return socket; + }, + + /** + * Show success or error toast dependent on response status code + * @param {object} res Response object + * @returns {void} + */ + toastRes(res) { + let msg = res.msg; + if (res.msgi18n) { + if (msg != null && typeof msg === "object") { + msg = this.$t(msg.key, msg.values); + } else { + msg = this.$t(msg); + } + } + + if (res.ok) { + toast.success(msg); + } else { + toast.error(msg); + } + }, + + /** + * Show a success toast + * @param {string} msg Message to show + * @returns {void} + */ + toastSuccess(msg) { + toast.success(this.$t(msg)); + }, + + /** + * Show an error toast + * @param {string} msg Message to show + * @returns {void} + */ + toastError(msg) { + toast.error(this.$t(msg)); + }, + + /** + * Callback for login + * @callback loginCB + * @param {object} res Response object + */ + + /** + * Send request to log user in + * @param {string} username Username to log in with + * @param {string} password Password to log in with + * @param {string} token User token + * @param {loginCB} callback Callback to call with result + * @returns {void} + */ + login(username, password, token, callback) { + socket.emit("login", { + username, + password, + token, + }, (res) => { + if (res.tokenRequired) { + callback(res); + } + + if (res.ok) { + this.storage().token = res.token; + this.socket.token = res.token; + this.loggedIn = true; + this.username = this.getJWTPayload()?.username; + + // Trigger Chrome Save Password + history.pushState({}, ""); + } + + callback(res); + }); + }, + + /** + * Log in using a token + * @param {string} token Token to log in with + * @returns {void} + */ + loginByToken(token) { + socket.emit("loginByToken", token, (res) => { + this.allowLoginDialog = true; + + if (! res.ok) { + this.logout(); + } else { + this.loggedIn = true; + this.username = this.getJWTPayload()?.username; + } + }); + }, + + /** + * Log out of the web application + * @returns {void} + */ + logout() { + socket.emit("logout", () => { }); + this.storage().removeItem("token"); + this.socket.token = null; + this.loggedIn = false; + this.username = null; + this.clearData(); + }, + + /** + * Callback for general socket requests + * @callback socketCB + * @param {object} res Result of operation + */ + /** + * Prepare 2FA configuration + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + prepare2FA(callback) { + socket.emit("prepare2FA", callback); + }, + + /** + * Save the current 2FA configuration + * @param {any} secret Unused + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + save2FA(secret, callback) { + socket.emit("save2FA", callback); + }, + + /** + * Disable 2FA for this user + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + disable2FA(callback) { + socket.emit("disable2FA", callback); + }, + + /** + * Verify the provided 2FA token + * @param {string} token Token to verify + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + verifyToken(token, callback) { + socket.emit("verifyToken", token, callback); + }, + + /** + * Get current 2FA status + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + twoFAStatus(callback) { + socket.emit("twoFAStatus", callback); + }, + + /** + * Get list of monitors + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + getMonitorList(callback) { + if (! callback) { + callback = () => { }; + } + socket.emit("getMonitorList", callback); + }, + + /** + * Get list of maintenances + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + getMaintenanceList(callback) { + if (! callback) { + callback = () => { }; + } + socket.emit("getMaintenanceList", callback); + }, + + /** + * Send list of API keys + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + getAPIKeyList(callback) { + if (!callback) { + callback = () => { }; + } + socket.emit("getAPIKeyList", callback); + }, + + /** + * Add a monitor + * @param {object} monitor Object representing monitor to add + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + add(monitor, callback) { + socket.emit("add", monitor, callback); + }, + + /** + * Adds a maintenance + * @param {object} maintenance Maintenance to add + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + addMaintenance(maintenance, callback) { + socket.emit("addMaintenance", maintenance, callback); + }, + + /** + * Add monitors to maintenance + * @param {number} maintenanceID Maintenance to modify + * @param {number[]} monitors IDs of monitors to add + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + addMonitorMaintenance(maintenanceID, monitors, callback) { + socket.emit("addMonitorMaintenance", maintenanceID, monitors, callback); + }, + + /** + * Add status page to maintenance + * @param {number} maintenanceID Maintenance to modify + * @param {number} statusPages ID of status page to add + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + addMaintenanceStatusPage(maintenanceID, statusPages, callback) { + socket.emit("addMaintenanceStatusPage", maintenanceID, statusPages, callback); + }, + + /** + * Get monitors affected by maintenance + * @param {number} maintenanceID Maintenance to read + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + getMonitorMaintenance(maintenanceID, callback) { + socket.emit("getMonitorMaintenance", maintenanceID, callback); + }, + + /** + * Get status pages where maintenance is shown + * @param {number} maintenanceID Maintenance to read + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + getMaintenanceStatusPage(maintenanceID, callback) { + socket.emit("getMaintenanceStatusPage", maintenanceID, callback); + }, + + /** + * Delete monitor by ID + * @param {number} monitorID ID of monitor to delete + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + deleteMonitor(monitorID, callback) { + socket.emit("deleteMonitor", monitorID, callback); + }, + + /** + * Delete specified maintenance + * @param {number} maintenanceID Maintenance to delete + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + deleteMaintenance(maintenanceID, callback) { + socket.emit("deleteMaintenance", maintenanceID, callback); + }, + + /** + * Add an API key + * @param {object} key API key to add + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + addAPIKey(key, callback) { + socket.emit("addAPIKey", key, callback); + }, + + /** + * Delete specified API key + * @param {int} keyID ID of key to delete + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + deleteAPIKey(keyID, callback) { + socket.emit("deleteAPIKey", keyID, callback); + }, + + /** + * Clear the hearbeat list + * @returns {void} + */ + clearData() { + console.log("reset heartbeat list"); + this.heartbeatList = {}; + }, + + /** + * Upload the provided backup + * @param {string} uploadedJSON JSON to upload + * @param {string} importHandle Type of import. If set to + * most data in database will be replaced + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + uploadBackup(uploadedJSON, importHandle, callback) { + socket.emit("uploadBackup", uploadedJSON, importHandle, callback); + }, + + /** + * Clear events for a specified monitor + * @param {number} monitorID ID of monitor to clear + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + clearEvents(monitorID, callback) { + socket.emit("clearEvents", monitorID, callback); + }, + + /** + * Clear the heartbeats of a specified monitor + * @param {number} monitorID Id of monitor to clear + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + clearHeartbeats(monitorID, callback) { + socket.emit("clearHeartbeats", monitorID, callback); + }, + + /** + * Clear all statistics + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + clearStatistics(callback) { + socket.emit("clearStatistics", callback); + }, + + /** + * Get monitor beats for a specific monitor in a time range + * @param {number} monitorID ID of monitor to fetch + * @param {number} period Time in hours from now + * @param {socketCB} callback Callback for socket response + * @returns {void} + */ + getMonitorBeats(monitorID, period, callback) { + socket.emit("getMonitorBeats", monitorID, period, callback); + }, + + /** + * Retrieves monitor chart data. + * @param {string} monitorID - The ID of the monitor. + * @param {number} period - The time period for the chart data, in hours. + * @param {socketCB} callback - The callback function to handle the chart data. + * @returns {void} + */ + getMonitorChartData(monitorID, period, callback) { + socket.emit("getMonitorChartData", monitorID, period, callback); + } + }, + + computed: { + + usernameFirstChar() { + if (typeof this.username == "string" && this.username.length >= 1) { + return this.username.charAt(0).toUpperCase(); + } else { + return "🐻"; + } + }, + + lastHeartbeatList() { + let result = {}; + + for (let monitorID in this.heartbeatList) { + let index = this.heartbeatList[monitorID].length - 1; + result[monitorID] = this.heartbeatList[monitorID][index]; + } + + return result; + }, + + statusList() { + let result = {}; + + let unknown = { + text: this.$t("Unknown"), + color: "secondary", + }; + + for (let monitorID in this.lastHeartbeatList) { + let lastHeartBeat = this.lastHeartbeatList[monitorID]; + + if (! lastHeartBeat) { + result[monitorID] = unknown; + } else if (lastHeartBeat.status === UP) { + result[monitorID] = { + text: this.$t("Up"), + color: "primary", + }; + } else if (lastHeartBeat.status === DOWN) { + result[monitorID] = { + text: this.$t("Down"), + color: "danger", + }; + } else if (lastHeartBeat.status === PENDING) { + result[monitorID] = { + text: this.$t("Pending"), + color: "warning", + }; + } else if (lastHeartBeat.status === MAINTENANCE) { + result[monitorID] = { + text: this.$t("statusMaintenance"), + color: "maintenance", + }; + } else { + result[monitorID] = unknown; + } + } + + return result; + }, + + stats() { + let result = { + active: 0, + up: 0, + down: 0, + maintenance: 0, + pending: 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) { + result.active++; + if (beat.status === UP) { + result.up++; + } else if (beat.status === DOWN) { + result.down++; + } else if (beat.status === PENDING) { + result.pending++; + } else if (beat.status === MAINTENANCE) { + result.maintenance++; + } else { + result.unknown++; + } + } else { + result.unknown++; + } + } + + return result; + }, + + /** + * Frontend Version + * It should be compiled to a static value while building the frontend. + * Please see ./config/vite.config.js, it is defined via vite.js + * @returns {string} Current version + */ + frontendVersion() { + // eslint-disable-next-line no-undef + return FRONTEND_VERSION; + }, + + /** + * Are both frontend and backend in the same version? + * @returns {boolean} The frontend and backend match? + */ + isFrontendBackendVersionMatched() { + if (!this.info.version) { + return true; + } + return this.info.version === this.frontendVersion; + } + }, + + watch: { + + // Update Badge + "stats.down"(to, from) { + if (to !== from) { + if (this.faviconUpdateDebounce != null) { + clearTimeout(this.faviconUpdateDebounce); + } + this.faviconUpdateDebounce = setTimeout(() => { + favicon.badge(to); + }, 1000); + } + }, + + // Reload the SPA if the server version is changed. + "info.version"(to, from) { + if (from && from !== to) { + window.location.reload(); + } + }, + + remember() { + localStorage.remember = (this.remember) ? "1" : "0"; + }, + + // Reconnect the socket io, if status-page to dashboard + "$route.fullPath"(newValue, oldValue) { + + if (newValue) { + for (let page of noSocketIOPages) { + if (newValue.match(page)) { + return; + } + } + } + + this.initSocketIO(); + }, + + }, + +}; diff --git a/src/mixins/theme.js b/src/mixins/theme.js new file mode 100644 index 0000000..e1486d5 --- /dev/null +++ b/src/mixins/theme.js @@ -0,0 +1,111 @@ +export default { + + data() { + return { + system: (window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light", + userTheme: localStorage.theme, + userHeartbeatBar: localStorage.heartbeatBarTheme, + styleElapsedTime: localStorage.styleElapsedTime, + statusPageTheme: "light", + forceStatusPageTheme: false, + path: "", + }; + }, + + mounted() { + // Default Light + if (! this.userTheme) { + this.userTheme = "auto"; + } + + // Default Heartbeat Bar + if (!this.userHeartbeatBar) { + this.userHeartbeatBar = "normal"; + } + + // Default Elapsed Time Style + if (!this.styleElapsedTime) { + this.styleElapsedTime = "no-line"; + } + + document.body.classList.add(this.theme); + this.updateThemeColorMeta(); + }, + + computed: { + theme() { + // As entry can be status page now, set forceStatusPageTheme to true to use status page theme + if (this.forceStatusPageTheme) { + if (this.statusPageTheme === "auto") { + return this.system; + } + return this.statusPageTheme; + } + + // Entry no need dark + if (this.path === "") { + return "light"; + } + + if (this.path.startsWith("/status-page") || this.path.startsWith("/status")) { + if (this.statusPageTheme === "auto") { + return this.system; + } + return this.statusPageTheme; + } else { + if (this.userTheme === "auto") { + return this.system; + } + return this.userTheme; + } + }, + + isDark() { + return this.theme === "dark"; + } + }, + + watch: { + "$route.fullPath"(path) { + this.path = path; + }, + + userTheme(to, from) { + localStorage.theme = to; + }, + + styleElapsedTime(to, from) { + localStorage.styleElapsedTime = to; + }, + + theme(to, from) { + document.body.classList.remove(from); + document.body.classList.add(this.theme); + this.updateThemeColorMeta(); + }, + + userHeartbeatBar(to, from) { + localStorage.heartbeatBarTheme = to; + }, + + heartbeatBarTheme(to, from) { + document.body.classList.remove(from); + document.body.classList.add(this.heartbeatBarTheme); + } + }, + + methods: { + /** + * Update the theme color meta tag + * @returns {void} + */ + updateThemeColorMeta() { + if (this.theme === "dark") { + document.querySelector("#theme-color").setAttribute("content", "#161B22"); + } else { + document.querySelector("#theme-color").setAttribute("content", "#5cdd8b"); + } + } + } +}; + |