mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-09-07 18:02:42 +00:00
Merge branch 'louislam:master' into group-monitors
This commit is contained in:
commit
97bd306a09
@ -4,6 +4,7 @@ const { setSetting, setting } = require("./util-server");
|
|||||||
const { log, sleep } = require("../src/util");
|
const { log, sleep } = require("../src/util");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const knex = require("knex");
|
const knex = require("knex");
|
||||||
|
const { PluginsManager } = require("./plugins-manager");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database & App Data Folder
|
* Database & App Data Folder
|
||||||
@ -86,6 +87,13 @@ class Database {
|
|||||||
static init(args) {
|
static init(args) {
|
||||||
// Data Directory (must be end with "/")
|
// Data Directory (must be end with "/")
|
||||||
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
|
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";
|
Database.path = Database.dataDir + "kuma.db";
|
||||||
if (! fs.existsSync(Database.dataDir)) {
|
if (! fs.existsSync(Database.dataDir)) {
|
||||||
fs.mkdirSync(Database.dataDir, { recursive: true });
|
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.msg = await redisPingAsync(this.databaseConnectionString);
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
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 {
|
} else {
|
||||||
bean.msg = "Unknown Monitor Type";
|
throw new Error("Unknown Monitor Type");
|
||||||
bean.status = PENDING;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isUpsideDown()) {
|
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) {
|
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||||
let okMsg = "Sent Successfully.";
|
let okMsg = "Sent Successfully.";
|
||||||
try {
|
try {
|
||||||
console.log({ notification });
|
|
||||||
let config = {
|
let config = {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"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 { generalSocketHandler } = require("./socket-handlers/general-socket-handler");
|
||||||
const { Settings } = require("./settings");
|
const { Settings } = require("./settings");
|
||||||
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
||||||
|
const { pluginsHandler } = require("./socket-handlers/plugins-handler");
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
@ -166,7 +167,7 @@ let needSetup = false;
|
|||||||
Database.init(args);
|
Database.init(args);
|
||||||
await initDatabase(testMode);
|
await initDatabase(testMode);
|
||||||
await server.initAfterDatabaseReady();
|
await server.initAfterDatabaseReady();
|
||||||
|
server.loadPlugins();
|
||||||
server.entryPage = await Settings.get("entryPage");
|
server.entryPage = await Settings.get("entryPage");
|
||||||
await StatusPage.loadDomainMappingList();
|
await StatusPage.loadDomainMappingList();
|
||||||
|
|
||||||
@ -574,7 +575,6 @@ let needSetup = false;
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: error.message,
|
msg: error.message,
|
||||||
@ -1502,6 +1502,7 @@ let needSetup = false;
|
|||||||
dockerSocketHandler(socket);
|
dockerSocketHandler(socket);
|
||||||
maintenanceSocketHandler(socket);
|
maintenanceSocketHandler(socket);
|
||||||
generalSocketHandler(socket, server);
|
generalSocketHandler(socket, server);
|
||||||
|
pluginsHandler(socket, server);
|
||||||
|
|
||||||
log.debug("server", "added all socket handlers");
|
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 { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
|
||||||
const { Settings } = require("./settings");
|
const { Settings } = require("./settings");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
|
const { PluginsManager } = require("./plugins-manager");
|
||||||
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`
|
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -48,6 +49,20 @@ class UptimeKumaServer {
|
|||||||
|
|
||||||
generateMaintenanceTimeslotsInterval = undefined;
|
generateMaintenanceTimeslotsInterval = undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugins Manager
|
||||||
|
* @type {PluginsManager}
|
||||||
|
*/
|
||||||
|
pluginsManager = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {{}}
|
||||||
|
*/
|
||||||
|
static monitorTypeList = {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
static getInstance(args) {
|
static getInstance(args) {
|
||||||
if (UptimeKumaServer.instance == null) {
|
if (UptimeKumaServer.instance == null) {
|
||||||
UptimeKumaServer.instance = new UptimeKumaServer(args);
|
UptimeKumaServer.instance = new UptimeKumaServer(args);
|
||||||
@ -272,6 +287,46 @@ class UptimeKumaServer {
|
|||||||
async stop() {
|
async stop() {
|
||||||
clearTimeout(this.generateMaintenanceTimeslotsInterval);
|
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 = {
|
module.exports = {
|
||||||
@ -280,3 +335,4 @@ module.exports = {
|
|||||||
|
|
||||||
// Must be at the end
|
// Must be at the end
|
||||||
const MaintenanceTimeslot = require("./model/maintenance_timeslot");
|
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;
|
let key = this.monitor.id + "_" + this.type;
|
||||||
|
|
||||||
if (this.$root.uptimeList[key] !== undefined) {
|
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");
|
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)
|
(2023-01-24 Updated)
|
||||||
|
|
||||||
1. Go to [https://weblate.kuma.pet](https://weblate.kuma.pet/projects/uptime-kuma/uptime-kuma/)
|
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
|
# How to add a new language in the dropdown
|
||||||
|
|
||||||
|
@ -428,6 +428,13 @@
|
|||||||
"Schedule Maintenance": "Schedule Maintenance",
|
"Schedule Maintenance": "Schedule Maintenance",
|
||||||
"Date and Time": "Date and Time",
|
"Date and Time": "Date and Time",
|
||||||
"DateTime Range": "DateTime Range",
|
"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)",
|
"smtp": "Email (SMTP)",
|
||||||
"secureOptionNone": "None / STARTTLS (25, 587)",
|
"secureOptionNone": "None / STARTTLS (25, 587)",
|
||||||
"secureOptionTLS": "TLS (465)",
|
"secureOptionTLS": "TLS (465)",
|
||||||
|
@ -73,6 +73,12 @@
|
|||||||
Redis
|
Redis
|
||||||
</option>
|
</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
|
|
||||||
|
<optgroup :label="$t('Custom Monitor Type')">
|
||||||
|
<option value="browser">
|
||||||
|
(Beta) HTTP(s) - Browser Engine (Chrome/Firefox)
|
||||||
|
</option>
|
||||||
|
</optgroup>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -92,7 +98,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- URL -->
|
<!-- 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>
|
<label for="url" class="form-label">{{ $t("URL") }}</label>
|
||||||
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
|
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
|
||||||
</div>
|
</div>
|
||||||
|
@ -113,6 +113,9 @@ export default {
|
|||||||
backup: {
|
backup: {
|
||||||
title: this.$t("Backup"),
|
title: this.$t("Backup"),
|
||||||
},
|
},
|
||||||
|
plugins: {
|
||||||
|
title: this.$tc("plugin", 2),
|
||||||
|
},
|
||||||
about: {
|
about: {
|
||||||
title: this.$t("About"),
|
title: this.$t("About"),
|
||||||
},
|
},
|
||||||
|
@ -18,6 +18,7 @@ import NotFound from "./pages/NotFound.vue";
|
|||||||
import DockerHosts from "./components/settings/Docker.vue";
|
import DockerHosts from "./components/settings/Docker.vue";
|
||||||
import MaintenanceDetails from "./pages/MaintenanceDetails.vue";
|
import MaintenanceDetails from "./pages/MaintenanceDetails.vue";
|
||||||
import ManageMaintenance from "./pages/ManageMaintenance.vue";
|
import ManageMaintenance from "./pages/ManageMaintenance.vue";
|
||||||
|
import Plugins from "./components/settings/Plugins.vue";
|
||||||
|
|
||||||
// Settings - Sub Pages
|
// Settings - Sub Pages
|
||||||
import Appearance from "./components/settings/Appearance.vue";
|
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 Backup from "./components/settings/Backup.vue";
|
||||||
import About from "./components/settings/About.vue";
|
import About from "./components/settings/About.vue";
|
||||||
|
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
@ -120,6 +122,10 @@ const routes = [
|
|||||||
path: "backup",
|
path: "backup",
|
||||||
component: Backup,
|
component: Backup,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "plugins",
|
||||||
|
component: Plugins,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "about",
|
path: "about",
|
||||||
component: About,
|
component: About,
|
||||||
|
Loading…
Reference in New Issue
Block a user