This commit is contained in:
znetsixe
2026-02-23 13:16:58 +01:00
parent b285d8e83a
commit 547333be7d
2 changed files with 31 additions and 9 deletions

View File

@@ -1,6 +1,10 @@
const { outputUtils } = require('generalFunctions'); const { outputUtils } = require('generalFunctions');
const Specific = require('./specificClass'); const Specific = 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;
@@ -44,7 +48,7 @@ class nodeClass {
this.node.on('input', async (msg, send, done) => { this.node.on('input', async (msg, send, done) => {
try { try {
if (msg.topic !== 'registerChild') { if (msg.topic !== 'registerChild') {
done(); if (typeof done === 'function') done();
return; return;
} }
@@ -52,9 +56,12 @@ class nodeClass {
const childObj = this.RED.nodes.getNode(childId); const childObj = this.RED.nodes.getNode(childId);
const childSource = childObj?.source; const childSource = childObj?.source;
if (!childSource?.config) { if (!childSource?.config) {
throw new Error(`Missing child source/config for id=${childId}`); this.node.warn(`registerChild skipped: missing child source/config for id=${childId}`);
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),
}); });
@@ -69,6 +76,7 @@ class nodeClass {
} }
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 }); const payload = this.source.buildUpsertRequest({ dashboard: dash.dashboard, folderId: 0, overwrite: true });
send({ send({
topic: 'grafana.dashboard.upsert', topic: 'grafana.dashboard.upsert',
@@ -85,19 +93,20 @@ class nodeClass {
}); });
} }
done(); if (typeof done === 'function') done();
} 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);
done(error); if (typeof done === 'function') done();
} }
}); });
} }
_attachCloseHandler() { _attachCloseHandler() {
this.node.on('close', (done) => done()); this.node.on('close', (done) => {
if (typeof done === 'function') done();
});
} }
} }
module.exports = nodeClass; module.exports = nodeClass;

View File

@@ -45,6 +45,10 @@ function updateTemplatingVar(dashboard, varName, value) {
variable.query = value; variable.query = value;
} }
/**
* Dashboard domain service.
* Builds Grafana dashboard payloads from EVOLV node config and child topology.
*/
class DashboardApi { class DashboardApi {
constructor(config = {}) { constructor(config = {}) {
this.config = { this.config = {
@@ -88,11 +92,13 @@ class DashboardApi {
if (fs.existsSync(fullPath)) return fullPath; if (fs.existsSync(fullPath)) return fullPath;
} }
throw new Error(`No dashboard template found for softwareType=${st}`); this.logger.warn(`No dashboard template found for softwareType=${st}`);
return null;
} }
loadTemplate(softwareType) { loadTemplate(softwareType) {
const templatePath = this._templateFileForSoftwareType(softwareType); const templatePath = this._templateFileForSoftwareType(softwareType);
if (!templatePath) return null;
const raw = fs.readFileSync(templatePath, 'utf8'); const raw = fs.readFileSync(templatePath, 'utf8');
return JSON.parse(raw); return JSON.parse(raw);
} }
@@ -111,7 +117,12 @@ class DashboardApi {
const measurementName = `${softwareType}_${nodeId}`; const measurementName = `${softwareType}_${nodeId}`;
const title = nodeConfig?.general?.name || String(nodeId); const title = nodeConfig?.general?.name || String(nodeId);
// Missing templates are treated as non-fatal: we skip only that dashboard.
const dashboard = this.loadTemplate(softwareType); const dashboard = this.loadTemplate(softwareType);
if (!dashboard) {
this.logger.warn(`Skipping dashboard generation: no template for softwareType=${softwareType}`);
return null;
}
const uid = stableUid(`${softwareType}:${nodeId}`); const uid = stableUid(`${softwareType}:${nodeId}`);
dashboard.id = null; dashboard.id = null;
@@ -150,11 +161,13 @@ class DashboardApi {
generateDashboardsForGraph(rootSource, { includeChildren = true } = {}) { generateDashboardsForGraph(rootSource, { includeChildren = true } = {}) {
if (!rootSource?.config) { if (!rootSource?.config) {
throw new Error('generateDashboardsForGraph requires a node source with `.config`'); this.logger.warn('generateDashboardsForGraph skipped: root source missing config');
return [];
} }
const rootPosition = rootSource?.positionVsParent || rootSource?.config?.functionality?.positionVsParent; const rootPosition = rootSource?.positionVsParent || rootSource?.config?.functionality?.positionVsParent;
const rootDash = this.buildDashboard({ nodeConfig: rootSource.config, positionVsParent: rootPosition }); const rootDash = this.buildDashboard({ nodeConfig: rootSource.config, positionVsParent: rootPosition });
if (!rootDash) return [];
const results = [rootDash]; const results = [rootDash];
@@ -163,7 +176,7 @@ class DashboardApi {
const children = this.extractChildren(rootSource); const children = this.extractChildren(rootSource);
for (const { childSource, positionVsParent } of children) { for (const { childSource, positionVsParent } of children) {
const childDash = this.buildDashboard({ nodeConfig: childSource.config, positionVsParent }); const childDash = this.buildDashboard({ nodeConfig: childSource.config, positionVsParent });
results.push(childDash); if (childDash) results.push(childDash);
} }
// Add links from the root dashboard to children dashboards (when possible) // Add links from the root dashboard to children dashboards (when possible)