mirror of
				https://github.com/louislam/uptime-kuma.git
				synced 2025-10-31 19:39:20 +08:00 
			
		
		
		
	Merge branch 'louislam:master' into group-monitors
This commit is contained in:
		| @@ -4,6 +4,7 @@ const { setSetting, setting } = require("./util-server"); | ||||
| const { log, sleep } = require("../src/util"); | ||||
| const dayjs = require("dayjs"); | ||||
| const knex = require("knex"); | ||||
| const { PluginsManager } = require("./plugins-manager"); | ||||
|  | ||||
| /** | ||||
|  * Database & App Data Folder | ||||
| @@ -86,6 +87,13 @@ class Database { | ||||
|     static init(args) { | ||||
|         // Data Directory (must be end with "/") | ||||
|         Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/"; | ||||
|  | ||||
|         // Plugin feature is working only if the dataDir = "./data"; | ||||
|         if (Database.dataDir !== "./data/") { | ||||
|             log.warn("PLUGIN", "Warning: In order to enable plugin feature, you need to use the default data directory: ./data/"); | ||||
|             PluginsManager.disable = true; | ||||
|         } | ||||
|  | ||||
|         Database.path = Database.dataDir + "kuma.db"; | ||||
|         if (! fs.existsSync(Database.dataDir)) { | ||||
|             fs.mkdirSync(Database.dataDir, { recursive: true }); | ||||
|   | ||||
							
								
								
									
										24
									
								
								server/git.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								server/git.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| const childProcess = require("child_process"); | ||||
|  | ||||
| class Git { | ||||
|  | ||||
|     static clone(repoURL, cwd, targetDir = ".") { | ||||
|         let result = childProcess.spawnSync("git", [ | ||||
|             "clone", | ||||
|             repoURL, | ||||
|             targetDir, | ||||
|         ], { | ||||
|             cwd: cwd, | ||||
|         }); | ||||
|  | ||||
|         if (result.status !== 0) { | ||||
|             throw new Error(result.stderr.toString("utf-8")); | ||||
|         } else { | ||||
|             return result.stdout.toString("utf-8") + result.stderr.toString("utf-8"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     Git, | ||||
| }; | ||||
| @@ -677,9 +677,17 @@ class Monitor extends BeanModel { | ||||
|                     bean.msg = await redisPingAsync(this.databaseConnectionString); | ||||
|                     bean.status = UP; | ||||
|                     bean.ping = dayjs().valueOf() - startTime; | ||||
|  | ||||
|                 } else if (this.type in UptimeKumaServer.monitorTypeList) { | ||||
|                     let startTime = dayjs().valueOf(); | ||||
|                     const monitorType = UptimeKumaServer.monitorTypeList[this.type]; | ||||
|                     await monitorType.check(this, bean); | ||||
|                     if (!bean.ping) { | ||||
|                         bean.ping = dayjs().valueOf() - startTime; | ||||
|                     } | ||||
|  | ||||
|                 } else { | ||||
|                     bean.msg = "Unknown Monitor Type"; | ||||
|                     bean.status = PENDING; | ||||
|                     throw new Error("Unknown Monitor Type"); | ||||
|                 } | ||||
|  | ||||
|                 if (this.isUpsideDown()) { | ||||
|   | ||||
							
								
								
									
										19
									
								
								server/monitor-types/monitor-type.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								server/monitor-types/monitor-type.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| class MonitorType { | ||||
|  | ||||
|     name = undefined; | ||||
|  | ||||
|     /** | ||||
|      * | ||||
|      * @param {Monitor} monitor | ||||
|      * @param {Heartbeat} heartbeat | ||||
|      * @returns {Promise<void>} | ||||
|      */ | ||||
|     async check(monitor, heartbeat) { | ||||
|         throw new Error("You need to override check()"); | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     MonitorType, | ||||
| }; | ||||
| @@ -8,7 +8,6 @@ class ClickSendSMS extends NotificationProvider { | ||||
|     async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { | ||||
|         let okMsg = "Sent Successfully."; | ||||
|         try { | ||||
|             console.log({ notification }); | ||||
|             let config = { | ||||
|                 headers: { | ||||
|                     "Content-Type": "application/json", | ||||
|   | ||||
							
								
								
									
										13
									
								
								server/plugin.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								server/plugin.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| class Plugin { | ||||
|     async load() { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     async unload() { | ||||
|  | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     Plugin, | ||||
| }; | ||||
							
								
								
									
										235
									
								
								server/plugins-manager.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										235
									
								
								server/plugins-manager.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,235 @@ | ||||
| const fs = require("fs"); | ||||
| const { log } = require("../src/util"); | ||||
| const path = require("path"); | ||||
| const axios = require("axios"); | ||||
| const { Git } = require("./git"); | ||||
| const childProcess = require("child_process"); | ||||
|  | ||||
| class PluginsManager { | ||||
|  | ||||
|     static disable = false; | ||||
|  | ||||
|     /** | ||||
|      * Plugin List | ||||
|      * @type {PluginWrapper[]} | ||||
|      */ | ||||
|     pluginList = []; | ||||
|  | ||||
|     /** | ||||
|      * Plugins Dir | ||||
|      */ | ||||
|     pluginsDir; | ||||
|  | ||||
|     server; | ||||
|  | ||||
|     /** | ||||
|      * | ||||
|      * @param {UptimeKumaServer} server | ||||
|      */ | ||||
|     constructor(server) { | ||||
|         this.server = server; | ||||
|  | ||||
|         if (!PluginsManager.disable) { | ||||
|             this.pluginsDir = "./data/plugins/"; | ||||
|  | ||||
|             if (! fs.existsSync(this.pluginsDir)) { | ||||
|                 fs.mkdirSync(this.pluginsDir, { recursive: true }); | ||||
|             } | ||||
|  | ||||
|             log.debug("plugin", "Scanning plugin directory"); | ||||
|             let list = fs.readdirSync(this.pluginsDir); | ||||
|  | ||||
|             this.pluginList = []; | ||||
|             for (let item of list) { | ||||
|                 this.loadPlugin(item); | ||||
|             } | ||||
|  | ||||
|         } else { | ||||
|             log.warn("PLUGIN", "Skip scanning plugin directory"); | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Install a Plugin | ||||
|      */ | ||||
|     async loadPlugin(name) { | ||||
|         log.info("plugin", "Load " + name); | ||||
|         let plugin = new PluginWrapper(this.server, this.pluginsDir + name); | ||||
|  | ||||
|         try { | ||||
|             await plugin.load(); | ||||
|             this.pluginList.push(plugin); | ||||
|         } catch (e) { | ||||
|             log.error("plugin", "Failed to load plugin: " + this.pluginsDir + name); | ||||
|             log.error("plugin", "Reason: " + e.message); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Download a Plugin | ||||
|      * @param {string} repoURL Git repo url | ||||
|      * @param {string} name Directory name, also known as plugin unique name | ||||
|      */ | ||||
|     downloadPlugin(repoURL, name) { | ||||
|         log.info("plugin", "Installing plugin: " + name + " " + repoURL); | ||||
|         let result = Git.clone(repoURL, this.pluginsDir, name); | ||||
|         log.info("plugin", "Install result: " + result); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Remove a plugin | ||||
|      * @param {string} name | ||||
|      */ | ||||
|     async removePlugin(name) { | ||||
|         log.info("plugin", "Removing plugin: " + name); | ||||
|         for (let plugin of this.pluginList) { | ||||
|             if (plugin.info.name === name) { | ||||
|                 await plugin.unload(); | ||||
|  | ||||
|                 // Delete the plugin directory | ||||
|                 fs.rmSync(this.pluginsDir + name, { | ||||
|                     recursive: true | ||||
|                 }); | ||||
|  | ||||
|                 this.pluginList.splice(this.pluginList.indexOf(plugin), 1); | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
|         log.warn("plugin", "Plugin not found: " + name); | ||||
|         throw new Error("Plugin not found: " + name); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * TODO: Update a plugin | ||||
|      * Only available for plugins which were downloaded from the official list | ||||
|      * @param pluginID | ||||
|      */ | ||||
|     updatePlugin(pluginID) { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get the plugin list from server + local installed plugin list | ||||
|      * Item will be merged if the `name` is the same. | ||||
|      * @returns {Promise<[]>} | ||||
|      */ | ||||
|     async fetchPluginList() { | ||||
|         const res = await axios.get("https://uptime.kuma.pet/c/plugins.json"); | ||||
|         const list = res.data.pluginList; | ||||
|  | ||||
|         for (let plugin of this.pluginList) { | ||||
|             let find = false; | ||||
|             // Try to merge | ||||
|             for (let remotePlugin of list) { | ||||
|                 if (remotePlugin.name === plugin.info.name) { | ||||
|                     find = true; | ||||
|                     remotePlugin.installed = true; | ||||
|                     remotePlugin.name = plugin.info.name; | ||||
|                     remotePlugin.fullName = plugin.info.fullName; | ||||
|                     remotePlugin.description = plugin.info.description; | ||||
|                     remotePlugin.version = plugin.info.version; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // Local plugin | ||||
|             if (!find) { | ||||
|                 plugin.info.local = true; | ||||
|                 list.push(plugin.info); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Sort Installed first, then sort by name | ||||
|         return list.sort((a, b) => { | ||||
|             if (a.installed === b.installed) { | ||||
|                 if ( a.fullName < b.fullName ) { | ||||
|                     return -1; | ||||
|                 } | ||||
|                 if ( a.fullName > b.fullName ) { | ||||
|                     return 1; | ||||
|                 } | ||||
|                 return 0; | ||||
|             } else if (a.installed) { | ||||
|                 return -1; | ||||
|             } else { | ||||
|                 return 1; | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| class PluginWrapper { | ||||
|  | ||||
|     server = undefined; | ||||
|     pluginDir = undefined; | ||||
|  | ||||
|     /** | ||||
|      * Must be an `new-able` class. | ||||
|      * @type {function} | ||||
|      */ | ||||
|     pluginClass = undefined; | ||||
|  | ||||
|     /** | ||||
|      * | ||||
|      * @type {Plugin} | ||||
|      */ | ||||
|     object = undefined; | ||||
|     info = {}; | ||||
|  | ||||
|     /** | ||||
|      * | ||||
|      * @param {UptimeKumaServer} server | ||||
|      * @param {string} pluginDir | ||||
|      */ | ||||
|     constructor(server, pluginDir) { | ||||
|         this.server = server; | ||||
|         this.pluginDir = pluginDir; | ||||
|     } | ||||
|  | ||||
|     async load() { | ||||
|         let indexFile = this.pluginDir + "/index.js"; | ||||
|         let packageJSON = this.pluginDir + "/package.json"; | ||||
|  | ||||
|         if (fs.existsSync(indexFile)) { | ||||
|             // Install dependencies | ||||
|             childProcess.execSync("npm install", { | ||||
|                 cwd: this.pluginDir, | ||||
|                 env: { | ||||
|                     PLAYWRIGHT_BROWSERS_PATH: "../../browsers",    // Special handling for read-browser-monitor | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             this.pluginClass = require(path.join(process.cwd(), indexFile)); | ||||
|  | ||||
|             let pluginClassType = typeof this.pluginClass; | ||||
|  | ||||
|             if (pluginClassType === "function") { | ||||
|                 this.object = new this.pluginClass(this.server); | ||||
|                 await this.object.load(); | ||||
|             } else { | ||||
|                 throw new Error("Invalid plugin, it does not export a class"); | ||||
|             } | ||||
|  | ||||
|             if (fs.existsSync(packageJSON)) { | ||||
|                 this.info = require(path.join(process.cwd(), packageJSON)); | ||||
|             } else { | ||||
|                 this.info.fullName = this.pluginDir; | ||||
|                 this.info.name = "[unknown]"; | ||||
|                 this.info.version = "[unknown-version]"; | ||||
|             } | ||||
|  | ||||
|             this.info.installed = true; | ||||
|             log.info("plugin", `${this.info.fullName} v${this.info.version} loaded`); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async unload() { | ||||
|         await this.object.unload(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     PluginsManager, | ||||
|     PluginWrapper | ||||
| }; | ||||
| @@ -138,6 +138,7 @@ const { maintenanceSocketHandler } = require("./socket-handlers/maintenance-sock | ||||
| const { generalSocketHandler } = require("./socket-handlers/general-socket-handler"); | ||||
| const { Settings } = require("./settings"); | ||||
| const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent"); | ||||
| const { pluginsHandler } = require("./socket-handlers/plugins-handler"); | ||||
|  | ||||
| app.use(express.json()); | ||||
|  | ||||
| @@ -166,7 +167,7 @@ let needSetup = false; | ||||
|     Database.init(args); | ||||
|     await initDatabase(testMode); | ||||
|     await server.initAfterDatabaseReady(); | ||||
|  | ||||
|     server.loadPlugins(); | ||||
|     server.entryPage = await Settings.get("entryPage"); | ||||
|     await StatusPage.loadDomainMappingList(); | ||||
|  | ||||
| @@ -574,7 +575,6 @@ let needSetup = false; | ||||
|                     }); | ||||
|                 } | ||||
|             } catch (error) { | ||||
|                 console.log(error); | ||||
|                 callback({ | ||||
|                     ok: false, | ||||
|                     msg: error.message, | ||||
| @@ -1502,6 +1502,7 @@ let needSetup = false; | ||||
|         dockerSocketHandler(socket); | ||||
|         maintenanceSocketHandler(socket); | ||||
|         generalSocketHandler(socket, server); | ||||
|         pluginsHandler(socket, server); | ||||
|  | ||||
|         log.debug("server", "added all socket handlers"); | ||||
|  | ||||
|   | ||||
							
								
								
									
										65
									
								
								server/socket-handlers/plugins-handler.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								server/socket-handlers/plugins-handler.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| const { checkLogin } = require("../util-server"); | ||||
| const { PluginManager } = require("../plugins-manager"); | ||||
|  | ||||
| /** | ||||
|  * Handlers for plugins | ||||
|  * @param {Socket} socket Socket.io instance | ||||
|  * @param {UptimeKumaServer} server | ||||
|  */ | ||||
| module.exports.pluginsHandler = (socket, server) => { | ||||
|  | ||||
|     const pluginManager = server.getPluginManager(); | ||||
|  | ||||
|     // Get Plugin List | ||||
|     socket.on("getPluginList", async (callback) => { | ||||
|         try { | ||||
|             checkLogin(socket); | ||||
|  | ||||
|             if (PluginManager.disable) { | ||||
|                 throw new Error("Plugin Disabled: In order to enable plugin feature, you need to use the default data directory: ./data/"); | ||||
|             } | ||||
|  | ||||
|             let pluginList = await pluginManager.fetchPluginList(); | ||||
|             callback({ | ||||
|                 ok: true, | ||||
|                 pluginList, | ||||
|             }); | ||||
|         } catch (error) { | ||||
|             callback({ | ||||
|                 ok: false, | ||||
|                 msg: error.message, | ||||
|             }); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     socket.on("installPlugin", async (repoURL, name, callback) => { | ||||
|         try { | ||||
|             checkLogin(socket); | ||||
|             pluginManager.downloadPlugin(repoURL, name); | ||||
|             await pluginManager.loadPlugin(name); | ||||
|             callback({ | ||||
|                 ok: true, | ||||
|             }); | ||||
|         } catch (error) { | ||||
|             callback({ | ||||
|                 ok: false, | ||||
|                 msg: error.message, | ||||
|             }); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     socket.on("uninstallPlugin", async (name, callback) => { | ||||
|         try { | ||||
|             checkLogin(socket); | ||||
|             await pluginManager.removePlugin(name); | ||||
|             callback({ | ||||
|                 ok: true, | ||||
|             }); | ||||
|         } catch (error) { | ||||
|             callback({ | ||||
|                 ok: false, | ||||
|                 msg: error.message, | ||||
|             }); | ||||
|         } | ||||
|     }); | ||||
| }; | ||||
| @@ -10,6 +10,7 @@ const util = require("util"); | ||||
| const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent"); | ||||
| const { Settings } = require("./settings"); | ||||
| const dayjs = require("dayjs"); | ||||
| const { PluginsManager } = require("./plugins-manager"); | ||||
| // DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()` | ||||
|  | ||||
| /** | ||||
| @@ -48,6 +49,20 @@ class UptimeKumaServer { | ||||
|  | ||||
|     generateMaintenanceTimeslotsInterval = undefined; | ||||
|  | ||||
|     /** | ||||
|      * Plugins Manager | ||||
|      * @type {PluginsManager} | ||||
|      */ | ||||
|     pluginsManager = null; | ||||
|  | ||||
|     /** | ||||
|      * | ||||
|      * @type {{}} | ||||
|      */ | ||||
|     static monitorTypeList = { | ||||
|  | ||||
|     }; | ||||
|  | ||||
|     static getInstance(args) { | ||||
|         if (UptimeKumaServer.instance == null) { | ||||
|             UptimeKumaServer.instance = new UptimeKumaServer(args); | ||||
| @@ -272,6 +287,46 @@ class UptimeKumaServer { | ||||
|     async stop() { | ||||
|         clearTimeout(this.generateMaintenanceTimeslotsInterval); | ||||
|     } | ||||
|  | ||||
|     loadPlugins() { | ||||
|         this.pluginsManager = new PluginsManager(this); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * | ||||
|      * @returns {PluginsManager} | ||||
|      */ | ||||
|     getPluginManager() { | ||||
|         return this.pluginsManager; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * | ||||
|      * @param {MonitorType} monitorType | ||||
|      */ | ||||
|     addMonitorType(monitorType) { | ||||
|         if (monitorType instanceof MonitorType && monitorType.name) { | ||||
|             if (monitorType.name in UptimeKumaServer.monitorTypeList) { | ||||
|                 log.error("", "Conflict Monitor Type name"); | ||||
|             } | ||||
|             UptimeKumaServer.monitorTypeList[monitorType.name] = monitorType; | ||||
|         } else { | ||||
|             log.error("", "Invalid Monitor Type: " + monitorType.name); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * | ||||
|      * @param {MonitorType} monitorType | ||||
|      */ | ||||
|     removeMonitorType(monitorType) { | ||||
|         if (UptimeKumaServer.monitorTypeList[monitorType.name] === monitorType) { | ||||
|             delete UptimeKumaServer.monitorTypeList[monitorType.name]; | ||||
|         } else { | ||||
|             log.error("", "Remove MonitorType failed: " + monitorType.name); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
| @@ -280,3 +335,4 @@ module.exports = { | ||||
|  | ||||
| // Must be at the end | ||||
| const MaintenanceTimeslot = require("./model/maintenance_timeslot"); | ||||
| const { MonitorType } = require("./monitor-types/monitor-type"); | ||||
|   | ||||
							
								
								
									
										102
									
								
								src/components/PluginItem.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								src/components/PluginItem.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | ||||
| <template> | ||||
|     <div v-if="! (!plugin.installed && plugin.local)" class="plugin-item pt-4 pb-2"> | ||||
|         <div class="info"> | ||||
|             <h5>{{ plugin.fullName }}</h5> | ||||
|             <p class="description"> | ||||
|                 {{ plugin.description }} | ||||
|             </p> | ||||
|             <span class="version">{{ $t("Version") }}: {{ plugin.version }} <a v-if="plugin.repo" :href="plugin.repo" target="_blank">Repo</a></span> | ||||
|         </div> | ||||
|         <div class="buttons"> | ||||
|             <button v-if="status === 'installing'" class="btn btn-primary" disabled>{{ $t("installing") }}</button> | ||||
|             <button v-else-if="status === 'uninstalling'" class="btn btn-danger" disabled>{{ $t("uninstalling") }}</button> | ||||
|             <button v-else-if="plugin.installed || status === 'installed'" class="btn btn-danger" @click="deleteConfirm">{{ $t("uninstall") }}</button> | ||||
|             <button v-else class="btn btn-primary" @click="install">{{ $t("install") }}</button> | ||||
|         </div> | ||||
|  | ||||
|         <Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="uninstall"> | ||||
|             {{ $t("confirmUninstallPlugin") }} | ||||
|         </Confirm> | ||||
|     </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import Confirm from "./Confirm.vue"; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         Confirm, | ||||
|     }, | ||||
|     props: { | ||||
|         plugin: { | ||||
|             type: Object, | ||||
|             required: true, | ||||
|         }, | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             status: "", | ||||
|         }; | ||||
|     }, | ||||
|     methods: { | ||||
|         /** | ||||
|          * Show confirmation for deleting a tag | ||||
|          */ | ||||
|         deleteConfirm() { | ||||
|             this.$refs.confirmDelete.show(); | ||||
|         }, | ||||
|  | ||||
|         install() { | ||||
|             this.status = "installing"; | ||||
|  | ||||
|             this.$root.getSocket().emit("installPlugin", this.plugin.repo, this.plugin.name, (res) => { | ||||
|                 if (res.ok) { | ||||
|                     this.status = ""; | ||||
|                     // eslint-disable-next-line vue/no-mutating-props | ||||
|                     this.plugin.installed = true; | ||||
|                 } else { | ||||
|                     this.$root.toastRes(res); | ||||
|                 } | ||||
|             }); | ||||
|         }, | ||||
|  | ||||
|         uninstall() { | ||||
|             this.status = "uninstalling"; | ||||
|  | ||||
|             this.$root.getSocket().emit("uninstallPlugin", this.plugin.name, (res) => { | ||||
|                 if (res.ok) { | ||||
|                     this.status = ""; | ||||
|                     // eslint-disable-next-line vue/no-mutating-props | ||||
|                     this.plugin.installed = false; | ||||
|                 } else { | ||||
|                     this.$root.toastRes(res); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| @import "../assets/vars.scss"; | ||||
|  | ||||
| .plugin-item { | ||||
|     display: flex; | ||||
|     justify-content: space-between; | ||||
|     align-content: center; | ||||
|     align-items: center; | ||||
|  | ||||
|     .info { | ||||
|         margin-right: 10px; | ||||
|     } | ||||
|  | ||||
|     .description { | ||||
|         font-size: 13px; | ||||
|         margin-bottom: 0; | ||||
|     } | ||||
|  | ||||
|     .version { | ||||
|         font-size: 13px; | ||||
|     } | ||||
| } | ||||
| </style> | ||||
| @@ -33,7 +33,13 @@ export default { | ||||
|             let key = this.monitor.id + "_" + this.type; | ||||
|  | ||||
|             if (this.$root.uptimeList[key] !== undefined) { | ||||
|                 return Math.round(this.$root.uptimeList[key] * 10000) / 100 + "%"; | ||||
|                 let result = Math.round(this.$root.uptimeList[key] * 10000) / 100; | ||||
|                 // Only perform sanity check on status page. See louislam/uptime-kuma#2628 | ||||
|                 if (this.$route.path.startsWith("/status") && result > 100) { | ||||
|                     return "100%"; | ||||
|                 } else { | ||||
|                     return result + "%"; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return this.$t("notAvailableShort"); | ||||
|   | ||||
							
								
								
									
										57
									
								
								src/components/settings/Plugins.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/components/settings/Plugins.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| <template> | ||||
|     <div> | ||||
|         <div class="mt-3">{{ remotePluginListMsg }}</div> | ||||
|         <PluginItem v-for="plugin in remotePluginList" :key="plugin.id" :plugin="plugin" /> | ||||
|     </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import PluginItem from "../PluginItem.vue"; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         PluginItem | ||||
|     }, | ||||
|  | ||||
|     data() { | ||||
|         return { | ||||
|             remotePluginList: [], | ||||
|             remotePluginListMsg: "", | ||||
|         }; | ||||
|     }, | ||||
|  | ||||
|     computed: { | ||||
|         pluginList() { | ||||
|             return this.$parent.$parent.$parent.pluginList; | ||||
|         }, | ||||
|         settings() { | ||||
|             return this.$parent.$parent.$parent.settings; | ||||
|         }, | ||||
|         saveSettings() { | ||||
|             return this.$parent.$parent.$parent.saveSettings; | ||||
|         }, | ||||
|         settingsLoaded() { | ||||
|             return this.$parent.$parent.$parent.settingsLoaded; | ||||
|         }, | ||||
|     }, | ||||
|  | ||||
|     async mounted() { | ||||
|         this.loadList(); | ||||
|     }, | ||||
|  | ||||
|     methods: { | ||||
|         loadList() { | ||||
|             this.remotePluginListMsg = this.$t("Loading") + "..."; | ||||
|  | ||||
|             this.$root.getSocket().emit("getPluginList", (res) => { | ||||
|                 if (res.ok) { | ||||
|                     this.remotePluginList = res.pluginList; | ||||
|                     this.remotePluginListMsg = ""; | ||||
|                 } else { | ||||
|                     this.remotePluginListMsg = this.$t("loadingError") + " " + res.message; | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| }; | ||||
| </script> | ||||
| @@ -3,6 +3,9 @@ | ||||
| (2023-01-24 Updated) | ||||
|  | ||||
| 1. Go to [https://weblate.kuma.pet](https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/) | ||||
| 2. Register an account on Weblate | ||||
| 3. Make sure your GitHub email is matched with Weblate's account, so that it could show you as a contributor on GitHub | ||||
| 4. Choose your language on Weblate and start translating. | ||||
|  | ||||
| # How to add a new language in the dropdown | ||||
|  | ||||
|   | ||||
| @@ -428,6 +428,13 @@ | ||||
|     "Schedule Maintenance": "Schedule Maintenance", | ||||
|     "Date and Time": "Date and Time", | ||||
|     "DateTime Range": "DateTime Range", | ||||
|     "loadingError": "Cannot fetch the data, please try again later.", | ||||
|     "plugin": "Plugin | Plugins", | ||||
|     "install": "Install", | ||||
|     "installing": "Installing", | ||||
|     "uninstall": "Uninstall", | ||||
|     "uninstalling": "Uninstalling", | ||||
|     "confirmUninstallPlugin": "Are you sure want to uninstall this plugin?", | ||||
|     "smtp": "Email (SMTP)", | ||||
|     "secureOptionNone": "None / STARTTLS (25, 587)", | ||||
|     "secureOptionTLS": "TLS (465)", | ||||
|   | ||||
| @@ -73,6 +73,12 @@ | ||||
|                                             Redis | ||||
|                                         </option> | ||||
|                                     </optgroup> | ||||
|  | ||||
|                                     <optgroup :label="$t('Custom Monitor Type')"> | ||||
|                                         <option value="browser"> | ||||
|                                             (Beta) HTTP(s) - Browser Engine (Chrome/Firefox) | ||||
|                                         </option> | ||||
|                                     </optgroup> | ||||
|                                 </select> | ||||
|                             </div> | ||||
|  | ||||
| @@ -92,7 +98,7 @@ | ||||
|                             </div> | ||||
|  | ||||
|                             <!-- URL --> | ||||
|                             <div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3"> | ||||
|                             <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'browser' " class="my-3"> | ||||
|                                 <label for="url" class="form-label">{{ $t("URL") }}</label> | ||||
|                                 <input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required> | ||||
|                             </div> | ||||
| @@ -114,7 +120,7 @@ | ||||
|                             </div> | ||||
|  | ||||
|                             <!-- Keyword --> | ||||
|                             <div v-if="monitor.type === 'keyword' || monitor.type === 'grpc-keyword' " class="my-3"> | ||||
|                             <div v-if="monitor.type === 'keyword' || monitor.type === 'grpc-keyword'" class="my-3"> | ||||
|                                 <label for="keyword" class="form-label">{{ $t("Keyword") }}</label> | ||||
|                                 <input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required> | ||||
|                                 <div class="form-text"> | ||||
|   | ||||
| @@ -113,6 +113,9 @@ export default { | ||||
|                 backup: { | ||||
|                     title: this.$t("Backup"), | ||||
|                 }, | ||||
|                 plugins: { | ||||
|                     title: this.$tc("plugin", 2), | ||||
|                 }, | ||||
|                 about: { | ||||
|                     title: this.$t("About"), | ||||
|                 }, | ||||
|   | ||||
| @@ -18,6 +18,7 @@ import NotFound from "./pages/NotFound.vue"; | ||||
| import DockerHosts from "./components/settings/Docker.vue"; | ||||
| import MaintenanceDetails from "./pages/MaintenanceDetails.vue"; | ||||
| import ManageMaintenance from "./pages/ManageMaintenance.vue"; | ||||
| import Plugins from "./components/settings/Plugins.vue"; | ||||
|  | ||||
| // Settings - Sub Pages | ||||
| import Appearance from "./components/settings/Appearance.vue"; | ||||
| @@ -31,6 +32,7 @@ import Proxies from "./components/settings/Proxies.vue"; | ||||
| import Backup from "./components/settings/Backup.vue"; | ||||
| import About from "./components/settings/About.vue"; | ||||
|  | ||||
|  | ||||
| const routes = [ | ||||
|     { | ||||
|         path: "/", | ||||
| @@ -120,6 +122,10 @@ const routes = [ | ||||
|                                 path: "backup", | ||||
|                                 component: Backup, | ||||
|                             }, | ||||
|                             { | ||||
|                                 path: "plugins", | ||||
|                                 component: Plugins, | ||||
|                             }, | ||||
|                             { | ||||
|                                 path: "about", | ||||
|                                 component: About, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user