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, '');
+});