update dashboardAPI -AGENT
This commit is contained in:
103
src/nodeClass.js
Normal file
103
src/nodeClass.js
Normal 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
195
src/specificClass.js
Normal 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;
|
||||
Reference in New Issue
Block a user