gui plugin admin updates part 2

Ryan Blenis committed Nov 1, 2019 at 16:49 UTC 0516b0afd336fefe5c74180e75b2e42d7510b135
6 files changed +353 -96
db.js
+5 -3
@@ -753,16 +753,18 @@ module.exports.CreateDB = function (parent, func) {
753 }
754
755 // Add a plugin
756 - obj.addPlugin = function (plugin) { obj.pluginsfile.insertOne(plugin); };
756 + obj.addPlugin = function (plugin, func) { obj.pluginsfile.insertOne(plugin, func); };
757
758 // Get all plugins
759 obj.getPlugins = function (func) { obj.pluginsfile.find().sort({ name: 1 }).toArray(func); };
760
761 // Get plugin
762 - obj.getPlugin = function (id, func) { obj.pluginsfile.find({ _id: id }).sort({ name: 1 }).toArray(func); };
762 + obj.getPlugin = function (id, func) { id = require('mongodb').ObjectID(id); obj.pluginsfile.find({ _id: id }).sort({ name: 1 }).toArray(func); };
763
764 // Delete plugin
765 - obj.deletePlugin = function (id) { obj.pluginsfile.deleteOne({ _id: id }); };
765 + obj.deletePlugin = function (id, func) { id = require('mongodb').ObjectID(id); obj.pluginsfile.deleteOne({ _id: id }, func); };
766 +
767 + obj.setPluginStatus = function(id, status, func) { id = require('mongodb').ObjectID(id); obj.pluginsfile.updateOne({ _id: id }, { $set: {status: status } }, func); };
768
769 } else {
770 // Database actions on the main collection (NeDB and MongoJS)
meshagent.js
+1 -2
@@ -1287,8 +1287,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
1287 case 'plugin': {
1288 if ((parent.parent.pluginHandler == null) || (typeof command.plugin != 'string')) break;
1289 try {
1290 - var pluginHandler = require('./pluginHandler.js').pluginHandler(parent.parent);
1291 - pluginHandler.plugins[command.plugin].serveraction(command, obj, parent);
1290 + parent.parent.pluginHandler.plugins[command.plugin].serveraction(command, obj, parent);
1291 } catch (e) {
1292 console.log('Error loading plugin handler (' + e + ')');
1293 }
meshuser.js
+34 -6
@@ -3103,22 +3103,51 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
3103 break;
3104 }
3105 case 'plugins': {
3106 - if ((user.siteadmin & 0xFFFFFFFF) == 0 || parent.parent.pluginHandler == null) break; // must be full admin, plugins enabled
3106 + // @Ylianst - Do we need a new permission set here?
3107 + if ((user.siteadmin & 0xFFFFFFFF) == 0 || parent.parent.pluginHandler == null) break; // must be full admin with plugins enabled
3108 parent.db.getPlugins(function(err, docs) {
3109 try { ws.send(JSON.stringify({ action: 'updatePluginList', list: docs, result: err })); } catch (ex) { }
3110 });
3111 break;
3112 }
3113 + case 'pluginLatestCheck': {
3114 + if ((user.siteadmin & 0xFFFFFFFF) == 0 || parent.parent.pluginHandler == null) break; // must be full admin with plugins enabled
3115 + parent.parent.pluginHandler.getPluginLatest(function(latest) {
3116 + try { ws.send(JSON.stringify({ action: 'pluginVersionsAvailable', list: latest })); } catch (ex) { }
3117 + });
3118 + break;
3119 + }
3120 case 'addplugin': {
3113 - // @Ylianst - Do we need a new permission here?
3121 if ((user.siteadmin & 0xFFFFFFFF) == 0 || parent.parent.pluginHandler == null) break; // must be full admin, plugins enabled
3122 parent.parent.pluginHandler.addPlugin(command.url);
3123 break;
3124 }
3125 + case 'installplugin': {
3126 + if ((user.siteadmin & 0xFFFFFFFF) == 0 || parent.parent.pluginHandler == null) break; // must be full admin, plugins enabled
3127 + parent.parent.pluginHandler.installPlugin(command.id, function(){
3128 + parent.parent.updateMeshCore();
3129 + parent.db.getPlugins(function(err, docs) {
3130 + try { ws.send(JSON.stringify({ action: 'updatePluginList', list: docs, result: err })); } catch (ex) { }
3131 + });
3132 + });
3133 + break;
3134 + }
3135 + case 'disableplugin': {
3136 + if ((user.siteadmin & 0xFFFFFFFF) == 0 || parent.parent.pluginHandler == null) break; // must be full admin, plugins enabled
3137 + parent.parent.pluginHandler.disablePlugin(command.id, function(){
3138 + parent.db.getPlugins(function(err, docs) {
3139 + try { ws.send(JSON.stringify({ action: 'updatePluginList', list: docs, result: err })); } catch (ex) { }
3140 + });
3141 + });
3142 + break;
3143 + }
3144 case 'removeplugin': {
3119 - // @Ylianst - Do we need a new permission here?
3145 if ((user.siteadmin & 0xFFFFFFFF) == 0 || parent.parent.pluginHandler == null) break; // must be full admin, plugins enabled
3121 - parent.parent.pluginHandler.removePlugin(command.id);
3146 + parent.parent.pluginHandler.removePlugin(command.id, function(){
3147 + parent.db.getPlugins(function(err, docs) {
3148 + try { ws.send(JSON.stringify({ action: 'updatePluginList', list: docs, result: err })); } catch (ex) { }
3149 + });
3150 + });
3151 break;
3152 }
3153 case 'plugin': {
@@ -3128,8 +3157,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
3157 routeCommandToNode(command);
3158 } else {
3159 try {
3131 - var pluginHandler = require('./pluginHandler.js').pluginHandler(parent.parent);
3132 - pluginHandler.plugins[command.plugin].serveraction(command, obj, parent);
3160 + parent.parent.pluginHandler.plugins[command.plugin].serveraction(command, obj, parent);
3161 } catch (e) { console.log('Error loading plugin handler (' + e + ')'); }
3162 }
3163 break;
pluginHandler.js
+203 -74
@@ -23,34 +23,52 @@ module.exports.pluginHandler = function (parent) {
23 obj.pluginPath = obj.parent.path.join(obj.parent.datapath, 'plugins');
24 obj.plugins = {};
25 obj.exports = {};
26 - obj.loadList = obj.parent.config.settings.plugins.list;
27 -
26 + obj.loadList = obj.parent.config.settings.plugins.list; // For local development / manual install, not from DB
27 +
28 if (typeof obj.loadList != 'object') {
29 obj.loadList = {};
30 - console.log('Plugin list not specified, please fix configuration file.');
31 - return null;
32 - }
33 -
34 - obj.loadList.forEach(function (plugin, index) {
35 - if (obj.fs.existsSync(obj.pluginPath + '/' + plugin)) {
36 - try {
37 - obj.plugins[plugin] = require(obj.pluginPath + '/' + plugin + '/' + plugin + '.js')[plugin](obj);
38 - obj.exports[plugin] = obj.plugins[plugin].exports;
39 - } catch (e) {
40 - console.log("Error loading plugin: " + plugin + " (" + e + "). It has been disabled.", e.stack);
30 + parent.db.getPlugins(function(err, plugins){
31 + plugins.forEach(function(plugin){
32 + if (plugin.status != 1) return;
33 + if (obj.fs.existsSync(obj.pluginPath + '/' + plugin.shortName)) {
34 + try {
35 + obj.plugins[plugin.shortName] = require(obj.pluginPath + '/' + plugin.shortName + '/' + plugin.shortName + '.js')[plugin.shortName](obj);
36 + obj.exports[plugin.shortName] = obj.plugins[plugin.shortName].exports;
37 + } catch (e) {
38 + console.log("Error loading plugin: " + plugin.shortName + " (" + e + "). It has been disabled.", e.stack);
39 + }
40 + }
41 + obj.parent.updateMeshCore(); // db calls are delayed, lets inject here once we're ready
42 + });
43 + });
44 + } else {
45 + obj.loadList.forEach(function (plugin, index) {
46 + if (obj.fs.existsSync(obj.pluginPath + '/' + plugin)) {
47 + try {
48 + obj.plugins[plugin] = require(obj.pluginPath + '/' + plugin + '/' + plugin + '.js')[plugin](obj);
49 + obj.exports[plugin] = obj.plugins[plugin].exports;
50 + } catch (e) {
51 + console.log("Error loading plugin: " + plugin + " (" + e + "). It has been disabled.", e.stack);
52 + }
53 }
54 + });
55 + }
56 +
57 + obj.prepExportsForPlugin = function(plugin) {
58 + var str = '';
59 + str += ' obj.' + plugin + ' = {};\r\n';
60 + for (const l of Object.values(obj.exports[plugin])) {
61 + str += ' obj.' + plugin + '.' + l + ' = ' + obj.plugins[plugin][l].toString() + '\r\n';
62 }
43 - });
44 -
63 + return str;
64 + };
65 +
66 obj.prepExports = function () {
67 var str = 'function() {\r\n';
68 str += ' var obj = {};\r\n';
69
70 for (const p of Object.keys(obj.plugins)) {
50 - str += ' obj.' + p + ' = {};\r\n';
51 - for (const l of Object.values(obj.exports[p])) {
52 - str += ' obj.' + p + '.' + l + ' = ' + obj.plugins[p][l].toString() + '\r\n';
53 - }
71 + str += obj.prepExportsForPlugin(p);
72 }
73
74 str += `obj.onDeviceRefeshEnd = function(nodeid, panel, refresh, event) {
@@ -75,7 +93,7 @@ module.exports.pluginHandler = function (parent) {
93 meshserver.send({ action: 'addplugin', url: Q('pluginurlinput').value});
94 };
95 obj.addPluginDlg = function() {
78 - setDialogMode(2, "Plugin URL", 3, obj.addPluginEx, '<input type=text id=pluginurlinput style=width:100% />');
96 + setDialogMode(2, "Plugin Config URL", 3, obj.addPluginEx, '<input type=text id=pluginurlinput style=width:100% />');
97 focusTextBox('pluginurlinput');
98 };
99 return obj; };`;
@@ -165,6 +183,7 @@ module.exports.pluginHandler = function (parent) {
183 var isValid = true;
184 if (!(
185 typeof conf.name == 'string'
186 + && typeof conf.shortName == 'string'
187 && typeof conf.version == 'string'
188 && typeof conf.author == 'string'
189 && typeof conf.description == 'string'
@@ -179,68 +198,178 @@ module.exports.pluginHandler = function (parent) {
198 // && conf.configUrl == url // make sure we're loading a plugin from its desired config
199 )) isValid = false;
200 // more checks here?
201 + if (conf.repository.type == 'git') {
202 + if (typeof conf.downloadUrl != 'string') isValid = false;
203 + }
204 return isValid;
205 };
206
185 - obj.addPlugin = function(url) {
186 - var https = require('https');
187 - //var pit = obj.path.join(obj.pluginPath, )
188 -
189 - https.get(url, function(res) {
190 - var configStr = '';
191 - res.on('data', function(chunk){
192 - configStr += chunk;
207 + obj.getPlugins = function(func) {
208 + var plugins = parent.db.getPlugins();
209 + if (typeof plugins == 'undefined' || plugins.length == 0) {
210 + return null;
211 + }
212 +
213 + plugins.forEach(function(p, x){
214 + // check semantic version
215 + console.log('FOREACH PLUGIN', p, x);
216 + // callbacks to new versions
217 +
218 });
194 - res.on('end', function(){
195 - if (configStr[0] == '{') {
196 - try {
197 - var pluginConfig = JSON.parse(configStr);
198 - if (obj.isValidConfig(pluginConfig, url)) {
199 - // add to database
200 - // we met the requirements of a valid config, but in case there's extra, let's rebuild for what we need
201 - parent.db.addPlugin({
202 - "name": pluginConfig.name,
203 - "version": pluginConfig.version,
204 - "description": pluginConfig.description,
205 - "hasAdminPanel": pluginConfig.hasAdminPanel,
206 - "homepage": pluginConfig.homepage,
207 - "changelogUrl": pluginConfig.changelogUrl,
208 - "configUrl": pluginConfig.configUrl,
209 - "repository": {
210 - "type": pluginConfig.repository.type,
211 - "url": pluginConfig.repository.url
212 - },
213 - "meshCentralCompat": pluginConfig.meshCentralCompat,
214 - "status": 0 // 0: disabled, 1: enabled
219 +
220 + return plugins;
221 + }
222 +
223 + obj.getPluginConfig = function(configUrl, func) {
224 + var https = require('https');
225 + if (configUrl.indexOf('://') === -1) return; // @TODO error here
226 + https.get(configUrl, function(res) {
227 + var configStr = '';
228 + res.on('data', function(chunk){
229 + configStr += chunk;
230 + });
231 + res.on('end', function(){
232 + if (configStr[0] == '{') { // let's be sure we're JSON
233 + try {
234 + var pluginConfig = JSON.parse(configStr);
235 + if (Array.isArray(pluginConfig) && pluginConfig.length == 1) pluginConfig = pluginConfig[0];
236 + if (obj.isValidConfig(pluginConfig, configUrl)) {
237 + func(pluginConfig);
238 + }
239 +
240 + } catch (e) { console.log('Error getting plugin config. Check that you have valid JSON.', e.stack); }
241 + }
242 + });
243 +
244 + }).on('error', function(e) {
245 + console.log("Error getting plugin config. Check that the URL is correct.: " + e.message);
246 + });
247 + };
248 +
249 + obj.getPluginLatest = function(func) {
250 + parent.db.getPlugins(function(err, plugins){
251 + plugins.forEach(function(curconf){
252 + obj.getPluginConfig(curconf.configUrl, function(newconf){
253 + var s = require('semver');
254 + func({
255 + "id": curconf._id,
256 + "installedVersion": curconf.version,
257 + "version": newconf.version,
258 + "hasUpdate": s.gt(newconf.version, curconf.version),
259 + "meshCentralCompat": s.satisfies(s.coerce(parent.currentVer), newconf.meshCentralCompat),
260 + "changelogUrl": curconf.changelogUrl,
261 + "status": curconf.status
262 + });
263 + });
264 + });
265 + });
266 +
267 + };
268 +
269 + obj.addPlugin = function(url) {
270 + obj.getPluginConfig(url, function(pluginConfig){
271 + parent.db.addPlugin({
272 + "name": pluginConfig.name,
273 + "shortName": pluginConfig.shortName,
274 + "version": pluginConfig.version,
275 + "description": pluginConfig.description,
276 + "hasAdminPanel": pluginConfig.hasAdminPanel,
277 + "homepage": pluginConfig.homepage,
278 + "changelogUrl": pluginConfig.changelogUrl,
279 + "configUrl": pluginConfig.configUrl,
280 + "downloadUrl": pluginConfig.downloadUrl,
281 + "repository": {
282 + "type": pluginConfig.repository.type,
283 + "url": pluginConfig.repository.url
284 + },
285 + "meshCentralCompat": pluginConfig.meshCentralCompat,
286 + "status": 0 // 0: disabled, 1: enabled
287 + }, function() {
288 + parent.db.getPlugins(function(err, docs){
289 + var targets = ['*', 'server-users'];
290 + parent.DispatchEvent(targets, obj, { action: 'updatePluginList', list: docs });
291 +
292 + });
293 + });
294 + });
295 + };
296 +
297 + obj.installPlugin = function(id, func) {
298 + parent.db.getPlugin(id, function(err, docs){
299 + var http = require('https');
300 + // the "id" would probably suffice, but is probably an sanitary issue, generate a random instead
301 + var randId = Math.random().toString(32).replace('0.', '');
302 + var fileName = obj.parent.path.join(require('os').tmpdir(), 'Plugin_'+randId+'.zip');
303 + var plugin = docs[0];
304 + if (plugin.repository.type == 'git') {
305 + const file = obj.fs.createWriteStream(fileName);
306 + var request = http.get(plugin.downloadUrl, function(response) {
307 + response.pipe(file);
308 + file.on('finish', function() {
309 + file.close(function(){
310 + var yauzl = require("yauzl");
311 + if (!obj.fs.existsSync(obj.pluginPath)) {
312 + obj.fs.mkdirSync(obj.pluginPath);
313 + }
314 + if (!obj.fs.existsSync(obj.parent.path.join(obj.pluginPath, plugin.shortName))) {
315 + obj.fs.mkdirSync(obj.parent.path.join(obj.pluginPath, plugin.shortName));
316 + }
317 + yauzl.open(fileName, { lazyEntries: true }, function (err, zipfile) {
318 + if (err) throw err;
319 + zipfile.readEntry();
320 + zipfile.on("entry", function (entry) {
321 + let pluginPath = obj.parent.path.join(obj.pluginPath, plugin.shortName);
322 + let pathReg = new RegExp(/(.*?\/)/);
323 + if (process.platform == 'win32') pathReg = new RegExp(/(.*?\\/);
324 + let filePath = obj.parent.path.join(pluginPath, entry.fileName.replace(pathReg, '')); // remove top level dir
325 +
326 + if (/\/$/.test(entry.fileName)) { // dir
327 + if (!obj.fs.existsSync(filePath))
328 + obj.fs.mkdirSync(filePath);
329 + zipfile.readEntry();
330 + } else { // file
331 + zipfile.openReadStream(entry, function (err, readStream) {
332 + if (err) throw err;
333 + readStream.on("end", function () { zipfile.readEntry(); });
334 + readStream.pipe(obj.fs.createWriteStream(filePath));
335 + });
336 + }
337 + });
338 + zipfile.on("end", function () { setTimeout(function () {
339 + obj.fs.unlinkSync(fileName);
340 + parent.db.setPluginStatus(id, 1, func);
341 + obj.plugins[plugin.shortName] = require(obj.pluginPath + '/' + plugin.shortName + '/' + plugin.shortName + '.js')[plugin.shortName](obj);
342 + obj.exports[plugin.shortName] = obj.plugins[plugin.shortName].exports;
343 + }); });
344 + });
345 });
216 - parent.db.getPlugins(function(err, docs){
217 - var targets = ['*', 'server-users'];
218 - parent.DispatchEvent(targets, obj, { action: 'updatePluginList', list: docs });
219 -
220 - })
221 - } else {
222 - // @TODO return error to user
223 - }
224 -
225 - } catch (e) { console.log('Error processing addPlugin request. Check that you have valid JSON.'); }
346 + });
347 + });
348 + } else if (plugin.repository.type == 'npm') {
349 + // @TODO npm install and symlink dirs (need a test plugin)
350 }
351 +
352 +
353 + });
354 +
355 +
356 + };
357 +
358 + obj.disablePlugin = function(id, func) {
359 + parent.db.setPluginStatus(id, 0, func);
360 + };
361 +
362 + obj.removePlugin = function(id, func) {
363 + parent.db.getPlugin(id, function(err, docs){
364 + var plugin = docs[0];
365 + var rimraf = require("rimraf");
366 + let pluginPath = obj.parent.path.join(obj.pluginPath, plugin.shortName);
367 + rimraf.sync(pluginPath);
368 + parent.db.deletePlugin(id, func);
369 + delete obj.plugins[plugin.shortName];
370 + obj.parent.updateMeshCore();
371 });
228 -
229 - }).on('error', function(e) {
230 - console.log("Got error: " + e.message);
231 - });
232 - /* const file = fs.createWriteStream("file.jpg");
233 - const request = http.get("http://i3.ytimg.com/vi/J---aiyznGQ/mqdefault.jpg", function(response) {
234 - response.pipe(file);
235 - }); */
372 };
373
238 - obj.getPlugins = function() {
239 - var p = parent.db.getPlugins();
240 - if (typeof p == 'undefined' || p.length == 0) {
241 - return null;
242 - }
243 - return p;
244 - }
374 return obj;
375 };
\ No newline at end of file
public/styles/style.css
+19 -2
@@ -2584,14 +2584,18 @@ a {
2584 }
2585
2586 #p7tbl .chDescription {
2587 - width: 40%;
2587 + width: 38%;
2588 }
2589
2590 #p7tbl .chSite {
2591 - width: 10%;
2591 + width: 7%;
2592 }
2593
2594 #p7tbl .chVersion {
2595 + width: 5%;
2596 +}
2597 +
2598 +#p7tbl .chUpgradeAvail {
2599 width: 10%;
2600 }
2601
@@ -2603,6 +2607,10 @@ a {
2607 width: 10%;
2608 }
2609
2610 +.pActDisable, .pActDelete, .pActInstall, .pActUpgrade {
2611 + cursor: pointer;
2612 +}
2613 +
2614 #addPlugin {
2615 background-image: url(../images/plus32.png);
2616 width: 32px;
@@ -2610,4 +2618,13 @@ a {
2618 float: right;
2619 cursor: pointer;
2620 margin-right: 12px;
2621 +}
2622 +
2623 +#pluginRestartNotice {
2624 + width: 40em;
2625 + font-weight: bold;
2626 + border: 1px solid red;
2627 + text-align: center;
2628 + padding: 14px;
2629 + margin: 50px auto;
2630 }
\ No newline at end of file
views/default.handlebars
+91 -9
@@ -411,10 +411,11 @@
411 </div>
412 <div id=p7 style="display:none">
413 <h1>My Plugins</h1>
414 - <div id="addPlugin" onclick="return pluginHandler.addPluginDlg();"></div>
414 + <div id="addPlugin" title="Add New Plugin" onclick="return pluginHandler.addPluginDlg();"></div>
415 <table id="p7tbl">
416 - <tr><th class="chName">Name</th><th class="chDescription">Description</th><th class="chSite">Link</th><th class="chVersion">Version</th><th class="chStatus">Status</th><th class="chAction">Action</th></tr>
416 + <tr><th class="chName">Name</th><th class="chDescription">Description</th><th class="chSite">Link</th><th class="chVersion">Version</th><th class="chUpgradeAvail">Latest Available</th><th class="chStatus">Status</th><th class="chAction">Action</th></tr>
417 </table>
418 + <div id="pluginRestartNotice" style="display:none;"><div>Notice:</div> MeshCentral restart required to complete plugin changes.</div>
419 </div>
420 <div id=p10 style="display:none">
421 <table style="width:100%" cellpadding="0" cellspacing="0">
@@ -2362,6 +2363,27 @@
2363 updatePluginList();
2364 break;
2365 }
2366 + case 'pluginVersionsAvailable': {
2367 + if (pluginHandler == null) break;
2368 + try {
2369 + var td = Q('pluginRow-'+message.list.id).querySelectorAll(".pluginUpgradeAvailable");
2370 + var sel = Q('pluginRow-'+message.list.id).querySelectorAll(".pluginAction > select");
2371 + td = td[0];
2372 + sel = sel[0];
2373 + if (message.list.hasUpdate && message.list.status) {
2374 + td.innerHTML = '<a title="View Changelog" target="_blank" href="' + message.list.changelogUrl + '">' + message.list.version + '</a>';
2375 + if (sel.innerHTML.indexOf('Upgrade') === -1) {
2376 + var option = document.createElement("option");
2377 + option.value = "install"
2378 + option.text = "Upgrade";
2379 + sel.add(option);
2380 + }
2381 + } else {
2382 + td.innerHTML = "Up to date";
2383 + }
2384 + } catch (e) { }
2385 + break;
2386 + }
2387 case 'plugin': {
2388 if ((pluginHandler == null) || (typeof message.plugin != 'string')) break;
2389 try { pluginHandler[message.plugin][message.method](server, message); } catch (e) { console.log('Error loading plugin handler ('+ e + ')'); }
@@ -9361,6 +9383,8 @@
9383 // Fetch the server timeline stats if needed
9384 if ((x == 40) && (serverTimelineStats == null)) { refreshServerTimelineStats(); }
9385
9386 + if (x == 7) refreshPluginLatest();
9387 +
9388 // Update the web page title
9389 if ((currentNode) && (x >= 10) && (x < 20)) {
9390 document.title = decodeURIComponent('{{{extitle}}}') + ' - ' + currentNode.name + ' - ' + meshes[currentNode.meshid].name;
@@ -9378,24 +9402,82 @@
9402 }
9403 }
9404 var statusMap = {
9381 - 0: 'Disabled',
9382 - 1: 'Installed'
9383 - }
9405 + 0: {
9406 + "text": 'Disabled',
9407 + "color": '858483'
9408 + },
9409 + 1: {
9410 + "text": 'Installed',
9411 + "color": '00ff00'
9412 + }
9413 + };
9414 + var statusAvailability = {
9415 + 0: {
9416 + 'install': 'Install',
9417 + 'delete': 'Delete'
9418 + },
9419 + 1: {
9420 + 'disable': 'Disable'
9421 + }
9422 + };
9423 var tbl = Q('p7tbl');
9424 installedPluginList.forEach(function(p){
9425 if (p.hasAdminPanel == true) {
9426 p.name = `<a onclick="return goPlugin('${p._id}');">${p.name}</a>`;
9427 }
9389 - p.status = statusMap[p.status];
9390 - p.actions = 'TODO'; // Install / Upgrade / Disable / Delete
9391 - let tpl = `<td>${p.name}</td><td>${p.description}</td><td><a href="${p.homepage}" target="_blank">Homepage</a></td><td>${p.version}</td><td>${p.status}</td><td>${p.actions}</td>`;
9428 + p.statusText = statusMap[p.status].text;
9429 + p.statusColor = statusMap[p.status].color;
9430 +
9431 + p.actions = '<select onchange="return pluginAction(this, \'' + p._id + '\');"><option value=""> --</option>';
9432 + for (const [k, v] of Object.entries(statusAvailability[p.status])) {
9433 + p.actions += '<option value="' + k + '">' + v + '</option>';
9434 + }
9435 + p.action += '</select>'
9436 +
9437 + let tpl = `<td>${p.name}</td><td>${p.description}</td><td><a href="${p.homepage}" target="_blank">Homepage</a></td><td>${p.version}</td><td class="pluginUpgradeAvailable">Checking...</td><td style="color: #${p.statusColor}">${p.statusText}</td><td class="pluginAction">${p.actions}</td>`;
9438 let tr = tbl.insertRow(-1);
9439 tr.innerHTML = tpl;
9440 tr.classList.add('p7tblRow');
9441 + tr.setAttribute('data-id', p._id);
9442 + tr.setAttribute('id', 'pluginRow-'+p._id);
9443 });
9444 + } else {
9445 + var tr = Q('p7tbl').querySelectorAll(".p7tblRow");
9446 + for (const i in Object.values(tr)) {
9447 + tr[i].parentNode.removeChild(tr[i]);
9448 + }
9449 }
9397 -
9450 + refreshPluginLatest();
9451 }
9452 +
9453 + function refreshPluginLatest() {
9454 + meshserver.send({ action: 'pluginLatestCheck' });
9455 + }
9456 +
9457 + function pluginActionEx() {
9458 + var act = Q('lastPluginAct').value, id = Q('lastPluginId').value;
9459 + switch(act) {
9460 + case 'install': {
9461 + meshserver.send({ "action": "installplugin", "id": id });
9462 + break;
9463 + }
9464 + case 'delete': {
9465 + meshserver.send({ "action": "removeplugin", "id": id });
9466 + break;
9467 + }
9468 + case 'disable': {
9469 + meshserver.send({ "action": "disableplugin", "id": id });
9470 + break;
9471 + }
9472 + }
9473 + QS('pluginRestartNotice').display = '';
9474 + }
9475 +
9476 + function pluginAction(elem, id) {
9477 + setDialogMode(2, 'Plugin Action', 3, pluginActionEx, 'Are you sure you want to ' + elem.value + ' the plugin: ' + elem.parentNode.parentNode.firstChild.innerText+'<input id="lastPluginAct" type="hidden" value="' + elem.value + '" /><input id="lastPluginId" type="hidden" value="' + elem.parentNode.parentNode.getAttribute('data-id') + '" />');
9478 + elem.value = '';
9479 + }
9480 +
9481 // Generic methods
9482 function joinPaths() { var x = []; for (var i in arguments) { var w = arguments[i]; if ((w != null) && (w != '')) { while (w.endsWith('/') || w.endsWith('\\')) { w = w.substring(0, w.length - 1); } while (w.startsWith('/') || w.startsWith('\\')) { w = w.substring(1); } x.push(w); } } return x.join('/'); }
9483 function putstore(name, val) {