summaryrefslogtreecommitdiffstats
path: root/src/mixins
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-11-26 09:28:28 +0100
committerDaniel Baumann <daniel@debian.org>2024-11-26 12:25:58 +0100
commita1882b67c41fe9901a0cd8059b5cc78a5beadec0 (patch)
tree2a24507c67aa99a15416707b2f7e645142230ed8 /src/mixins
parentInitial commit. (diff)
downloaduptime-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.js138
-rw-r--r--src/mixins/lang.js38
-rw-r--r--src/mixins/mobile.js44
-rw-r--r--src/mixins/public.js55
-rw-r--r--src/mixins/socket.js879
-rw-r--r--src/mixins/theme.js111
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");
+ }
+ }
+ }
+};
+