update dashboardAPI -AGENT

This commit is contained in:
znetsixe
2026-01-13 14:29:43 +01:00
parent c99a93f73b
commit 1ea4788848
16 changed files with 1202 additions and 8393 deletions

103
src/nodeClass.js Normal file
View File

@@ -0,0 +1,103 @@
const { outputUtils } = require('generalFunctions');
const Specific = require('./specificClass');
class nodeClass {
constructor(uiConfig, RED, nodeInstance, nameOfNode) {
this.node = nodeInstance;
this.RED = RED;
this.name = nameOfNode;
this.source = null;
this.config = null;
this._loadConfig(uiConfig);
this._setupSpecificClass();
this._attachInputHandler();
this._attachCloseHandler();
}
_loadConfig(uiConfig) {
this.config = {
general: {
name: uiConfig.name || this.name,
logging: {
enabled: uiConfig.enableLog,
logLevel: uiConfig.logLevel || 'info',
},
},
grafanaConnector: {
protocol: uiConfig.protocol || 'http',
host: uiConfig.host || 'localhost',
port: Number(uiConfig.port || 3000),
bearerToken: uiConfig.bearerToken || '',
},
};
this._output = new outputUtils();
}
_setupSpecificClass() {
this.source = new Specific(this.config);
this.node.source = this.source;
}
_attachInputHandler() {
this.node.on('input', async (msg, send, done) => {
try {
if (msg.topic !== 'registerChild') {
done();
return;
}
const childId = msg.payload;
const childObj = this.RED.nodes.getNode(childId);
const childSource = childObj?.source;
if (!childSource?.config) {
throw new Error(`Missing child source/config for id=${childId}`);
}
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) {
const payload = this.source.buildUpsertRequest({ dashboard: dash.dashboard, folderId: 0, overwrite: true });
send({
topic: 'grafana.dashboard.upsert',
url,
method: 'POST',
headers,
payload,
meta: {
nodeId: dash.nodeId,
softwareType: dash.softwareType,
uid: dash.uid,
title: dash.title,
},
});
}
done();
} catch (error) {
this.node.status({ fill: 'red', shape: 'ring', text: 'dashboardapi error' });
this.node.error(error?.message || error, msg);
done(error);
}
});
}
_attachCloseHandler() {
this.node.on('close', (done) => done());
}
}
module.exports = nodeClass;

195
src/specificClass.js Normal file
View File

@@ -0,0 +1,195 @@
const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');
const { logger } = require('generalFunctions');
function stableUid(input) {
const digest = crypto.createHash('sha1').update(String(input)).digest('hex');
return digest.slice(0, 12);
}
function slugify(input) {
return String(input || '')
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '')
.slice(0, 60);
}
function defaultBucketForPosition(positionVsParent) {
const pos = String(positionVsParent || '').toLowerCase();
if (pos === 'upstream') return 'lvl1';
if (pos === 'downstream') return 'lvl3';
return 'lvl2';
}
function updateTemplatingVar(dashboard, varName, value) {
const list = dashboard?.templating?.list;
if (!Array.isArray(list)) return;
const variable = list.find((v) => v && v.name === varName);
if (!variable) return;
variable.current = variable.current || {};
variable.current.text = value;
variable.current.value = value;
if (Array.isArray(variable.options) && variable.options.length > 0) {
variable.options[0] = variable.options[0] || {};
variable.options[0].text = value;
variable.options[0].value = value;
}
variable.query = value;
}
class DashboardApi {
constructor(config = {}) {
this.config = {
general: {
name: config?.general?.name || 'dashboardapi',
logging: {
enabled: Boolean(config?.general?.logging?.enabled),
logLevel: config?.general?.logging?.logLevel || 'info',
},
},
grafanaConnector: {
protocol: config?.grafanaConnector?.protocol || 'http',
host: config?.grafanaConnector?.host || 'localhost',
port: Number(config?.grafanaConnector?.port || 3000),
bearerToken: config?.grafanaConnector?.bearerToken || '',
},
bucketMap: config?.bucketMap || {},
};
this.logger = new logger(
this.config.general.logging.enabled,
this.config.general.logging.logLevel,
this.config.general.name
);
}
_templatesDir() {
return path.join(__dirname, '..', 'config');
}
_templateFileForSoftwareType(softwareType) {
const st = String(softwareType || '').trim();
const candidates = [
`${st}.json`,
`${st.toLowerCase()}.json`,
st === 'machineGroupControl' ? 'machineGroup.json' : null,
].filter(Boolean);
for (const filename of candidates) {
const fullPath = path.join(this._templatesDir(), filename);
if (fs.existsSync(fullPath)) return fullPath;
}
throw new Error(`No dashboard template found for softwareType=${st}`);
}
loadTemplate(softwareType) {
const templatePath = this._templateFileForSoftwareType(softwareType);
const raw = fs.readFileSync(templatePath, 'utf8');
return JSON.parse(raw);
}
grafanaUpsertUrl() {
const { protocol, host, port } = this.config.grafanaConnector;
return `${protocol}://${host}:${port}/api/dashboards/db`;
}
buildDashboard({ nodeConfig, positionVsParent }) {
const softwareType =
nodeConfig?.functionality?.softwareType ||
nodeConfig?.functionality?.software_type ||
'measurement';
const nodeId = nodeConfig?.general?.id || nodeConfig?.general?.name || softwareType;
const measurementName = `${softwareType}_${nodeId}`;
const title = nodeConfig?.general?.name || String(nodeId);
const dashboard = this.loadTemplate(softwareType);
const uid = stableUid(`${softwareType}:${nodeId}`);
dashboard.id = null;
dashboard.uid = uid;
dashboard.title = title;
dashboard.tags = Array.from(
new Set([...(dashboard.tags || []), 'EVOLV', softwareType, String(positionVsParent || '')].filter(Boolean))
);
const bucket =
this.config.bucketMap[String(positionVsParent)] || defaultBucketForPosition(positionVsParent);
updateTemplatingVar(dashboard, 'measurement', measurementName);
updateTemplatingVar(dashboard, 'bucket', bucket);
return { dashboard, uid, title, softwareType, nodeId, measurementName };
}
buildUpsertRequest({ dashboard, folderId = 0, overwrite = true }) {
return { dashboard, folderId, overwrite };
}
extractChildren(nodeSource) {
const out = [];
const reg = nodeSource?.childRegistrationUtils?.registeredChildren;
if (reg && typeof reg.values === 'function') {
for (const entry of reg.values()) {
const child = entry?.child;
if (!child?.config) continue;
out.push({ childSource: child, positionVsParent: entry?.position || child.positionVsParent });
}
return out;
}
return out;
}
generateDashboardsForGraph(rootSource, { includeChildren = true } = {}) {
if (!rootSource?.config) {
throw new Error('generateDashboardsForGraph requires a node source with `.config`');
}
const rootPosition = rootSource?.positionVsParent || rootSource?.config?.functionality?.positionVsParent;
const rootDash = this.buildDashboard({ nodeConfig: rootSource.config, positionVsParent: rootPosition });
const results = [rootDash];
if (!includeChildren) return results;
const children = this.extractChildren(rootSource);
for (const { childSource, positionVsParent } of children) {
const childDash = this.buildDashboard({ nodeConfig: childSource.config, positionVsParent });
results.push(childDash);
}
// Add links from the root dashboard to children dashboards (when possible)
if (children.length > 0) {
rootDash.dashboard.links = Array.isArray(rootDash.dashboard.links) ? rootDash.dashboard.links : [];
for (const { childSource } of children) {
const childConfig = childSource.config;
const childSoftwareType = childConfig?.functionality?.softwareType || 'measurement';
const childNodeId = childConfig?.general?.id || childConfig?.general?.name || childSoftwareType;
const childUid = stableUid(`${childSoftwareType}:${childNodeId}`);
const childTitle = childConfig?.general?.name || String(childNodeId);
rootDash.dashboard.links.push({
type: 'link',
title: childTitle,
url: `/d/${childUid}/${slugify(childTitle)}`,
tags: [],
targetBlank: false,
keepTime: true,
keepVariables: true,
});
}
}
return results;
}
}
module.exports = DashboardApi;