diff --git a/src/nodeClass.js b/src/nodeClass.js index 6251bbe..62766f4 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -6,6 +6,8 @@ class nodeClass { this.node = nodeInstance; this.RED = RED; this.name = nameOfNode; + this.source = null; + this.config = null; this._loadConfig(uiConfig); this._setupSpecificClass(); @@ -21,11 +23,12 @@ class nodeClass { role: 'auto ui generator', }, grafanaConnector: { - host: uiConfig.host || 'localhost', - port: Number(uiConfig.port) || 3000, protocol: uiConfig.protocol || 'http', - bearerToken: uiConfig.bearerToken || null, + host: uiConfig.host || 'localhost', + port: Number(uiConfig.port || 3000), + bearerToken: uiConfig.bearerToken || '', }, + defaultBucket: uiConfig.defaultBucket || process.env.INFLUXDB_BUCKET || '', }); } @@ -48,17 +51,17 @@ class nodeClass { return runtimeNode || flowNode || null; } - _resolveChildConfig(payload) { + _resolveChildSource(payload) { if (payload?.source?.config) { - return payload.source.config; + return payload.source; } if (payload?.config) { - return payload.config; + return { config: payload.config }; } if (typeof payload === 'string') { - return this._resolveChildNode(payload)?.source?.config || null; + return this._resolveChildNode(payload)?.source || null; } return null; @@ -67,45 +70,63 @@ class nodeClass { _attachInputHandler() { this.node.on('input', async (msg, send, done) => { try { - switch (msg.topic) { - case 'registerChild': { - const childConfig = this._resolveChildConfig(msg.payload); - if (!childConfig) { - throw new Error('Missing or invalid child node'); - } - - const payload = await this.source.generateDashB(childConfig); - const authToken = process.env.GRAFANA_TOKEN || this.config.grafanaConnector.bearerToken || ''; - - send({ - ...msg, - topic: 'create', - payload, - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - Authorization: `Bearer ${authToken}`, - }, - }); - break; - } - default: - this.source.logger.warn(`Unknown topic: ${msg.topic}`); - break; + if (msg.topic !== 'registerChild') { + if (typeof done === 'function') done(); + return; } - done(); + const childSource = this._resolveChildSource(msg.payload); + if (!childSource?.config) { + throw new Error('Missing or invalid child node'); + } + + const dashboards = this.source.generateDashboardsForGraph(childSource, { + includeChildren: Boolean(msg.includeChildren ?? true), + }); + + const url = this.source.grafanaUpsertUrl(); + const headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + }; + + if (this.config.grafanaConnector.bearerToken) { + headers.Authorization = `Bearer ${this.config.grafanaConnector.bearerToken}`; + } + + for (const dash of dashboards) { + send({ + ...msg, + topic: 'create', + url, + method: 'POST', + headers, + payload: this.source.buildUpsertRequest({ + dashboard: dash.dashboard, + folderId: 0, + overwrite: true, + }), + meta: { + nodeId: dash.nodeId, + softwareType: dash.softwareType, + uid: dash.uid, + title: dash.title, + }, + }); + } + + if (typeof done === 'function') done(); } catch (error) { - this.node.status({ fill: 'red', shape: 'ring', text: 'Bad request data' }); - this.node.error(`Bad request data: ${error.message}`, msg); - done(error); + this.node.status({ fill: 'red', shape: 'ring', text: 'dashboardapi error' }); + this.node.error(error?.message || error, msg); + if (typeof done === 'function') done(error); } }); } _attachCloseHandler() { this.node.on('close', (done) => { - done(); + if (typeof done === 'function') done(); }); } } diff --git a/test/basic/structure-module-load.basic.test.js b/test/basic/structure-module-load.basic.test.js index dde1c59..766c755 100644 --- a/test/basic/structure-module-load.basic.test.js +++ b/test/basic/structure-module-load.basic.test.js @@ -1,8 +1,7 @@ -const test = require('node:test'); -const assert = require('node:assert/strict'); - -test('dashboardAPI module load smoke', () => { - assert.doesNotThrow(() => { - require('../../dashboardapi.js'); +describe('dashboardAPI basic structure', () => { + it('module load smoke', () => { + expect(() => { + require('../../dashboardapi.js'); + }).not.toThrow(); }); }); diff --git a/test/dashboardapi.test.js b/test/dashboardapi.test.js index 1ea6e0d..90ca2fd 100644 --- a/test/dashboardapi.test.js +++ b/test/dashboardapi.test.js @@ -1,6 +1,3 @@ -const test = require('node:test'); -const assert = require('node:assert/strict'); - const DashboardApi = require('../src/specificClass'); function makeNodeSource({ id, name, softwareType, positionVsParent, children = [] }) { @@ -24,59 +21,61 @@ function makeNodeSource({ id, name, softwareType, positionVsParent, children = [ }; } -test('buildDashboard sets id=null, stable uid, title, measurement and bucket vars', () => { - const api = new DashboardApi({ - general: { name: 'dashboardapi-test', logging: { enabled: false, logLevel: 'error' } }, - grafanaConnector: { protocol: 'http', host: 'localhost', port: 3000, bearerToken: '' }, +describe('DashboardApi specificClass', () => { + it('buildDashboard sets id=null, stable uid, title, measurement and bucket vars', () => { + const api = new DashboardApi({ + general: { name: 'dashboardapi-test', logging: { enabled: false, logLevel: 'error' } }, + grafanaConnector: { protocol: 'http', host: 'localhost', port: 3000, bearerToken: '' }, + }); + + const nodeSource = makeNodeSource({ + id: 'm-1', + name: 'PT-1', + softwareType: 'measurement', + positionVsParent: 'downstream', + }); + + const dash = api.buildDashboard({ nodeConfig: nodeSource.config, positionVsParent: 'downstream' }); + + expect(dash.dashboard.id).toBeNull(); + expect(dash.uid).toHaveLength(12); + expect(dash.dashboard.uid).toBe(dash.uid); + expect(dash.dashboard.title).toBe('PT-1'); + + const templ = dash.dashboard.templating.list; + const measurement = templ.find((v) => v.name === 'measurement'); + const bucket = templ.find((v) => v.name === 'bucket'); + + expect(measurement.current.value).toBe('measurement_m-1'); + expect(bucket.current.value).toBe('lvl3'); }); - const nodeSource = makeNodeSource({ - id: 'm-1', - name: 'PT-1', - softwareType: 'measurement', - positionVsParent: 'downstream', + it('generateDashboardsForGraph returns root + direct child dashboards and adds links', () => { + const api = new DashboardApi({ + general: { name: 'dashboardapi-test', logging: { enabled: false, logLevel: 'error' } }, + grafanaConnector: { protocol: 'http', host: 'localhost', port: 3000, bearerToken: '' }, + }); + + const child = makeNodeSource({ + id: 'c-1', + name: 'ChildSensor', + softwareType: 'measurement', + positionVsParent: 'upstream', + }); + + const root = makeNodeSource({ + id: 'p-1', + name: 'ParentMachine', + softwareType: 'machine', + positionVsParent: 'atEquipment', + children: [child], + }); + + const results = api.generateDashboardsForGraph(root, { includeChildren: true }); + expect(results).toHaveLength(2); + + const rootDash = results[0]; + expect(Array.isArray(rootDash.dashboard.links)).toBe(true); + expect(rootDash.dashboard.links.some((l) => l.url && l.url.includes('/d/'))).toBe(true); }); - - const dash = api.buildDashboard({ nodeConfig: nodeSource.config, positionVsParent: 'downstream' }); - - assert.equal(dash.dashboard.id, null); - assert.equal(dash.uid.length, 12); - assert.equal(dash.dashboard.uid, dash.uid); - assert.equal(dash.dashboard.title, 'PT-1'); - - const templ = dash.dashboard.templating.list; - const measurement = templ.find((v) => v.name === 'measurement'); - const bucket = templ.find((v) => v.name === 'bucket'); - - assert.equal(measurement.current.value, 'measurement_m-1'); - assert.equal(bucket.current.value, 'lvl3'); -}); - -test('generateDashboardsForGraph returns root + direct child dashboards and adds links', () => { - const api = new DashboardApi({ - general: { name: 'dashboardapi-test', logging: { enabled: false, logLevel: 'error' } }, - grafanaConnector: { protocol: 'http', host: 'localhost', port: 3000, bearerToken: '' }, - }); - - const child = makeNodeSource({ - id: 'c-1', - name: 'ChildSensor', - softwareType: 'measurement', - positionVsParent: 'upstream', - }); - - const root = makeNodeSource({ - id: 'p-1', - name: 'ParentMachine', - softwareType: 'machine', - positionVsParent: 'atEquipment', - children: [child], - }); - - const results = api.generateDashboardsForGraph(root, { includeChildren: true }); - assert.equal(results.length, 2); - - const rootDash = results[0]; - assert.ok(Array.isArray(rootDash.dashboard.links)); - assert.ok(rootDash.dashboard.links.some((l) => l.url && l.url.includes('/d/'))); }); diff --git a/test/edge/structure-examples-node-type.edge.test.js b/test/edge/structure-examples-node-type.edge.test.js index d90c297..63913d6 100644 --- a/test/edge/structure-examples-node-type.edge.test.js +++ b/test/edge/structure-examples-node-type.edge.test.js @@ -1,11 +1,11 @@ -const test = require('node:test'); -const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const flow = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../../examples/basic.flow.json'), 'utf8')); -test('basic example includes node type dashboardapi', () => { +describe('dashboardAPI edge example structure', () => { + it('basic example includes node type dashboardapi', () => { const count = flow.filter((n) => n && n.type === 'dashboardapi').length; - assert.equal(count >= 1, true); + expect(count).toBeGreaterThanOrEqual(1); + }); }); diff --git a/test/integration/structure-examples.integration.test.js b/test/integration/structure-examples.integration.test.js index 5918e79..e6594f3 100644 --- a/test/integration/structure-examples.integration.test.js +++ b/test/integration/structure-examples.integration.test.js @@ -1,5 +1,3 @@ -const test = require('node:test'); -const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); @@ -9,15 +7,17 @@ function loadJson(file) { return JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8')); } -test('examples package exists for dashboardAPI', () => { - for (const file of ['README.md', 'basic.flow.json', 'integration.flow.json', 'edge.flow.json']) { - assert.equal(fs.existsSync(path.join(dir, file)), true, file + ' missing'); - } -}); +describe('dashboardAPI integration examples', () => { + it('examples package exists for dashboardAPI', () => { + for (const file of ['README.md', 'basic.flow.json', 'integration.flow.json', 'edge.flow.json']) { + expect(fs.existsSync(path.join(dir, file))).toBe(true); + } + }); -test('example flows are parseable arrays for dashboardAPI', () => { - for (const file of ['basic.flow.json', 'integration.flow.json', 'edge.flow.json']) { - const parsed = loadJson(file); - assert.equal(Array.isArray(parsed), true); - } + it('example flows are parseable arrays for dashboardAPI', () => { + for (const file of ['basic.flow.json', 'integration.flow.json', 'edge.flow.json']) { + const parsed = loadJson(file); + expect(Array.isArray(parsed)).toBe(true); + } + }); }); diff --git a/test/nodeClass.test.js b/test/nodeClass.test.js index 5934752..7728d80 100644 --- a/test/nodeClass.test.js +++ b/test/nodeClass.test.js @@ -5,7 +5,19 @@ jest.mock('../src/specificClass', () => { logger: { warn: jest.fn(), }, - generateDashB: jest.fn().mockResolvedValue({ dashboard: { title: 'ok' } }), + generateDashboardsForGraph: jest.fn(() => [{ + dashboard: { title: 'ok' }, + nodeId: 'child-node-id', + softwareType: 'measurement', + uid: 'child-uid', + title: 'ok', + }]), + buildUpsertRequest: jest.fn(({ dashboard, folderId, overwrite }) => ({ + dashboard, + folderId, + overwrite, + })), + grafanaUpsertUrl: jest.fn(() => 'http://grafana:3000/api/dashboards/db'), })); }); @@ -77,7 +89,13 @@ describe('dashboardAPI nodeClass', () => { expect(send).toHaveBeenCalledWith( expect.objectContaining({ topic: 'create', - payload: { dashboard: { title: 'ok' } }, + url: 'http://grafana:3000/api/dashboards/db', + method: 'POST', + payload: { + dashboard: { title: 'ok' }, + folderId: 0, + overwrite: true, + }, }), ); expect(done).toHaveBeenCalledWith(); @@ -109,7 +127,11 @@ describe('dashboardAPI nodeClass', () => { expect(send).toHaveBeenCalledWith( expect.objectContaining({ topic: 'create', - payload: { dashboard: { title: 'ok' } }, + payload: { + dashboard: { title: 'ok' }, + folderId: 0, + overwrite: true, + }, }), ); expect(done).toHaveBeenCalledWith(); @@ -145,7 +167,11 @@ describe('dashboardAPI nodeClass', () => { expect(send).toHaveBeenCalledWith( expect.objectContaining({ topic: 'create', - payload: { dashboard: { title: 'ok' } }, + payload: { + dashboard: { title: 'ok' }, + folderId: 0, + overwrite: true, + }, }), ); expect(done).toHaveBeenCalledWith();