From 3388f699a002c332af6b7a176f63b93ea7a539b1 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Thu, 24 Mar 2022 16:00:45 +0000 Subject: [PATCH 01/16] Add diagnostics endpoint fixes #3430 --- .../editor-api/lib/admin/diagnostics.js | 27 ++++ .../@node-red/editor-api/lib/admin/index.js | 5 + .../@node-red/runtime/lib/api/diagnostics.js | 116 ++++++++++++++++++ .../@node-red/runtime/lib/api/index.js | 2 + .../@node-red/runtime/lib/index.js | 7 +- packages/node_modules/node-red/lib/red.js | 9 +- 6 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 packages/node_modules/@node-red/editor-api/lib/admin/diagnostics.js create mode 100644 packages/node_modules/@node-red/runtime/lib/api/diagnostics.js diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/diagnostics.js b/packages/node_modules/@node-red/editor-api/lib/admin/diagnostics.js new file mode 100644 index 000000000..dd37e0fe4 --- /dev/null +++ b/packages/node_modules/@node-red/editor-api/lib/admin/diagnostics.js @@ -0,0 +1,27 @@ +var apiUtils = require("@node-red/editor-api/lib/util"); +/** @type {runtime.RuntimeModule} */var runtimeAPI; + + +module.exports = { + init: function(/** @type {runtime.RuntimeModule} */_runtimeAPI) { + runtimeAPI = _runtimeAPI; + }, + getBasicReport: function(req, res) { + var opts = { + user: req.user, + scope: "basic" + } + runtimeAPI.diagnostics.get(opts).then(function(result) { + res.json(result); + }); + }, + getAdminReport: function(req, res) { + var opts = { + user: req.user, + scope: "admin" + } + runtimeAPI.diagnostics.get(opts).then(function(result) { + res.json(result); + }); + } +} diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/index.js b/packages/node_modules/@node-red/editor-api/lib/admin/index.js index be9bb5317..a6e42fdfc 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/index.js @@ -23,6 +23,7 @@ var context = require("./context"); var auth = require("../auth"); var info = require("./settings"); var plugins = require("./plugins"); +var diagnostics = require("./diagnostics"); var apiUtil = require("../util"); @@ -34,6 +35,7 @@ module.exports = { context.init(runtimeAPI); info.init(settings,runtimeAPI); plugins.init(runtimeAPI); + diagnostics.init(runtimeAPI); var needsPermission = auth.needsPermission; @@ -95,6 +97,9 @@ module.exports = { adminApp.get("/plugins", needsPermission("plugins.read"), plugins.getAll, apiUtil.errorHandler); adminApp.get("/plugins/messages", needsPermission("plugins.read"), plugins.getCatalogs, apiUtil.errorHandler); + adminApp.get("/diagnostics/basic", needsPermission("settings.read"), diagnostics.getBasicReport, apiUtil.errorHandler); + adminApp.get("/diagnostics/admin", needsPermission("flows.write"), diagnostics.getAdminReport, apiUtil.errorHandler); + return adminApp; } } diff --git a/packages/node_modules/@node-red/runtime/lib/api/diagnostics.js b/packages/node_modules/@node-red/runtime/lib/api/diagnostics.js new file mode 100644 index 000000000..8440bd85e --- /dev/null +++ b/packages/node_modules/@node-red/runtime/lib/api/diagnostics.js @@ -0,0 +1,116 @@ + +/** + * @mixin @node-red/diagnostics + * @namespace RED.runtime.diagnostics + */ + +var runtime; + +var util = require("@node-red/util").util; + +function buildDiagnosticReport(scope, callback) { + var basic = { + "report": "diagnostics", + "scope": scope, + "runtime": { + version: runtime.settings.version, + isStarted: runtime.isStarted() + }, + settings: { + available: runtime.settings.available(), + apiMaxLength: runtime.settings.apiMaxLength || "NO SETTING", + coreNodesDir: runtime.settings.coreNodesDir, + contextStorage: listContextModules(), + debugMaxLength: runtime.settings.debugMaxLength, + editorTheme: runtime.settings.editorTheme, + flowFile: runtime.settings.flowFile, + disableEditor:runtime.settings.disableEditor, + debugMaxLength:runtime.settings.debugMaxLength, + + httpAdminRoot: runtime.settings.httpAdminRoot, + httpAdminCors: runtime.settings.httpAdminCors ? "HAS SETTING": "NOT SET", + httpNodeAuth: runtime.settings.httpNodeAuth ? "HAS SETTING": "NOT SET", + + httpNodeRoot: runtime.settings.httpNodeRoot, + httpNodeCors: runtime.settings.httpNodeCors ? "HAS SETTING": "NOT SET", + + httpStatic: runtime.settings.httpStatic, + httpStaticCors: runtime.settings.httpStaticCors, + + mqttReconnectTime: runtime.settings.mqttReconnectTime, + + + uiHost: runtime.settings.uiHost ? "HAS SETTING": "NOT SET", + uiPort: runtime.settings.uiPort ? "HAS SETTING": "NOT SET", + userDir: runtime.settings.userDir ? "HAS SETTING": "NOT SET", + + version: runtime.settings.version + } + } + var admin = {}; + if(scope == "admin") { + admin = { + httpAdminCors: runtime.settings.httpAdminCors ? runtime.settings.httpAdminCors : "NOT SET", + httpNodeCors: runtime.settings.httpNodeCors ? runtime.settings.httpNodeCors : "NOT SET", + uiHost: runtime.settings.uiHost ? runtime.settings.uiHost : "NOT SET", + uiPort: runtime.settings.uiPort ? runtime.settings.uiPort : "NOT SET", + userDir: runtime.settings.userDir ? runtime.settings.userDir : "NOT SET", + } + } + + var report = Object.assign({}, admin, basic); + + callback(report); + + function listContextModules() { + var keys = Object.keys(runtime.settings.contextStorage); + var result = {}; + keys.forEach(e => { + result[e] = { + module: runtime.settings.contextStorage[e].module + } + }) + return result; + } +} + + +var api = module.exports = { + init: function (_runtime) { + runtime = _runtime; + }, + /** + * Gets the node-red diagnostics report + * @param {{scope: string}} - settings + * @return {Promise} - the diagnostics information + * @memberof @node-red/diagnostics + */ + get: async function (opts) { + return new Promise(function (resolve, reject) { + opts = opts || {} + var scope = opts.scope; + try { + if (scope === 'admin') { + //admin level info + runtime.log.audit({ event: "diagnostics.get", scope: "admin" }, opts.req); + buildDiagnosticReport(scope, (report) => resolve(report)); + } else if (scope === 'detail') { + //detail! + runtime.log.audit({ event: "diagnostics.get", scope: "detail" }, opts.req); + buildDiagnosticReport(scope, (report) => resolve(report)); + } else if (scope === 'basic') { + //basic! + runtime.log.audit({ event: "diagnostics.get", scope: "basic" }, opts.req); + buildDiagnosticReport(scope, (report) => resolve(report)); + + } else { + runtime.log.audit({ event: "diagnostics.get", scope: scope }, opts.req); + resolve({}); + } + } catch (error) { + runtime.log.audit({ event: "diagnostics.get", scope: scope }, opts.req); + reject(error); + } + }) + }, +} diff --git a/packages/node_modules/@node-red/runtime/lib/api/index.js b/packages/node_modules/@node-red/runtime/lib/api/index.js index 46d15d1e7..d2fa3c6fd 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/index.js +++ b/packages/node_modules/@node-red/runtime/lib/api/index.js @@ -29,6 +29,7 @@ var api = module.exports = { api.projects.init(runtime); api.context.init(runtime); api.plugins.init(runtime); + api.diagnostics.init(runtime); }, comms: require("./comms"), @@ -39,6 +40,7 @@ var api = module.exports = { projects: require("./projects"), context: require("./context"), plugins: require("./plugins"), + diagnostics: require("./diagnostics"), isStarted: async function(opts) { return runtime.isStarted(); diff --git a/packages/node_modules/@node-red/runtime/lib/index.js b/packages/node_modules/@node-red/runtime/lib/index.js index b78d7d372..535b5f5e4 100644 --- a/packages/node_modules/@node-red/runtime/lib/index.js +++ b/packages/node_modules/@node-red/runtime/lib/index.js @@ -395,7 +395,12 @@ module.exports = { * @memberof @node-red/runtime */ version: externalAPI.version, - + + /** + * @memberof @node-red/diagnostics + */ + diagnostics:externalAPI.diagnostics, + storage: storage, events: events, hooks: hooks, diff --git a/packages/node_modules/node-red/lib/red.js b/packages/node_modules/node-red/lib/red.js index 2c6f00a6e..4dc5f1281 100644 --- a/packages/node_modules/node-red/lib/red.js +++ b/packages/node_modules/node-red/lib/red.js @@ -229,5 +229,12 @@ module.exports = { * @see @node-red/editor-api_auth * @memberof node-red */ - auth: api.auth + auth: api.auth, + + /** + * The editor authentication api. + * @see @node-red/editor-api_auth + * @memberof node-red + */ + get diagnostics() { debugger; return api.diagnostics } }; From a2fd705153e4e301d2ca276092a8665ba7bf0e19 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 28 Mar 2022 18:49:56 +0100 Subject: [PATCH 02/16] Improve diagnostics content --- .../editor-api/lib/admin/diagnostics.js | 34 ++- .../@node-red/editor-api/lib/admin/index.js | 5 +- .../@node-red/runtime/lib/api/diagnostics.js | 231 ++++++++++++------ 3 files changed, 176 insertions(+), 94 deletions(-) diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/diagnostics.js b/packages/node_modules/@node-red/editor-api/lib/admin/diagnostics.js index dd37e0fe4..3ad83fc63 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/diagnostics.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/diagnostics.js @@ -1,27 +1,23 @@ -var apiUtils = require("@node-red/editor-api/lib/util"); -/** @type {runtime.RuntimeModule} */var runtimeAPI; - - +let runtimeAPI; +let settings; +const apiUtil = require("../util"); module.exports = { - init: function(/** @type {runtime.RuntimeModule} */_runtimeAPI) { + init: function(_settings, _runtimeAPI) { + settings = _settings; runtimeAPI = _runtimeAPI; }, - getBasicReport: function(req, res) { - var opts = { + getReport: function(req, res) { + const diagnosticsOptions = settings.diagnosticsOptions || {}; + const opts = { user: req.user, - scope: "basic" + scope: diagnosticsOptions.level || "basic" } - runtimeAPI.diagnostics.get(opts).then(function(result) { - res.json(result); - }); - }, - getAdminReport: function(req, res) { - var opts = { - user: req.user, - scope: "admin" + if(diagnosticsOptions.enabled === false || diagnosticsOptions.enabled === "false") { + apiUtil.rejectHandler(req, res, {message: "disabled", status: 403, code: "diagnosticsOptions.enabled" }) + } else { + runtimeAPI.diagnostics.get(opts) + .then(function(result) { res.json(result); }) + .catch(err => apiUtil.rejectHandler(req, res, err)) } - runtimeAPI.diagnostics.get(opts).then(function(result) { - res.json(result); - }); } } diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/index.js b/packages/node_modules/@node-red/editor-api/lib/admin/index.js index a6e42fdfc..87fd0dec0 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/index.js @@ -35,7 +35,7 @@ module.exports = { context.init(runtimeAPI); info.init(settings,runtimeAPI); plugins.init(runtimeAPI); - diagnostics.init(runtimeAPI); + diagnostics.init(settings, runtimeAPI); var needsPermission = auth.needsPermission; @@ -97,8 +97,7 @@ module.exports = { adminApp.get("/plugins", needsPermission("plugins.read"), plugins.getAll, apiUtil.errorHandler); adminApp.get("/plugins/messages", needsPermission("plugins.read"), plugins.getCatalogs, apiUtil.errorHandler); - adminApp.get("/diagnostics/basic", needsPermission("settings.read"), diagnostics.getBasicReport, apiUtil.errorHandler); - adminApp.get("/diagnostics/admin", needsPermission("flows.write"), diagnostics.getAdminReport, apiUtil.errorHandler); + adminApp.get("/diagnostics", needsPermission("diagnostics.read"), diagnostics.getReport, apiUtil.errorHandler); return adminApp; } diff --git a/packages/node_modules/@node-red/runtime/lib/api/diagnostics.js b/packages/node_modules/@node-red/runtime/lib/api/diagnostics.js index 8440bd85e..f8ab9bbaf 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/diagnostics.js +++ b/packages/node_modules/@node-red/runtime/lib/api/diagnostics.js @@ -1,73 +1,176 @@ -/** - * @mixin @node-red/diagnostics - * @namespace RED.runtime.diagnostics - */ +const os = require('os'); +const fs = require('fs'); -var runtime; +let runtime; +let isContainerCached; +let isWSLCached; -var util = require("@node-red/util").util; +const isInWsl = () => { + if (isWSLCached === undefined) { + isWSLCached = getIsInWSL(); + } + return isWSLCached; + function getIsInWSL() { + if (process.platform !== 'linux') { + return false; + } + try { + if (os.release().toLowerCase().includes('microsoft')) { + if (isInContainer()) { + return false; + } + return true; + } + return fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft') ? !isInContainer() : false; + } catch (_) { + return false; + } + } +}; + +const isInContainer = () => { + if (isContainerCached === undefined) { + isContainerCached = hasDockerEnv() || hasDockerCGroup(); + } + return isContainerCached; + function hasDockerEnv() { + try { + fs.statSync('/.dockerenv'); + return true; + } catch { + return false; + } + } + function hasDockerCGroup() { + try { + const s = fs.readFileSync('/proc/self/cgroup', 'utf8'); + if (s.includes('docker')) { + return "docker" + } else if (s.includes('kubepod')) { + return "kubepod" + } else if (s.includes('lxc')) { + return "lxc" + } + } catch { + return false; + } + } +} function buildDiagnosticReport(scope, callback) { - var basic = { - "report": "diagnostics", - "scope": scope, - "runtime": { - version: runtime.settings.version, - isStarted: runtime.isStarted() + const modules = {}; + const nl = runtime.nodes.getNodeList(); + for (let i = 0; i < nl.length; i++) { + if (modules[nl[i].module]) { + continue; + } + modules[nl[i].module] = nl[i].version + } + + const now = new Date(); + const report = { + report: "diagnostics", + scope: scope, + version: runtime.settings.version, + isStarted: runtime.isStarted(), + containerised: isInContainer(), + wsl: isInWsl(), + time: { + timestamp: now.valueOf(), + utc: "" + now, + locale: now.toLocaleString(), }, - settings: { - available: runtime.settings.available(), - apiMaxLength: runtime.settings.apiMaxLength || "NO SETTING", - coreNodesDir: runtime.settings.coreNodesDir, - contextStorage: listContextModules(), - debugMaxLength: runtime.settings.debugMaxLength, - editorTheme: runtime.settings.editorTheme, - flowFile: runtime.settings.flowFile, - disableEditor:runtime.settings.disableEditor, - debugMaxLength:runtime.settings.debugMaxLength, - - httpAdminRoot: runtime.settings.httpAdminRoot, - httpAdminCors: runtime.settings.httpAdminCors ? "HAS SETTING": "NOT SET", - httpNodeAuth: runtime.settings.httpNodeAuth ? "HAS SETTING": "NOT SET", - - httpNodeRoot: runtime.settings.httpNodeRoot, - httpNodeCors: runtime.settings.httpNodeCors ? "HAS SETTING": "NOT SET", + intl: Intl.DateTimeFormat().resolvedOptions(), + nodejs: { + version: process.version, + arch: process.arch, + platform: process.platform, + memoryUsage: process.memoryUsage(), + }, + os: { + totalmem: os.totalmem(), + freemem: os.freemem(), + arch: os.arch(), + loadavg: os.loadavg(), + platform: os.platform(), + release: os.release(), + type: os.type(), + uptime: os.uptime(), + version: os.version(), + }, + runtime: { + modules: modules, + settings: { + available: runtime.settings.available(), + apiMaxLength: runtime.settings.apiMaxLength || "NO SETTING", + //coreNodesDir: runtime.settings.coreNodesDir, + disableEditor: runtime.settings.disableEditor, + contextStorage: listContextModules(), + debugMaxLength: runtime.settings.debugMaxLength || "NO SETTING", + editorTheme: runtime.settings.editorTheme || "NO SETTING", + flowFile: runtime.settings.flowFile || "NO SETTING", + mqttReconnectTime: runtime.settings.mqttReconnectTime || "NO SETTING", + serialReconnectTime: runtime.settings.serialReconnectTime || "NO SETTING", - httpStatic: runtime.settings.httpStatic, - httpStaticCors: runtime.settings.httpStaticCors, + adminAuth: runtime.settings.adminAuth ? "HAS SETTING" : "NO SETTING", - mqttReconnectTime: runtime.settings.mqttReconnectTime, + httpAdminRoot: runtime.settings.adminAuth ? "HAS SETTING" : "NO SETTING", + httpAdminCors: runtime.settings.httpAdminCors ? "HAS SETTING" : "NO SETTING", + httpNodeAuth: runtime.settings.httpNodeAuth ? "HAS SETTING" : "NO SETTING", + httpAdminRoot: runtime.settings.httpAdminRoot || "NO SETTING", + httpAdminCors: runtime.settings.httpAdminCors ? "HAS SETTING" : "NO SETTING", - uiHost: runtime.settings.uiHost ? "HAS SETTING": "NOT SET", - uiPort: runtime.settings.uiPort ? "HAS SETTING": "NOT SET", - userDir: runtime.settings.userDir ? "HAS SETTING": "NOT SET", + httpNodeRoot: runtime.settings.httpNodeRoot || "NO SETTING", + httpNodeCors: runtime.settings.httpNodeCors ? "HAS SETTING" : "NO SETTING", - version: runtime.settings.version - } - } - var admin = {}; - if(scope == "admin") { - admin = { - httpAdminCors: runtime.settings.httpAdminCors ? runtime.settings.httpAdminCors : "NOT SET", - httpNodeCors: runtime.settings.httpNodeCors ? runtime.settings.httpNodeCors : "NOT SET", - uiHost: runtime.settings.uiHost ? runtime.settings.uiHost : "NOT SET", - uiPort: runtime.settings.uiPort ? runtime.settings.uiPort : "NOT SET", - userDir: runtime.settings.userDir ? runtime.settings.userDir : "NOT SET", + httpStatic: runtime.settings.httpStatic ? "HAS SETTING" : "NO SETTING", + httpStatic: runtime.settings.httpStaticRoot || "NO SETTING", + httpStaticCors: runtime.settings.httpStaticCors ? "HAS SETTING" : "NO SETTING", + + uiHost: runtime.settings.uiHost ? "HAS SETTING" : "NO SETTING", + uiPort: runtime.settings.uiPort ? "HAS SETTING" : "NO SETTING", + userDir: runtime.settings.userDir ? "HAS SETTING" : "NO SETTING", + } } } - var report = Object.assign({}, admin, basic); + if (scope == "admin") { + const moreSettings = { + adminAuth_type: (runtime.settings.adminAuth && runtime.settings.adminAuth.type) ? runtime.settings.adminAuth.type : "NO SETTING", + httpAdminCors: runtime.settings.httpAdminCors ? runtime.settings.httpAdminCors : "NO SETTING", + httpNodeCors: runtime.settings.httpNodeCors ? runtime.settings.httpNodeCors : "NO SETTING", + httpStaticCors: runtime.settings.httpStaticCors ? "HAS SETTING" : "NO SETTING", + settingsFile: runtime.settings.settingsFile ? runtime.settings.settingsFile : "NO SETTING", + uiHost: runtime.settings.uiHost ? runtime.settings.uiHost : "NO SETTING", + uiPort: runtime.settings.uiPort ? runtime.settings.uiPort : "NO SETTING", + userDir: runtime.settings.userDir ? runtime.settings.userDir : "NO SETTING", + } + const moreNodejs = { + execPath: process.execPath, + pid: process.pid, + } + const moreOs = { + cpus: os.cpus(), + homedir: os.homedir(), + hostname: os.hostname(), + networkInterfaces: os.networkInterfaces(), + } + report.runtime.settings = Object.assign({}, report.runtime.settings, moreSettings); + report.nodejs = Object.assign({}, report.nodejs, moreNodejs); + report.os = Object.assign({}, report.os, moreOs); + } callback(report); + /** gets a sanitised list containing only the module name */ function listContextModules() { - var keys = Object.keys(runtime.settings.contextStorage); - var result = {}; + const keys = Object.keys(runtime.settings.contextStorage); + const result = {}; keys.forEach(e => { result[e] = { - module: runtime.settings.contextStorage[e].module + module: String(runtime.settings.contextStorage[e].module) } }) return result; @@ -75,40 +178,24 @@ function buildDiagnosticReport(scope, callback) { } -var api = module.exports = { +module.exports = { init: function (_runtime) { runtime = _runtime; }, /** * Gets the node-red diagnostics report - * @param {{scope: string}} - settings - * @return {Promise} - the diagnostics information + * @param {{scope: string}} opts - settings + * @return {Promise} the diagnostics information * @memberof @node-red/diagnostics */ get: async function (opts) { return new Promise(function (resolve, reject) { opts = opts || {} - var scope = opts.scope; try { - if (scope === 'admin') { - //admin level info - runtime.log.audit({ event: "diagnostics.get", scope: "admin" }, opts.req); - buildDiagnosticReport(scope, (report) => resolve(report)); - } else if (scope === 'detail') { - //detail! - runtime.log.audit({ event: "diagnostics.get", scope: "detail" }, opts.req); - buildDiagnosticReport(scope, (report) => resolve(report)); - } else if (scope === 'basic') { - //basic! - runtime.log.audit({ event: "diagnostics.get", scope: "basic" }, opts.req); - buildDiagnosticReport(scope, (report) => resolve(report)); - - } else { - runtime.log.audit({ event: "diagnostics.get", scope: scope }, opts.req); - resolve({}); - } + runtime.log.audit({ event: "diagnostics.get", scope: opts.scope }, opts.req); + buildDiagnosticReport(opts.scope, (report) => resolve(report)); } catch (error) { - runtime.log.audit({ event: "diagnostics.get", scope: scope }, opts.req); + error.status = 500; reject(error); } }) From cdc8a423936db147e50f7f3790b8fb0307c2076f Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Mon, 28 Mar 2022 18:50:12 +0100 Subject: [PATCH 03/16] Add `diagnosticsOptions` to settings.js --- packages/node_modules/node-red/settings.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js index 89994e9c3..0cfa1f975 100644 --- a/packages/node_modules/node-red/settings.js +++ b/packages/node_modules/node-red/settings.js @@ -224,6 +224,7 @@ module.exports = { /******************************************************************************* * Runtime Settings * - lang + * - diagnosticsOptions * - logging * - contextStorage * - exportGlobalContextKeys @@ -236,6 +237,19 @@ module.exports = { */ // lang: "de", + /** ### Configure diagnostics options + * - `.enabled`: When `enabled` is `true` (or unset), diagnostics data will + * be available at http://localhost:1880/diagnostics + * - `.level`: When `level` is "basic" (or unset), the diagnostics will not + * include sensitive data. Set level to "admin" for detailed diagnostics + */ + diagnosticsOptions: { + /** @type {boolean} enable or disable diagnostics. Must be set to `false` to disable */ + enabled: true, + /** @type {"basic"|"admin"} diagnostic level can be "basic" (default) or "admin" (more sensitive details are included) */ + level: "basic", + }, + /** Configure the logging output */ logging: { /** Only console logging is currently supported */ From cf6df1556c49d4c7a1acc2165bb2f7166dc0fcce Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Tue, 29 Mar 2022 08:35:09 +0100 Subject: [PATCH 04/16] Ensure UTC time is UTC --- packages/node_modules/@node-red/runtime/lib/api/diagnostics.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/runtime/lib/api/diagnostics.js b/packages/node_modules/@node-red/runtime/lib/api/diagnostics.js index f8ab9bbaf..9dc5a2dc0 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/diagnostics.js +++ b/packages/node_modules/@node-red/runtime/lib/api/diagnostics.js @@ -78,7 +78,7 @@ function buildDiagnosticReport(scope, callback) { wsl: isInWsl(), time: { timestamp: now.valueOf(), - utc: "" + now, + utc: "" + now.toUTCString(), locale: now.toLocaleString(), }, intl: Intl.DateTimeFormat().resolvedOptions(), From 03763a1423dbd9b3b0acaa9f1c2da6ce0fdb7bbc Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Tue, 29 Mar 2022 20:48:29 +0100 Subject: [PATCH 05/16] Update diagnostics as per Issue discussion --- .../editor-api/lib/admin/diagnostics.js | 8 +- .../@node-red/runtime/lib/api/diagnostics.js | 108 +++++++++--------- packages/node_modules/node-red/settings.js | 14 +-- 3 files changed, 66 insertions(+), 64 deletions(-) diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/diagnostics.js b/packages/node_modules/@node-red/editor-api/lib/admin/diagnostics.js index 3ad83fc63..f57a5d125 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/diagnostics.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/diagnostics.js @@ -7,13 +7,13 @@ module.exports = { runtimeAPI = _runtimeAPI; }, getReport: function(req, res) { - const diagnosticsOptions = settings.diagnosticsOptions || {}; + const diagnosticsOpts = settings.diagnostics || {}; const opts = { user: req.user, - scope: diagnosticsOptions.level || "basic" + scope: diagnosticsOpts.level || "basic" } - if(diagnosticsOptions.enabled === false || diagnosticsOptions.enabled === "false") { - apiUtil.rejectHandler(req, res, {message: "disabled", status: 403, code: "diagnosticsOptions.enabled" }) + if(diagnosticsOpts.enabled === false || diagnosticsOpts.enabled === "false") { + apiUtil.rejectHandler(req, res, {message: "disabled", status: 403, code: "diagnostics.enabled" }) } else { runtimeAPI.diagnostics.get(opts) .then(function(result) { res.json(result); }) diff --git a/packages/node_modules/@node-red/runtime/lib/api/diagnostics.js b/packages/node_modules/@node-red/runtime/lib/api/diagnostics.js index 9dc5a2dc0..d942a9aa1 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/diagnostics.js +++ b/packages/node_modules/@node-red/runtime/lib/api/diagnostics.js @@ -69,19 +69,17 @@ function buildDiagnosticReport(scope, callback) { } const now = new Date(); + const {locale, timeZone} = Intl.DateTimeFormat().resolvedOptions(); const report = { report: "diagnostics", scope: scope, - version: runtime.settings.version, - isStarted: runtime.isStarted(), - containerised: isInContainer(), - wsl: isInWsl(), time: { - timestamp: now.valueOf(), - utc: "" + now.toUTCString(), - locale: now.toLocaleString(), + utc: now.toUTCString(), + local: now.toLocaleString(), + }, + intl: { + locale, timeZone }, - intl: Intl.DateTimeFormat().resolvedOptions(), nodejs: { version: process.version, arch: process.arch, @@ -89,6 +87,8 @@ function buildDiagnosticReport(scope, callback) { memoryUsage: process.memoryUsage(), }, os: { + containerised: isInContainer(), + wsl: isInWsl(), totalmem: os.totalmem(), freemem: os.freemem(), arch: os.arch(), @@ -100,67 +100,69 @@ function buildDiagnosticReport(scope, callback) { version: os.version(), }, runtime: { + isStarted: runtime.isStarted(), modules: modules, + version: runtime.settings.version, settings: { available: runtime.settings.available(), - apiMaxLength: runtime.settings.apiMaxLength || "NO SETTING", + apiMaxLength: runtime.settings.apiMaxLength || "UNSET", //coreNodesDir: runtime.settings.coreNodesDir, disableEditor: runtime.settings.disableEditor, contextStorage: listContextModules(), - debugMaxLength: runtime.settings.debugMaxLength || "NO SETTING", - editorTheme: runtime.settings.editorTheme || "NO SETTING", - flowFile: runtime.settings.flowFile || "NO SETTING", - mqttReconnectTime: runtime.settings.mqttReconnectTime || "NO SETTING", - serialReconnectTime: runtime.settings.serialReconnectTime || "NO SETTING", + debugMaxLength: runtime.settings.debugMaxLength || "UNSET", + editorTheme: runtime.settings.editorTheme || "UNSET", + flowFile: runtime.settings.flowFile || "UNSET", + mqttReconnectTime: runtime.settings.mqttReconnectTime || "UNSET", + serialReconnectTime: runtime.settings.serialReconnectTime || "UNSET", - adminAuth: runtime.settings.adminAuth ? "HAS SETTING" : "NO SETTING", + adminAuth: runtime.settings.adminAuth ? "SET" : "UNSET", - httpAdminRoot: runtime.settings.adminAuth ? "HAS SETTING" : "NO SETTING", - httpAdminCors: runtime.settings.httpAdminCors ? "HAS SETTING" : "NO SETTING", - httpNodeAuth: runtime.settings.httpNodeAuth ? "HAS SETTING" : "NO SETTING", + httpAdminRoot: runtime.settings.adminAuth ? "SET" : "UNSET", + httpAdminCors: runtime.settings.httpAdminCors ? "SET" : "UNSET", + httpNodeAuth: runtime.settings.httpNodeAuth ? "SET" : "UNSET", - httpAdminRoot: runtime.settings.httpAdminRoot || "NO SETTING", - httpAdminCors: runtime.settings.httpAdminCors ? "HAS SETTING" : "NO SETTING", + httpAdminRoot: runtime.settings.httpAdminRoot || "UNSET", + httpAdminCors: runtime.settings.httpAdminCors ? "SET" : "UNSET", - httpNodeRoot: runtime.settings.httpNodeRoot || "NO SETTING", - httpNodeCors: runtime.settings.httpNodeCors ? "HAS SETTING" : "NO SETTING", + httpNodeRoot: runtime.settings.httpNodeRoot || "UNSET", + httpNodeCors: runtime.settings.httpNodeCors ? "SET" : "UNSET", - httpStatic: runtime.settings.httpStatic ? "HAS SETTING" : "NO SETTING", - httpStatic: runtime.settings.httpStaticRoot || "NO SETTING", - httpStaticCors: runtime.settings.httpStaticCors ? "HAS SETTING" : "NO SETTING", + httpStatic: runtime.settings.httpStatic ? "SET" : "UNSET", + httpStatic: runtime.settings.httpStaticRoot || "UNSET", + httpStaticCors: runtime.settings.httpStaticCors ? "SET" : "UNSET", - uiHost: runtime.settings.uiHost ? "HAS SETTING" : "NO SETTING", - uiPort: runtime.settings.uiPort ? "HAS SETTING" : "NO SETTING", - userDir: runtime.settings.userDir ? "HAS SETTING" : "NO SETTING", + uiHost: runtime.settings.uiHost ? "SET" : "UNSET", + uiPort: runtime.settings.uiPort ? "SET" : "UNSET", + userDir: runtime.settings.userDir ? "SET" : "UNSET", } } } - if (scope == "admin") { - const moreSettings = { - adminAuth_type: (runtime.settings.adminAuth && runtime.settings.adminAuth.type) ? runtime.settings.adminAuth.type : "NO SETTING", - httpAdminCors: runtime.settings.httpAdminCors ? runtime.settings.httpAdminCors : "NO SETTING", - httpNodeCors: runtime.settings.httpNodeCors ? runtime.settings.httpNodeCors : "NO SETTING", - httpStaticCors: runtime.settings.httpStaticCors ? "HAS SETTING" : "NO SETTING", - settingsFile: runtime.settings.settingsFile ? runtime.settings.settingsFile : "NO SETTING", - uiHost: runtime.settings.uiHost ? runtime.settings.uiHost : "NO SETTING", - uiPort: runtime.settings.uiPort ? runtime.settings.uiPort : "NO SETTING", - userDir: runtime.settings.userDir ? runtime.settings.userDir : "NO SETTING", - } - const moreNodejs = { - execPath: process.execPath, - pid: process.pid, - } - const moreOs = { - cpus: os.cpus(), - homedir: os.homedir(), - hostname: os.hostname(), - networkInterfaces: os.networkInterfaces(), - } - report.runtime.settings = Object.assign({}, report.runtime.settings, moreSettings); - report.nodejs = Object.assign({}, report.nodejs, moreNodejs); - report.os = Object.assign({}, report.os, moreOs); - } + // if (scope == "admin") { + // const moreSettings = { + // adminAuth_type: (runtime.settings.adminAuth && runtime.settings.adminAuth.type) ? runtime.settings.adminAuth.type : "UNSET", + // httpAdminCors: runtime.settings.httpAdminCors ? runtime.settings.httpAdminCors : "UNSET", + // httpNodeCors: runtime.settings.httpNodeCors ? runtime.settings.httpNodeCors : "UNSET", + // httpStaticCors: runtime.settings.httpStaticCors ? "SET" : "UNSET", + // settingsFile: runtime.settings.settingsFile ? runtime.settings.settingsFile : "UNSET", + // uiHost: runtime.settings.uiHost ? runtime.settings.uiHost : "UNSET", + // uiPort: runtime.settings.uiPort ? runtime.settings.uiPort : "UNSET", + // userDir: runtime.settings.userDir ? runtime.settings.userDir : "UNSET", + // } + // const moreNodejs = { + // execPath: process.execPath, + // pid: process.pid, + // } + // const moreOs = { + // cpus: os.cpus(), + // homedir: os.homedir(), + // hostname: os.hostname(), + // networkInterfaces: os.networkInterfaces(), + // } + // report.runtime.settings = Object.assign({}, report.runtime.settings, moreSettings); + // report.nodejs = Object.assign({}, report.nodejs, moreNodejs); + // report.os = Object.assign({}, report.os, moreOs); + // } callback(report); diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js index 0cfa1f975..7136c8644 100644 --- a/packages/node_modules/node-red/settings.js +++ b/packages/node_modules/node-red/settings.js @@ -224,7 +224,7 @@ module.exports = { /******************************************************************************* * Runtime Settings * - lang - * - diagnosticsOptions + * - diagnostics * - logging * - contextStorage * - exportGlobalContextKeys @@ -240,14 +240,14 @@ module.exports = { /** ### Configure diagnostics options * - `.enabled`: When `enabled` is `true` (or unset), diagnostics data will * be available at http://localhost:1880/diagnostics - * - `.level`: When `level` is "basic" (or unset), the diagnostics will not - * include sensitive data. Set level to "admin" for detailed diagnostics + * - `.ui`: When `ui` is `true` (or unset), the action `show-system-info` will + * be available to logged in users of node-red editor */ - diagnosticsOptions: { - /** @type {boolean} enable or disable diagnostics. Must be set to `false` to disable */ + diagnostics: { + /** @type {boolean} `enabled` - enable or disable diagnostics endpoint. Must be set to `false` to disable */ enabled: true, - /** @type {"basic"|"admin"} diagnostic level can be "basic" (default) or "admin" (more sensitive details are included) */ - level: "basic", + /** @type {boolean} `ui` - enable or disable diagnostics display in the node-red editor. Must be set to `false` to disable */ + ui: true, }, /** Configure the logging output */ From 5633c5224e7da4dc94b64862653b998deb497cd1 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Tue, 29 Mar 2022 20:59:35 +0100 Subject: [PATCH 06/16] Add system info UI --- Gruntfile.js | 1 + .../@node-red/editor-client/src/js/red.js | 1 + .../editor-client/src/js/ui/diagnostics.js | 61 +++++++++++++ .../editor-client/src/js/ui/editors/json.js | 89 ++++++++++++------- .../editor-client/src/sass/editor.scss | 8 ++ .../@node-red/runtime/lib/api/settings.js | 7 ++ 6 files changed, 136 insertions(+), 31 deletions(-) create mode 100644 packages/node_modules/@node-red/editor-client/src/js/ui/diagnostics.js diff --git a/Gruntfile.js b/Gruntfile.js index f1f0aaefd..979b38051 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -165,6 +165,7 @@ module.exports = function(grunt) { "packages/node_modules/@node-red/editor-client/src/js/ui/common/autoComplete.js", "packages/node_modules/@node-red/editor-client/src/js/ui/actions.js", "packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js", + "packages/node_modules/@node-red/editor-client/src/js/ui/diagnostics.js", "packages/node_modules/@node-red/editor-client/src/js/ui/diff.js", "packages/node_modules/@node-red/editor-client/src/js/ui/keyboard.js", "packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js", diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index 046fec988..dc36e5c1e 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -730,6 +730,7 @@ var RED = (function() { RED.search.init(); RED.actionList.init(); RED.editor.init(); + RED.diagnostics.init(); RED.diff.init(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/diagnostics.js b/packages/node_modules/@node-red/editor-client/src/js/ui/diagnostics.js new file mode 100644 index 000000000..89bd093c4 --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/diagnostics.js @@ -0,0 +1,61 @@ + +RED.diagnostics = (function () { + + function init() { + if (RED.settings.get('diagnostics.ui', true) === false) { + return; + } + RED.actions.add("core:show-system-info", function () { show(); }); + } + + function show() { + $.ajax({ + headers: { + "Accept": "application/json" + }, + cache: false, + url: 'diagnostics', + success: function (data) { + var json = JSON.stringify(data || {}, "", 4); + if (json === "{}") { + json = "{\n\n}"; + } + RED.editor.editJSON({ + title: "System Info", //RED._('sidebar.project.editDependencies'), + value: json, + requireValid: true, + readOnly: true, + jsonButtons: [ + { + text: 'Copy', + icon: 'fa fa-copy', + click: function () { + RED.clipboard.copyText(json, $(this), "Copied to clipboard") + } + }, + { + text: 'Download', + icon: 'fa fa-download', + click: function () { + var element = document.createElement('a'); + element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(json)); + element.setAttribute('download', "system-info.json"); + element.style.display = 'none'; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); + } + }, + ] + }); + }, + error: function (jqXHR, textStatus, errorThrown) { + console.log("Unexpected error loading system info:", jqXHR.status, textStatus, errorThrown); + } + }); + } + + return { + init: init, + }; +})(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js index 52ab820ac..7687abee5 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js @@ -34,7 +34,7 @@ var activeTab; - function insertNewItem(parent,index,copyIndex) { + function insertNewItem(parent,index,copyIndex,readOnly) { var newValue = ""; if (parent.children.length > 0) { @@ -60,26 +60,26 @@ newKey = keyRoot+"-"+(keySuffix++); } } - var newItem = handleItem(newKey,newValue,parent.depth+1,parent); + var newItem = handleItem(newKey,newValue,parent.depth+1,parent,readOnly); parent.treeList.insertChildAt(newItem, index, true); parent.treeList.expand(); } - function showObjectMenu(button,item) { + function showObjectMenu(button,item,readOnly) { var elementPos = button.offset(); var options = []; if (item.parent) { options.push({id:"red-ui-editor-type-json-menu-insert-above", icon:"fa fa-toggle-up", label:RED._('jsonEditor.insertAbove'),onselect:function(){ var index = item.parent.children.indexOf(item); - insertNewItem(item.parent,index,index); + insertNewItem(item.parent,index,index,readOnly); }}); options.push({id:"red-ui-editor-type-json-menu-insert-below", icon:"fa fa-toggle-down", label:RED._('jsonEditor.insertBelow'),onselect:function(){ var index = item.parent.children.indexOf(item)+1; - insertNewItem(item.parent,index,index-1); + insertNewItem(item.parent,index,index-1,readOnly); }}); } if (item.type === 'array' || item.type === 'object') { options.push({id:"red-ui-editor-type-json-menu-add-child", icon:"fa fa-plus", label:RED._('jsonEditor.addItem'),onselect:function(){ - insertNewItem(item,item.children.length,item.children.length-1); + insertNewItem(item,item.children.length,item.children.length-1,readOnly); }}); } if (item.parent) { @@ -121,7 +121,7 @@ newKey = keyRoot+"-"+(keySuffix++); } } - var newItem = handleItem(newKey,convertToObject(item),item.parent.depth+1,item.parent); + var newItem = handleItem(newKey,convertToObject(item),item.parent.depth+1,item.parent,readOnly); var index = item.parent.children.indexOf(item)+1; item.parent.treeList.insertChildAt(newItem, index, true); @@ -171,24 +171,24 @@ menuOptionMenu.show(); } - function parseObject(obj,depth,parent) { + function parseObject(obj,depth,parent,readOnly) { var result = []; for (var prop in obj) { if (obj.hasOwnProperty(prop)) { - result.push(handleItem(prop,obj[prop],depth,parent)); + result.push(handleItem(prop,obj[prop],depth,parent,readOnly)); } } return result; } - function parseArray(obj,depth,parent) { + function parseArray(obj,depth,parent,readOnly) { var result = []; var l = obj.length; for (var i=0;i'); if (key != null) { @@ -204,11 +204,14 @@ if (parent && parent.type === "array") { keyLabel.addClass("red-ui-editor-type-json-editor-label-array-key") } - + if(readOnly) { + keyLabel.addClass("readonly") + } keyLabel.on("click", function(evt) { if (item.parent.type === 'array') { return; } + if (readOnly) { return; } evt.preventDefault(); evt.stopPropagation(); var w = Math.max(150,keyLabel.width()); @@ -253,10 +256,10 @@ item.expanded = depth < 2; item.type = "array"; item.deferBuild = depth >= 2; - item.children = parseArray(val,depth+1,item); + item.children = parseArray(val,depth+1,item,readOnly); } else if (val !== null && item.type === "object") { item.expanded = depth < 2; - item.children = parseObject(val,depth+1,item); + item.children = parseObject(val,depth+1,item,readOnly); item.deferBuild = depth >= 2; } else { item.value = val; @@ -287,7 +290,11 @@ // var orphanedChildren; var valueLabel = $('').addClass(valClass).text(valValue).appendTo(container); + if (readOnly) { + valueLabel.addClass("readonly") + } valueLabel.on("click", function(evt) { + if (readOnly) { return; } evt.preventDefault(); evt.stopPropagation(); if (valType === 'str') { @@ -395,17 +402,19 @@ valueLabel.hide(); }) item.gutter = $(''); - - if (parent) {//red-ui-editor-type-json-editor-item-handle - $('').appendTo(item.gutter); - } else { - $('').appendTo(item.gutter); + if(!readOnly) { + if (parent) { + $('').appendTo(item.gutter); + } else { + $('').appendTo(item.gutter); + } + $('').appendTo(item.gutter).on("click", function(evt) { + evt.preventDefault(); + evt.stopPropagation(); + showObjectMenu($(this), item, readOnly); + }); } - $('').appendTo(item.gutter).on("click", function(evt) { - evt.preventDefault(); - evt.stopPropagation(); - showObjectMenu($(this), item); - }); + item.element = container; return item; } @@ -498,7 +507,25 @@ open: function(tray) { var trayBody = tray.find('.red-ui-tray-body'); var dialogForm = RED.editor.buildEditForm(tray.find('.red-ui-tray-body'),'dialog-form',type,'editor'); - + var jsonButtons = options.jsonButtons || []; + if (jsonButtons.length) { + jsonButtons.forEach(function (button) { + var element = $('') + .insertBefore("#node-input-json-reformat") + .on("click", function (evt) { + evt.preventDefault(); + if (button.click !== undefined) { + button.click(evt); + } + }); + if (button.id) { element.attr("id", button.id); } + if (button.title) { element.attr("title", button.title); } + if (button.icon) { element.append($("").attr("class", button.icon)); } + if (button.label || button.text) { + element.append($("").text(" " + (button.label || button.text))); + } + }); + } var container = $("#red-ui-editor-type-json-tab-ui-container").css({"height":"100%"}); var filterDepth = Infinity; var list = $('
').appendTo(container).treeList({ @@ -528,11 +555,11 @@ }) }); - expressionEditor = RED.editor.createEditor({ id: 'node-input-json', value: "", - mode:"ace/mode/json" + mode:"ace/mode/json", + readOnly: !!options.readOnly }); expressionEditor.getSession().setValue(value||"",-1); if (options.requireValid) { @@ -571,7 +598,7 @@ var raw = expressionEditor.getValue().trim() ||"{}"; try { var parsed = JSON.parse(raw); - rootNode = handleItem(null,parsed,0,null); + rootNode = handleItem(null,parsed,0,null,options.readOnly); rootNode.class = "red-ui-editor-type-json-root-node" list.treeList('data',[rootNode]); } catch(err) { @@ -589,12 +616,12 @@ tabs.addTab({ id: 'json-raw', - label: RED._('jsonEditor.rawMode'), + label: options.readOnly ? "JSON" : RED._('jsonEditor.rawMode'), content: $("#red-ui-editor-type-json-tab-raw") }); tabs.addTab({ id: 'json-ui', - label: RED._('jsonEditor.uiMode'), + label: options.readOnly ? "Visual" : RED._('jsonEditor.uiMode'), content: $("#red-ui-editor-type-json-tab-ui") }); finishedBuild = true; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/editor.scss b/packages/node_modules/@node-red/editor-client/src/sass/editor.scss index 473571042..df2d3cc6f 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/editor.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/editor.scss @@ -685,6 +685,10 @@ div.red-ui-button-small.red-ui-color-picker-opacity-slider-handle { border-color: $list-item-background-hover; border-style: dashed; } + &.readonly { + cursor: pointer; + pointer-events: none; + } } .red-ui-editor-type-json-editor-item-gutter { width: 48px; @@ -704,6 +708,10 @@ div.red-ui-button-small.red-ui-color-picker-opacity-slider-handle { > span, > button { display: none; } + &.readonly { + cursor: pointer; + pointer-events: none; + } } diff --git a/packages/node_modules/@node-red/runtime/lib/api/settings.js b/packages/node_modules/@node-red/runtime/lib/api/settings.js index 0224c0ca6..f56b8ab61 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/settings.js +++ b/packages/node_modules/@node-red/runtime/lib/api/settings.js @@ -142,6 +142,13 @@ var api = module.exports = { } safeSettings.flowEncryptionType = runtime.nodes.getCredentialKeyType(); + + safeSettings.diagnostics = { + //unless diagnostics.ui and diagnostics.enabled are explicitly false, they will default to true. + enabled: (runtime.settings.diagnostics && runtime.settings.diagnostics.enabled === false) ? false : true, + ui: (runtime.settings.diagnostics && runtime.settings.diagnostics.ui === false) ? false : true + } + runtime.settings.exportNodeSettings(safeSettings); runtime.plugins.exportPluginSettings(safeSettings); } From ccb3c991a63d546d71f1d358ccd5b37984eec013 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Wed, 6 Apr 2022 15:11:03 +0100 Subject: [PATCH 07/16] improve rejection message --- .../node_modules/@node-red/editor-api/lib/admin/diagnostics.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/diagnostics.js b/packages/node_modules/@node-red/editor-api/lib/admin/diagnostics.js index f57a5d125..98710a069 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/diagnostics.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/diagnostics.js @@ -13,7 +13,7 @@ module.exports = { scope: diagnosticsOpts.level || "basic" } if(diagnosticsOpts.enabled === false || diagnosticsOpts.enabled === "false") { - apiUtil.rejectHandler(req, res, {message: "disabled", status: 403, code: "diagnostics.enabled" }) + apiUtil.rejectHandler(req, res, {message: "diagnostics are disabled", status: 403, code: "diagnostics.disabled" }) } else { runtimeAPI.diagnostics.get(opts) .then(function(result) { res.json(result); }) From 76c0e140cf8d97f0a519d3fcdd69d13261d3c015 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Wed, 6 Apr 2022 15:11:28 +0100 Subject: [PATCH 08/16] fix up report - remove duplicate entries --- .../node_modules/@node-red/runtime/lib/api/diagnostics.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/node_modules/@node-red/runtime/lib/api/diagnostics.js b/packages/node_modules/@node-red/runtime/lib/api/diagnostics.js index d942a9aa1..0293c7f97 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/diagnostics.js +++ b/packages/node_modules/@node-red/runtime/lib/api/diagnostics.js @@ -117,18 +117,15 @@ function buildDiagnosticReport(scope, callback) { adminAuth: runtime.settings.adminAuth ? "SET" : "UNSET", - httpAdminRoot: runtime.settings.adminAuth ? "SET" : "UNSET", - httpAdminCors: runtime.settings.httpAdminCors ? "SET" : "UNSET", - httpNodeAuth: runtime.settings.httpNodeAuth ? "SET" : "UNSET", - httpAdminRoot: runtime.settings.httpAdminRoot || "UNSET", httpAdminCors: runtime.settings.httpAdminCors ? "SET" : "UNSET", + httpNodeAuth: runtime.settings.httpNodeAuth ? "SET" : "UNSET", httpNodeRoot: runtime.settings.httpNodeRoot || "UNSET", httpNodeCors: runtime.settings.httpNodeCors ? "SET" : "UNSET", httpStatic: runtime.settings.httpStatic ? "SET" : "UNSET", - httpStatic: runtime.settings.httpStaticRoot || "UNSET", + httpStaticRoot: runtime.settings.httpStaticRoot || "UNSET", httpStaticCors: runtime.settings.httpStaticCors ? "SET" : "UNSET", uiHost: runtime.settings.uiHost ? "SET" : "UNSET", From 39f303fcd6a995ac4608daf85a513b1ef5b36db5 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Wed, 6 Apr 2022 15:11:37 +0100 Subject: [PATCH 09/16] add unit tests --- .../editor-api/lib/admin/diagnostics_spec.js | 119 +++++++++++++++++ .../runtime/lib/api/diagnostics_spec.js | 126 ++++++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 test/unit/@node-red/editor-api/lib/admin/diagnostics_spec.js create mode 100644 test/unit/@node-red/runtime/lib/api/diagnostics_spec.js diff --git a/test/unit/@node-red/editor-api/lib/admin/diagnostics_spec.js b/test/unit/@node-red/editor-api/lib/admin/diagnostics_spec.js new file mode 100644 index 000000000..16d1e5b94 --- /dev/null +++ b/test/unit/@node-red/editor-api/lib/admin/diagnostics_spec.js @@ -0,0 +1,119 @@ +const should = require("should"); +const request = require('supertest'); +const express = require('express'); +const bodyParser = require("body-parser"); +const sinon = require('sinon'); + +let app; + +const NR_TEST_UTILS = require("nr-test-utils"); +const diagnostics = NR_TEST_UTILS.require("@node-red/editor-api/lib/admin/diagnostics"); + +describe("api/editor/diagnostics", function() { + before(function() { + app = express(); + app.use(bodyParser.json()); + app.get("/diagnostics",diagnostics.getReport); + }); + + it('returns the diagnostics report when explicitly enabled', function(done) { + const settings = { diagnostics: { ui: true, enabled: true } } + const runtimeAPI = { + diagnostics: { + get: async function (opts) { + return new Promise(function (resolve, reject) { + opts = opts || {} + try { + resolve({ opts: opts, a:1, b:2}); + } catch (error) { + error.status = 500; + reject(error); + } + }) + } + } + } + + diagnostics.init(settings, runtimeAPI); + + request(app) + .get("/diagnostics") + .expect(200) + .end(function(err,res) { + if (err || typeof res.error === "object") { + return done(err || res.error); + } + res.should.have.property("statusCode",200); + res.body.should.have.property("a",1); + res.body.should.have.property("b",2); + done(); + }); + }); + it('returns the diagnostics report when not explicitly enabled (implicitly enabled)', function(done) { + const settings = { diagnostics: { enabled: undefined } } + const runtimeAPI = { + diagnostics: { + get: async function (opts) { + return new Promise(function (resolve, reject) { + opts = opts || {} + try { + resolve({ opts: opts, a:3, b:4}); + } catch (error) { + error.status = 500; + reject(error); + } + }) + } + } + } + + diagnostics.init(settings, runtimeAPI); + + request(app) + .get("/diagnostics") + .expect(200) + .end(function(err,res) { + if (err || typeof res.error === "object") { + return done(err || res.error); + } + res.should.have.property("statusCode",200); + res.body.should.have.property("a",3); + res.body.should.have.property("b",4); + done(); + }); + }); + it('should error when setting is disabled', function(done) { + const settings = { diagnostics: { ui: true, enabled: false } } + const runtimeAPI = { + diagnostics: { + get: async function (opts) { + return new Promise(function (resolve, reject) { + opts = opts || {} + try { + resolve({ opts: opts}); + } catch (error) { + error.status = 500; + reject(error); + } + }) + } + } + } + + diagnostics.init(settings, runtimeAPI); + + request(app) + .get("/diagnostics") + .expect(403) + .end(function(err,res) { + if (!err && typeof res.error !== "object") { + return done(new Error("accessing diagnostics endpoint while disabled should raise error")); + } + res.should.have.property("statusCode",403); + res.body.should.have.property("message","diagnostics are disabled"); + res.body.should.have.property("code","diagnostics.disabled"); + done(); + }); + }); + +}); diff --git a/test/unit/@node-red/runtime/lib/api/diagnostics_spec.js b/test/unit/@node-red/runtime/lib/api/diagnostics_spec.js new file mode 100644 index 000000000..07e499344 --- /dev/null +++ b/test/unit/@node-red/runtime/lib/api/diagnostics_spec.js @@ -0,0 +1,126 @@ + +var should = require("should"); +var sinon = require("sinon"); +var NR_TEST_UTILS = require("nr-test-utils"); +var diagnostics = NR_TEST_UTILS.require("@node-red/runtime/lib/api/diagnostics") + +var mockLog = () => ({ + log: sinon.stub(), + debug: sinon.stub(), + trace: sinon.stub(), + warn: sinon.stub(), + info: sinon.stub(), + metric: sinon.stub(), + audit: sinon.stub(), + _: function() { return "abc"} +}) + +describe("runtime-api/diagnostics", function() { + + describe("get", function() { + before(function() { + diagnostics.init({ + isStarted: () => true, + nodes: { + getNodeList: () => [{module:"node-red", version:"9.9.9"},{module:"node-red-node-inject", version:"8.8.8"}] + }, + settings: { + version: "7.7.7", + available: () => true, + //apiMaxLength: xxx, deliberately left blank. Should arrive in report as "UNSET" + debugMaxLength: 1111, + disableEditor: false, + flowFile: "flows.json", + mqttReconnectTime: 321, + serialReconnectTime: 432, + adminAuth: {},//should be sanitised to "SET" + httpAdminRoot: "/admin/root/", + httpAdminCors: {},//should be sanitised to "SET" + httpNodeAuth: {},//should be sanitised to "SET" + httpNodeRoot: "/node/root/", + httpNodeCors: {},//should be sanitised to "SET" + httpStatic: "/var/static/",//should be sanitised to "SET" + httpStaticRoot: "/static/root/", + httpStaticCors: {},//should be sanitised to "SET" + uiHost: "something.secret.com",//should be sanitised to "SET" + uiPort: 1337,//should be sanitised to "SET" + userDir: "/var/super/secret/",//should be sanitised to "SET", + contextStorage: { + default : { module: "memory" }, + file: { module: "localfilesystem" }, + secured: { module: "secure_store", user: "fred", pass: "super-duper-secret" }, + }, + editorTheme: {} + }, + log: mockLog() + }); + }) + it("returns basic user settings", function() { + return diagnostics.get({scope:"fake_scope"}).then(result => { + should(result).be.type("object"); + + //result.xxxxx + Object.keys(result) + const reportPropCount = Object.keys(result).length; + reportPropCount.should.eql(7);//ensure no more than 7 keys are present in the report (avoid leakage of extra info) + result.should.have.property("report","diagnostics"); + result.should.have.property("scope","fake_scope"); + result.should.have.property("time").type("object"); + result.should.have.property("intl").type("object"); + result.should.have.property("nodejs").type("object"); + result.should.have.property("os").type("object"); + result.should.have.property("runtime").type("object"); + + //result.runtime.xxxxx + const runtimeCount = Object.keys(result.runtime).length; + runtimeCount.should.eql(4);//ensure no more than 4 keys are present in runtime + result.runtime.should.have.property('isStarted',true) + result.runtime.should.have.property('modules').type("object"); + result.runtime.should.have.property('settings').type("object"); + result.runtime.should.have.property('version','7.7.7'); + + //result.runtime.modules.xxxxx + const moduleCount = Object.keys(result.runtime.modules).length; + moduleCount.should.eql(2);//ensure no more than the 2 modules specified are present + result.runtime.modules.should.have.property('node-red','9.9.9'); + result.runtime.modules.should.have.property('node-red-node-inject','8.8.8'); + + //result.runtime.settings.xxxxx + const settingsCount = Object.keys(result.runtime.settings).length; + settingsCount.should.eql(21);//ensure no more than the 21 settings listed below are present in the settings object + result.runtime.settings.should.have.property('available',true); + result.runtime.settings.should.have.property('apiMaxLength', "UNSET");//deliberately disabled to ensure UNSET is returned + result.runtime.settings.should.have.property('debugMaxLength', 1111); + result.runtime.settings.should.have.property('disableEditor', false); + result.runtime.settings.should.have.property('editorTheme', {}); + result.runtime.settings.should.have.property('flowFile', "flows.json"); + result.runtime.settings.should.have.property('mqttReconnectTime', 321); + result.runtime.settings.should.have.property('serialReconnectTime', 432); + result.runtime.settings.should.have.property("adminAuth", "SET"); //should be sanitised to "SET" + result.runtime.settings.should.have.property("httpAdminCors", "SET"); //should be sanitised to "SET" + result.runtime.settings.should.have.property('httpAdminRoot', "/admin/root/"); + result.runtime.settings.should.have.property("httpNodeAuth", "SET"); //should be sanitised to "SET" + result.runtime.settings.should.have.property("httpNodeCors", "SET"); //should be sanitised to "SET" + result.runtime.settings.should.have.property('httpNodeRoot', "/node/root/"); + result.runtime.settings.should.have.property("httpStatic", "SET"); //should be sanitised to "SET" + result.runtime.settings.should.have.property('httpStaticRoot', "/static/root/"); + result.runtime.settings.should.have.property("httpStaticCors", "SET"); //should be sanitised to "SET" + result.runtime.settings.should.have.property("uiHost", "SET"); //should be sanitised to "SET" + result.runtime.settings.should.have.property("uiPort", "SET"); //should be sanitised to "SET" + result.runtime.settings.should.have.property("userDir", "SET"); //should be sanitised to "SET" + result.runtime.settings.should.have.property('contextStorage').type("object"); + + //result.runtime.settings.contextStorage.xxxxx + const contextCount = Object.keys(result.runtime.settings.contextStorage).length; + contextCount.should.eql(3);//ensure no more than the 3 settings listed below are present in the contextStorage object + result.runtime.settings.contextStorage.should.have.property('default', {module:"memory"}); + result.runtime.settings.contextStorage.should.have.property('file', {module:"localfilesystem"}); + result.runtime.settings.contextStorage.should.have.property('secured', {module:"secure_store"}); //only module should be present, other fields are dropped for security + + }) + }) + + }); + + +}); From edcdc6c97cb1f9b8e799411940f70a06f87a9526 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 25 Apr 2022 20:56:03 +0100 Subject: [PATCH 10/16] Update packages/node_modules/node-red/lib/red.js --- packages/node_modules/node-red/lib/red.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/node-red/lib/red.js b/packages/node_modules/node-red/lib/red.js index 4dc5f1281..ddc140cc4 100644 --- a/packages/node_modules/node-red/lib/red.js +++ b/packages/node_modules/node-red/lib/red.js @@ -236,5 +236,5 @@ module.exports = { * @see @node-red/editor-api_auth * @memberof node-red */ - get diagnostics() { debugger; return api.diagnostics } + get diagnostics() { return api.diagnostics } }; From 0fec9c7c5562e75b4796d9d984f98c041dacccdb Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 26 Apr 2022 16:24:13 +0100 Subject: [PATCH 11/16] Apply i18n messages to diagnostics dialog --- .../@node-red/editor-client/src/js/ui/diagnostics.js | 6 +++--- .../@node-red/editor-client/src/js/ui/editors/json.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/diagnostics.js b/packages/node_modules/@node-red/editor-client/src/js/ui/diagnostics.js index 89bd093c4..a6cf16a96 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/diagnostics.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/diagnostics.js @@ -27,14 +27,14 @@ RED.diagnostics = (function () { readOnly: true, jsonButtons: [ { - text: 'Copy', + text: RED._('clipboard.export.copy'), icon: 'fa fa-copy', click: function () { - RED.clipboard.copyText(json, $(this), "Copied to clipboard") + RED.clipboard.copyText(json, $(this), RED._('clipboard.copyMessageValue')) } }, { - text: 'Download', + text: RED._('clipboard.download'), icon: 'fa fa-download', click: function () { var element = document.createElement('a'); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js index 7687abee5..a2738f5eb 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js @@ -515,7 +515,7 @@ .on("click", function (evt) { evt.preventDefault(); if (button.click !== undefined) { - button.click(evt); + button.click.call(element, evt); } }); if (button.id) { element.attr("id", button.id); } From 367f9b623228bb88480dfbe67be362f5a0c89ee3 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Wed, 27 Apr 2022 12:05:20 +0100 Subject: [PATCH 12/16] i18n --- .../@node-red/editor-client/locales/en-US/editor.json | 3 +++ .../@node-red/editor-client/locales/ja/editor.json | 3 ++- .../@node-red/editor-client/src/js/ui/diagnostics.js | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index 93e847066..043301146 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -1154,6 +1154,9 @@ "start": "Start", "next": "Next" }, + "diagnostics": { + "title": "System Info" + }, "languages" : { "de": "German", "en-US": "English", diff --git a/packages/node_modules/@node-red/editor-client/locales/ja/editor.json b/packages/node_modules/@node-red/editor-client/locales/ja/editor.json index c45162246..ad5181cca 100644 --- a/packages/node_modules/@node-red/editor-client/locales/ja/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/ja/editor.json @@ -1287,6 +1287,7 @@ "zoom-in": "ズームイン", "zoom-out": "ズームアウト", "zoom-reset": "ズームリセット", - "toggle-navigator": "ナビゲータ表示切替" + "toggle-navigator": "ナビゲータ表示切替", + "show-system-info": "システムインフォメーション" } } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/diagnostics.js b/packages/node_modules/@node-red/editor-client/src/js/ui/diagnostics.js index a6cf16a96..5fc2f4c3e 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/diagnostics.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/diagnostics.js @@ -21,7 +21,7 @@ RED.diagnostics = (function () { json = "{\n\n}"; } RED.editor.editJSON({ - title: "System Info", //RED._('sidebar.project.editDependencies'), + title: RED._('diagnostics.title'), value: json, requireValid: true, readOnly: true, From 4054d0eca7e1aaac203b29a886010170bbf0f551 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Wed, 27 Apr 2022 12:05:51 +0100 Subject: [PATCH 13/16] adjust settings comments to more like existing std --- packages/node_modules/node-red/settings.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js index 7136c8644..7afa6070f 100644 --- a/packages/node_modules/node-red/settings.js +++ b/packages/node_modules/node-red/settings.js @@ -237,16 +237,16 @@ module.exports = { */ // lang: "de", - /** ### Configure diagnostics options - * - `.enabled`: When `enabled` is `true` (or unset), diagnostics data will + /** Configure diagnostics options + * - enabled: When `enabled` is `true` (or unset), diagnostics data will * be available at http://localhost:1880/diagnostics - * - `.ui`: When `ui` is `true` (or unset), the action `show-system-info` will + * - ui: When `ui` is `true` (or unset), the action `show-system-info` will * be available to logged in users of node-red editor */ diagnostics: { - /** @type {boolean} `enabled` - enable or disable diagnostics endpoint. Must be set to `false` to disable */ + /** enable or disable diagnostics endpoint. Must be set to `false` to disable */ enabled: true, - /** @type {boolean} `ui` - enable or disable diagnostics display in the node-red editor. Must be set to `false` to disable */ + /** enable or disable diagnostics display in the node-red editor. Must be set to `false` to disable */ ui: true, }, From b23fea9cb540705676faf6d2db10423b4f631491 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Wed, 27 Apr 2022 12:50:45 +0100 Subject: [PATCH 14/16] group json editor toolbar buttons --- .../@node-red/editor-client/src/js/ui/editors/json.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js index a2738f5eb..281fad400 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js @@ -21,7 +21,9 @@ '
    '+ '
    '+ '
    '+ - ''+ + ''+ + ''+ + ''+ '
    '+ '
    '+ '
    '+ From fcdf252f03b0f734069b60d505bf60814ad0c5e3 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Wed, 27 Apr 2022 12:52:01 +0100 Subject: [PATCH 15/16] rename jsonButtons to toolbarButtons --- .../@node-red/editor-client/src/js/ui/diagnostics.js | 2 +- .../@node-red/editor-client/src/js/ui/editors/json.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/diagnostics.js b/packages/node_modules/@node-red/editor-client/src/js/ui/diagnostics.js index 5fc2f4c3e..4e4a2d657 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/diagnostics.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/diagnostics.js @@ -25,7 +25,7 @@ RED.diagnostics = (function () { value: json, requireValid: true, readOnly: true, - jsonButtons: [ + toolbarButtons: [ { text: RED._('clipboard.export.copy'), icon: 'fa fa-copy', diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js index 281fad400..ad80f88b9 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js @@ -509,9 +509,9 @@ open: function(tray) { var trayBody = tray.find('.red-ui-tray-body'); var dialogForm = RED.editor.buildEditForm(tray.find('.red-ui-tray-body'),'dialog-form',type,'editor'); - var jsonButtons = options.jsonButtons || []; - if (jsonButtons.length) { - jsonButtons.forEach(function (button) { + var toolbarButtons = options.toolbarButtons || []; + if (toolbarButtons.length) { + toolbarButtons.forEach(function (button) { var element = $('') .insertBefore("#node-input-json-reformat") .on("click", function (evt) { From 3e16cc4912f974240d425caff08c33ce71df5c4c Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Wed, 27 Apr 2022 12:52:41 +0100 Subject: [PATCH 16/16] Add i18n for json editor title when readonly --- .../@node-red/editor-client/locales/en-US/editor.json | 2 ++ .../@node-red/editor-client/locales/ja/editor.json | 2 ++ .../@node-red/editor-client/src/js/ui/editors/json.js | 6 ++---- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index 5ea4f2a23..8551d39a6 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -940,6 +940,8 @@ "format": "format JSON", "rawMode": "Edit JSON", "uiMode": "Visual editor", + "rawMode-readonly": "JSON", + "uiMode-readonly": "Visual", "insertAbove": "Insert above", "insertBelow": "Insert below", "addItem": "Add item", diff --git a/packages/node_modules/@node-red/editor-client/locales/ja/editor.json b/packages/node_modules/@node-red/editor-client/locales/ja/editor.json index 366a71244..a6326b635 100644 --- a/packages/node_modules/@node-red/editor-client/locales/ja/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/ja/editor.json @@ -940,6 +940,8 @@ "format": "JSONフォーマット", "rawMode": "JSONを編集", "uiMode": "ビジュアルエディタ", + "rawMode-readonly": "JSON", + "uiMode-readonly": "ビジュアル", "insertAbove": "上に挿入", "insertBelow": "下に挿入", "addItem": "要素を追加", diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js index ad80f88b9..c2b7c4905 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js @@ -618,17 +618,15 @@ tabs.addTab({ id: 'json-raw', - label: options.readOnly ? "JSON" : RED._('jsonEditor.rawMode'), + label: options.readOnly ? RED._('jsonEditor.rawMode-readonly') : RED._('jsonEditor.rawMode'), content: $("#red-ui-editor-type-json-tab-raw") }); tabs.addTab({ id: 'json-ui', - label: options.readOnly ? "Visual" : RED._('jsonEditor.uiMode'), + label: options.readOnly ? RED._('jsonEditor.uiMode-readonly') : RED._('jsonEditor.uiMode'), content: $("#red-ui-editor-type-json-tab-ui") }); finishedBuild = true; - - }, close: function() { if (options.onclose) {