diff --git a/dashboardAPI.html b/dashboardAPI.html index 9cc5b4c..9c6a866 100644 --- a/dashboardAPI.html +++ b/dashboardAPI.html @@ -13,9 +13,12 @@ protocol: { value: 'http' }, host: { value: 'localhost' }, port: { value: 3000 }, - bearerToken: { value: '' }, + folderUid: { value: '' }, defaultBucket: { value: '' }, }, + credentials: { + bearerToken: { type: 'password' }, + }, inputs: 1, outputs: 1, inputLabels: ['Input'], @@ -44,11 +47,12 @@ window.EVOLV.nodes.dashboardapi.loggerMenu.saveEditor(node); } - ['name', 'protocol', 'host', 'port', 'bearerToken', 'defaultBucket'].forEach((field) => { + ['name', 'protocol', 'host', 'port', 'folderUid', 'defaultBucket'].forEach((field) => { const element = document.getElementById(`node-input-${field}`); if (!element) return; node[field] = field === 'port' ? parseInt(element.value, 10) || 3000 : element.value || ''; }); + // bearerToken is handled by Node-RED's credentials system (encrypted at rest in flow_cred.json). }, }); @@ -80,7 +84,12 @@
- + +
+ +
+ +
diff --git a/dashboardAPI.js b/dashboardAPI.js index 06d71cd..45a84d1 100644 --- a/dashboardAPI.js +++ b/dashboardAPI.js @@ -9,6 +9,10 @@ module.exports = function (RED) { RED.nodes.registerType(nameOfNode, function (config) { RED.nodes.createNode(this, config); this.nodeClass = new nodeClass(config, RED, this, nameOfNode); + }, { + credentials: { + bearerToken: { type: 'password' }, + }, }); const menuMgr = new MenuManager(); diff --git a/src/commands/handlers.js b/src/commands/handlers.js index e735997..4916ed6 100644 --- a/src/commands/handlers.js +++ b/src/commands/handlers.js @@ -48,7 +48,7 @@ function registerChild(source, msg, ctx) { headers, payload: source.buildUpsertRequest({ dashboard: dash.dashboard, - folderId: 0, + folderUid: source.config?.grafanaConnector?.folderUid || undefined, overwrite: true, }), meta: { diff --git a/src/nodeClass.js b/src/nodeClass.js index 85a3861..3c7f7d7 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -30,6 +30,18 @@ class nodeClass { _buildConfig(uiConfig) { const cfgMgr = new configManager(); + // Credentials block (Node-RED encrypts at rest in flow_cred.json). Legacy + // installs may still carry bearerToken on uiConfig — fall back with a + // one-time deprecation warning so the user knows to re-save. + const credentialToken = this.node?.credentials?.bearerToken || ''; + const legacyToken = uiConfig.bearerToken || ''; + if (!credentialToken && legacyToken) { + this.RED?.log?.warn?.( + `[${this.name}] bearer token loaded from legacy plain config field. ` + + `Re-open this node in the editor and click Done to migrate to encrypted credentials.` + ); + } + const bearerToken = credentialToken || legacyToken; return cfgMgr.buildConfig(this.name, uiConfig, this.node.id, { functionality: { softwareType: this.name.toLowerCase(), @@ -39,7 +51,8 @@ class nodeClass { protocol: uiConfig.protocol || 'http', host: uiConfig.host || 'localhost', port: Number(uiConfig.port || 3000), - bearerToken: uiConfig.bearerToken || '', + bearerToken, + folderUid: uiConfig.folderUid || '', }, defaultBucket: uiConfig.defaultBucket || process.env.INFLUXDB_BUCKET || '', }); diff --git a/src/specificClass.js b/src/specificClass.js index 7fdebfd..911a316 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -64,6 +64,7 @@ class DashboardApi { host: config?.grafanaConnector?.host || 'localhost', port: Number(config?.grafanaConnector?.port || 3000), bearerToken: config?.grafanaConnector?.bearerToken || '', + folderUid: config?.grafanaConnector?.folderUid || '', }, defaultBucket: config?.defaultBucket || '', bucketMap: config?.bucketMap || {}, @@ -144,8 +145,13 @@ class DashboardApi { return { dashboard, uid, title, softwareType, nodeId, measurementName }; } - buildUpsertRequest({ dashboard, folderId = 0, overwrite = true }) { - return { dashboard, folderId, overwrite }; + buildUpsertRequest({ dashboard, folderId, folderUid, overwrite = true }) { + const out = { dashboard, overwrite }; + // Prefer folderUid (modern Grafana API). Fall back to folderId for older callers. + const uid = folderUid ?? this.config?.grafanaConnector?.folderUid ?? ''; + if (uid) out.folderUid = uid; + else if (typeof folderId === 'number') out.folderId = folderId; + return out; } extractChildren(nodeSource) { diff --git a/test/basic/slice34-credentials-and-folder.basic.test.js b/test/basic/slice34-credentials-and-folder.basic.test.js new file mode 100644 index 0000000..df7771d --- /dev/null +++ b/test/basic/slice34-credentials-and-folder.basic.test.js @@ -0,0 +1,43 @@ +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const DashboardApi = require('../../src/specificClass.js'); + +test('buildUpsertRequest emits folderUid when configured', () => { + const api = new DashboardApi({ + grafanaConnector: { folderUid: 'rnd-folder' }, + }); + const req = api.buildUpsertRequest({ dashboard: { uid: 'x', title: 'X' } }); + assert.equal(req.folderUid, 'rnd-folder'); + assert.equal(req.overwrite, true); + assert.ok(!('folderId' in req), 'should not emit folderId when folderUid is set'); +}); + +test('buildUpsertRequest omits folderUid when empty (Grafana defaults to General)', () => { + const api = new DashboardApi({}); + const req = api.buildUpsertRequest({ dashboard: { uid: 'x' } }); + assert.equal(req.folderUid, undefined); + // folderId fallback only when explicitly passed + assert.equal(req.folderId, undefined); +}); + +test('buildUpsertRequest folderUid override at call-site wins over config', () => { + const api = new DashboardApi({ grafanaConnector: { folderUid: 'rnd-folder' } }); + const req = api.buildUpsertRequest({ dashboard: { uid: 'x' }, folderUid: 'override-folder' }); + assert.equal(req.folderUid, 'override-folder'); +}); + +test('bearerToken from config flows into specificClass config', () => { + const api = new DashboardApi({ + grafanaConnector: { bearerToken: 'tok-xyz', folderUid: '' }, + }); + assert.equal(api.config.grafanaConnector.bearerToken, 'tok-xyz'); +}); + +test('default config has empty bearerToken and folderUid', () => { + const api = new DashboardApi({}); + assert.equal(api.config.grafanaConnector.bearerToken, ''); + assert.equal(api.config.grafanaConnector.folderUid, ''); +});