Compare commits
1 Commits
dac8576cab
...
slice/34-w
| Author | SHA1 | Date | |
|---|---|---|---|
| 7fdab73ba0 |
@@ -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).
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -80,7 +84,12 @@
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-bearerToken"><i class="fa fa-key"></i> Bearer Token</label>
|
||||
<input type="password" id="node-input-bearerToken" placeholder="optional" style="width:70%;" />
|
||||
<input type="password" id="node-input-bearerToken" placeholder="encrypted at rest" style="width:70%;" />
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-folderUid"><i class="fa fa-folder-open"></i> Grafana Folder UID</label>
|
||||
<input type="text" id="node-input-folderUid" placeholder="optional — empty = General folder" style="width:70%;" />
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 || '',
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
43
test/basic/slice34-credentials-and-folder.basic.test.js
Normal file
43
test/basic/slice34-credentials-and-folder.basic.test.js
Normal file
@@ -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, '');
|
||||
});
|
||||
Reference in New Issue
Block a user