Compare commits
3 Commits
89d2260351
...
869ba4fca5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
869ba4fca5 | ||
|
|
66b91883ac | ||
|
|
c5272fcc24 |
23
CLAUDE.md
Normal file
23
CLAUDE.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# dashboardAPI — Claude Code context
|
||||||
|
|
||||||
|
InfluxDB telemetry and FlowFuse chart endpoints.
|
||||||
|
Part of the [EVOLV](https://gitea.wbd-rd.nl/RnD/EVOLV) wastewater-automation platform.
|
||||||
|
|
||||||
|
## S88 classification
|
||||||
|
|
||||||
|
| Level | Colour | Placement lane |
|
||||||
|
|---|---|---|
|
||||||
|
| **Utility (no S88 level)** | `none` | n/a |
|
||||||
|
|
||||||
|
## Flow layout rules
|
||||||
|
|
||||||
|
When wiring this node into a multi-node demo or production flow, follow the
|
||||||
|
placement rule set in the **EVOLV superproject**:
|
||||||
|
|
||||||
|
> `.claude/rules/node-red-flow-layout.md` (in the EVOLV repo root)
|
||||||
|
|
||||||
|
Key points for this node:
|
||||||
|
- Place on lane **n/a** (x-position per the lane table in the rule).
|
||||||
|
- Stack same-level siblings vertically.
|
||||||
|
- Parent/children sit on adjacent lanes (children one lane left, parent one lane right).
|
||||||
|
- Wrap in a Node-RED group box coloured `none` (Utility (no S88 level)).
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
const { outputUtils } = require('generalFunctions');
|
const { configManager } = require('generalFunctions');
|
||||||
const Specific = require('./specificClass');
|
const DashboardApi = require('./specificClass');
|
||||||
|
|
||||||
/**
|
|
||||||
* Node-RED wrapper for dashboard generation requests.
|
|
||||||
* It listens for `registerChild` messages and emits Grafana upsert requests.
|
|
||||||
*/
|
|
||||||
class nodeClass {
|
class nodeClass {
|
||||||
constructor(uiConfig, RED, nodeInstance, nameOfNode) {
|
constructor(uiConfig, RED, nodeInstance, nameOfNode) {
|
||||||
this.node = nodeInstance;
|
this.node = nodeInstance;
|
||||||
@@ -20,13 +16,11 @@ class nodeClass {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_loadConfig(uiConfig) {
|
_loadConfig(uiConfig) {
|
||||||
this.config = {
|
const cfgMgr = new configManager();
|
||||||
general: {
|
this.config = cfgMgr.buildConfig(this.name, uiConfig, this.node.id, {
|
||||||
name: uiConfig.name || this.name,
|
functionality: {
|
||||||
logging: {
|
softwareType: this.name.toLowerCase(),
|
||||||
enabled: uiConfig.enableLog,
|
role: 'auto ui generator',
|
||||||
logLevel: uiConfig.logLevel || 'info',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
grafanaConnector: {
|
grafanaConnector: {
|
||||||
protocol: uiConfig.protocol || 'http',
|
protocol: uiConfig.protocol || 'http',
|
||||||
@@ -35,16 +29,44 @@ class nodeClass {
|
|||||||
bearerToken: uiConfig.bearerToken || '',
|
bearerToken: uiConfig.bearerToken || '',
|
||||||
},
|
},
|
||||||
defaultBucket: uiConfig.defaultBucket || process.env.INFLUXDB_BUCKET || '',
|
defaultBucket: uiConfig.defaultBucket || process.env.INFLUXDB_BUCKET || '',
|
||||||
};
|
});
|
||||||
|
|
||||||
this._output = new outputUtils();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_setupSpecificClass() {
|
_setupSpecificClass() {
|
||||||
this.source = new Specific(this.config);
|
this.source = new DashboardApi(this.config);
|
||||||
this.node.source = this.source;
|
this.node.source = this.source;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_resolveChildNode(childId) {
|
||||||
|
const runtimeNode = this.RED.nodes.getNode(childId);
|
||||||
|
if (runtimeNode?.source?.config) {
|
||||||
|
return runtimeNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const flowNode = this.node._flow?.getNode?.(childId);
|
||||||
|
if (flowNode?.source?.config) {
|
||||||
|
return flowNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return runtimeNode || flowNode || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_resolveChildSource(payload) {
|
||||||
|
if (payload?.source?.config) {
|
||||||
|
return payload.source;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload?.config) {
|
||||||
|
return { config: payload.config };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof payload === 'string') {
|
||||||
|
return this._resolveChildNode(payload)?.source || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
_attachInputHandler() {
|
_attachInputHandler() {
|
||||||
this.node.on('input', async (msg, send, done) => {
|
this.node.on('input', async (msg, send, done) => {
|
||||||
try {
|
try {
|
||||||
@@ -53,16 +75,11 @@ class nodeClass {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const childId = msg.payload;
|
const childSource = this._resolveChildSource(msg.payload);
|
||||||
const childObj = this.RED.nodes.getNode(childId);
|
|
||||||
const childSource = childObj?.source;
|
|
||||||
if (!childSource?.config) {
|
if (!childSource?.config) {
|
||||||
this.node.warn(`registerChild skipped: missing child source/config for id=${childId}`);
|
throw new Error('Missing or invalid child node');
|
||||||
if (typeof done === 'function') done();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate one dashboard for the root source and optionally its registered children.
|
|
||||||
const dashboards = this.source.generateDashboardsForGraph(childSource, {
|
const dashboards = this.source.generateDashboardsForGraph(childSource, {
|
||||||
includeChildren: Boolean(msg.includeChildren ?? true),
|
includeChildren: Boolean(msg.includeChildren ?? true),
|
||||||
});
|
});
|
||||||
@@ -72,19 +89,23 @@ class nodeClass {
|
|||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.config.grafanaConnector.bearerToken) {
|
if (this.config.grafanaConnector.bearerToken) {
|
||||||
headers.Authorization = `Bearer ${this.config.grafanaConnector.bearerToken}`;
|
headers.Authorization = `Bearer ${this.config.grafanaConnector.bearerToken}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const dash of dashboards) {
|
for (const dash of dashboards) {
|
||||||
// Forward dashboard definitions to an HTTP request node configured for Grafana API.
|
|
||||||
const payload = this.source.buildUpsertRequest({ dashboard: dash.dashboard, folderId: 0, overwrite: true });
|
|
||||||
send({
|
send({
|
||||||
topic: 'grafana.dashboard.upsert',
|
...msg,
|
||||||
|
topic: 'create',
|
||||||
url,
|
url,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers,
|
headers,
|
||||||
payload,
|
payload: this.source.buildUpsertRequest({
|
||||||
|
dashboard: dash.dashboard,
|
||||||
|
folderId: 0,
|
||||||
|
overwrite: true,
|
||||||
|
}),
|
||||||
meta: {
|
meta: {
|
||||||
nodeId: dash.nodeId,
|
nodeId: dash.nodeId,
|
||||||
softwareType: dash.softwareType,
|
softwareType: dash.softwareType,
|
||||||
@@ -98,7 +119,7 @@ class nodeClass {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.node.status({ fill: 'red', shape: 'ring', text: 'dashboardapi error' });
|
this.node.status({ fill: 'red', shape: 'ring', text: 'dashboardapi error' });
|
||||||
this.node.error(error?.message || error, msg);
|
this.node.error(error?.message || error, msg);
|
||||||
if (typeof done === 'function') done();
|
if (typeof done === 'function') done(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
const test = require('node:test');
|
describe('dashboardAPI basic structure', () => {
|
||||||
const assert = require('node:assert/strict');
|
it('module load smoke', () => {
|
||||||
|
expect(() => {
|
||||||
test('dashboardAPI module load smoke', () => {
|
|
||||||
assert.doesNotThrow(() => {
|
|
||||||
require('../../dashboardapi.js');
|
require('../../dashboardapi.js');
|
||||||
|
}).not.toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
const test = require('node:test');
|
|
||||||
const assert = require('node:assert/strict');
|
|
||||||
|
|
||||||
const DashboardApi = require('../src/specificClass');
|
const DashboardApi = require('../src/specificClass');
|
||||||
|
|
||||||
function makeNodeSource({ id, name, softwareType, positionVsParent, children = [] }) {
|
function makeNodeSource({ id, name, softwareType, positionVsParent, children = [] }) {
|
||||||
@@ -24,7 +21,8 @@ function makeNodeSource({ id, name, softwareType, positionVsParent, children = [
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
test('buildDashboard sets id=null, stable uid, title, measurement and bucket vars', () => {
|
describe('DashboardApi specificClass', () => {
|
||||||
|
it('buildDashboard sets id=null, stable uid, title, measurement and bucket vars', () => {
|
||||||
const api = new DashboardApi({
|
const api = new DashboardApi({
|
||||||
general: { name: 'dashboardapi-test', logging: { enabled: false, logLevel: 'error' } },
|
general: { name: 'dashboardapi-test', logging: { enabled: false, logLevel: 'error' } },
|
||||||
grafanaConnector: { protocol: 'http', host: 'localhost', port: 3000, bearerToken: '' },
|
grafanaConnector: { protocol: 'http', host: 'localhost', port: 3000, bearerToken: '' },
|
||||||
@@ -39,20 +37,20 @@ test('buildDashboard sets id=null, stable uid, title, measurement and bucket var
|
|||||||
|
|
||||||
const dash = api.buildDashboard({ nodeConfig: nodeSource.config, positionVsParent: 'downstream' });
|
const dash = api.buildDashboard({ nodeConfig: nodeSource.config, positionVsParent: 'downstream' });
|
||||||
|
|
||||||
assert.equal(dash.dashboard.id, null);
|
expect(dash.dashboard.id).toBeNull();
|
||||||
assert.equal(dash.uid.length, 12);
|
expect(dash.uid).toHaveLength(12);
|
||||||
assert.equal(dash.dashboard.uid, dash.uid);
|
expect(dash.dashboard.uid).toBe(dash.uid);
|
||||||
assert.equal(dash.dashboard.title, 'PT-1');
|
expect(dash.dashboard.title).toBe('PT-1');
|
||||||
|
|
||||||
const templ = dash.dashboard.templating.list;
|
const templ = dash.dashboard.templating.list;
|
||||||
const measurement = templ.find((v) => v.name === 'measurement');
|
const measurement = templ.find((v) => v.name === 'measurement');
|
||||||
const bucket = templ.find((v) => v.name === 'bucket');
|
const bucket = templ.find((v) => v.name === 'bucket');
|
||||||
|
|
||||||
assert.equal(measurement.current.value, 'measurement_m-1');
|
expect(measurement.current.value).toBe('measurement_m-1');
|
||||||
assert.equal(bucket.current.value, 'lvl3');
|
expect(bucket.current.value).toBe('lvl3');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('generateDashboardsForGraph returns root + direct child dashboards and adds links', () => {
|
it('generateDashboardsForGraph returns root + direct child dashboards and adds links', () => {
|
||||||
const api = new DashboardApi({
|
const api = new DashboardApi({
|
||||||
general: { name: 'dashboardapi-test', logging: { enabled: false, logLevel: 'error' } },
|
general: { name: 'dashboardapi-test', logging: { enabled: false, logLevel: 'error' } },
|
||||||
grafanaConnector: { protocol: 'http', host: 'localhost', port: 3000, bearerToken: '' },
|
grafanaConnector: { protocol: 'http', host: 'localhost', port: 3000, bearerToken: '' },
|
||||||
@@ -74,9 +72,10 @@ test('generateDashboardsForGraph returns root + direct child dashboards and adds
|
|||||||
});
|
});
|
||||||
|
|
||||||
const results = api.generateDashboardsForGraph(root, { includeChildren: true });
|
const results = api.generateDashboardsForGraph(root, { includeChildren: true });
|
||||||
assert.equal(results.length, 2);
|
expect(results).toHaveLength(2);
|
||||||
|
|
||||||
const rootDash = results[0];
|
const rootDash = results[0];
|
||||||
assert.ok(Array.isArray(rootDash.dashboard.links));
|
expect(Array.isArray(rootDash.dashboard.links)).toBe(true);
|
||||||
assert.ok(rootDash.dashboard.links.some((l) => l.url && l.url.includes('/d/')));
|
expect(rootDash.dashboard.links.some((l) => l.url && l.url.includes('/d/'))).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
const test = require('node:test');
|
|
||||||
const assert = require('node:assert/strict');
|
|
||||||
const fs = require('node:fs');
|
const fs = require('node:fs');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
|
|
||||||
const flow = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../../examples/basic.flow.json'), 'utf8'));
|
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;
|
const count = flow.filter((n) => n && n.type === 'dashboardapi').length;
|
||||||
assert.equal(count >= 1, true);
|
expect(count).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
const test = require('node:test');
|
|
||||||
const assert = require('node:assert/strict');
|
|
||||||
const fs = require('node:fs');
|
const fs = require('node:fs');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
|
|
||||||
@@ -9,15 +7,17 @@ function loadJson(file) {
|
|||||||
return JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
|
return JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
|
||||||
}
|
}
|
||||||
|
|
||||||
test('examples package exists for dashboardAPI', () => {
|
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']) {
|
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');
|
expect(fs.existsSync(path.join(dir, file))).toBe(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('example flows are parseable arrays for dashboardAPI', () => {
|
it('example flows are parseable arrays for dashboardAPI', () => {
|
||||||
for (const file of ['basic.flow.json', 'integration.flow.json', 'edge.flow.json']) {
|
for (const file of ['basic.flow.json', 'integration.flow.json', 'edge.flow.json']) {
|
||||||
const parsed = loadJson(file);
|
const parsed = loadJson(file);
|
||||||
assert.equal(Array.isArray(parsed), true);
|
expect(Array.isArray(parsed)).toBe(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|||||||
179
test/nodeClass.test.js
Normal file
179
test/nodeClass.test.js
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
const NodeClass = require('../src/nodeClass');
|
||||||
|
|
||||||
|
jest.mock('../src/specificClass', () => {
|
||||||
|
return jest.fn().mockImplementation(() => ({
|
||||||
|
logger: {
|
||||||
|
warn: jest.fn(),
|
||||||
|
},
|
||||||
|
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'),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const SpecificClass = require('../src/specificClass');
|
||||||
|
|
||||||
|
function createNodeHarness(flowNode = null) {
|
||||||
|
const handlers = {};
|
||||||
|
const node = {
|
||||||
|
id: 'dashboard-node-id',
|
||||||
|
on: jest.fn((event, handler) => {
|
||||||
|
handlers[event] = handler;
|
||||||
|
}),
|
||||||
|
status: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
_flow: {
|
||||||
|
getNode: jest.fn(() => flowNode),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return { node, handlers };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('dashboardAPI nodeClass', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
delete process.env.GRAFANA_TOKEN;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses RED.nodes.getNode when it returns a runtime child', async () => {
|
||||||
|
const childNode = {
|
||||||
|
source: {
|
||||||
|
config: {
|
||||||
|
general: { name: 'child' },
|
||||||
|
functionality: { softwareType: 'measurement' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const { node, handlers } = createNodeHarness();
|
||||||
|
const RED = {
|
||||||
|
nodes: {
|
||||||
|
getNode: jest.fn(() => childNode),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
new NodeClass({ name: 'E2E-DashboardAPI', host: 'grafana', port: 3000 }, RED, node, 'dashboardapi');
|
||||||
|
|
||||||
|
expect(SpecificClass).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
general: expect.objectContaining({
|
||||||
|
name: 'E2E-DashboardAPI',
|
||||||
|
id: 'dashboard-node-id',
|
||||||
|
}),
|
||||||
|
functionality: expect.objectContaining({
|
||||||
|
softwareType: 'dashboardapi',
|
||||||
|
role: 'auto ui generator',
|
||||||
|
}),
|
||||||
|
grafanaConnector: expect.objectContaining({
|
||||||
|
host: 'grafana',
|
||||||
|
port: 3000,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const send = jest.fn();
|
||||||
|
const done = jest.fn();
|
||||||
|
await handlers.input({ topic: 'registerChild', payload: 'measurement-e2e-node' }, send, done);
|
||||||
|
|
||||||
|
expect(RED.nodes.getNode).toHaveBeenCalledWith('measurement-e2e-node');
|
||||||
|
expect(send).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
topic: 'create',
|
||||||
|
url: 'http://grafana:3000/api/dashboards/db',
|
||||||
|
method: 'POST',
|
||||||
|
payload: {
|
||||||
|
dashboard: { title: 'ok' },
|
||||||
|
folderId: 0,
|
||||||
|
overwrite: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(done).toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to the active flow when RED.nodes.getNode lacks source state', async () => {
|
||||||
|
const flowChildNode = {
|
||||||
|
source: {
|
||||||
|
config: {
|
||||||
|
general: { name: 'child' },
|
||||||
|
functionality: { softwareType: 'measurement' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const { node, handlers } = createNodeHarness(flowChildNode);
|
||||||
|
const RED = {
|
||||||
|
nodes: {
|
||||||
|
getNode: jest.fn(() => ({ id: 'measurement-e2e-node' })),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
new NodeClass({ name: 'E2E-DashboardAPI', host: 'grafana', port: 3000 }, RED, node, 'dashboardapi');
|
||||||
|
|
||||||
|
const send = jest.fn();
|
||||||
|
const done = jest.fn();
|
||||||
|
await handlers.input({ topic: 'registerChild', payload: 'measurement-e2e-node' }, send, done);
|
||||||
|
|
||||||
|
expect(node._flow.getNode).toHaveBeenCalledWith('measurement-e2e-node');
|
||||||
|
expect(send).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
topic: 'create',
|
||||||
|
payload: {
|
||||||
|
dashboard: { title: 'ok' },
|
||||||
|
folderId: 0,
|
||||||
|
overwrite: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(done).toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts a child config payload directly', async () => {
|
||||||
|
const { node, handlers } = createNodeHarness();
|
||||||
|
const RED = {
|
||||||
|
nodes: {
|
||||||
|
getNode: jest.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
new NodeClass({ name: 'E2E-DashboardAPI', host: 'grafana', port: 3000 }, RED, node, 'dashboardapi');
|
||||||
|
|
||||||
|
const send = jest.fn();
|
||||||
|
const done = jest.fn();
|
||||||
|
await handlers.input(
|
||||||
|
{
|
||||||
|
topic: 'registerChild',
|
||||||
|
payload: {
|
||||||
|
config: {
|
||||||
|
general: { name: 'E2E-Level-Sensor' },
|
||||||
|
functionality: { softwareType: 'measurement' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
send,
|
||||||
|
done,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(RED.nodes.getNode).not.toHaveBeenCalled();
|
||||||
|
expect(send).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
topic: 'create',
|
||||||
|
payload: {
|
||||||
|
dashboard: { title: 'ok' },
|
||||||
|
folderId: 0,
|
||||||
|
overwrite: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(done).toHaveBeenCalledWith();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user