Compare commits
7 Commits
5f1dd7675c
...
66b91883ac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66b91883ac | ||
|
|
c5272fcc24 | ||
|
|
89d2260351 | ||
|
|
547333be7d | ||
|
|
b285d8e83a | ||
|
|
1ea4788848 | ||
|
|
c99a93f73b |
71
config/dashboardapi.json
Normal file
71
config/dashboardapi.json
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": { "type": "grafana", "uid": "-- Grafana --" },
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"type": "dashboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"editable": true,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"id": null,
|
||||||
|
"links": [],
|
||||||
|
"panels": [
|
||||||
|
{ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 1, "title": "DashboardAPI", "type": "row" },
|
||||||
|
{
|
||||||
|
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
|
||||||
|
"gridPos": { "h": 9, "w": 24, "x": 0, "y": 1 },
|
||||||
|
"id": 2,
|
||||||
|
"options": { "legend": { "displayMode": "list", "placement": "bottom" } },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"query": "from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn:(r) => r._measurement==\"${measurement}\")\n |> aggregateWindow(every: v.windowPeriod, fn: count, createEmpty: false)",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Upsert Activity (if logged)",
|
||||||
|
"type": "timeseries"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"schemaVersion": 39,
|
||||||
|
"tags": ["EVOLV", "dashboardapi", "template"],
|
||||||
|
"templating": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"name": "dbase",
|
||||||
|
"type": "custom",
|
||||||
|
"label": "dbase",
|
||||||
|
"query": "cdzg44tv250jkd",
|
||||||
|
"current": { "text": "cdzg44tv250jkd", "value": "cdzg44tv250jkd", "selected": false },
|
||||||
|
"options": [{ "text": "cdzg44tv250jkd", "value": "cdzg44tv250jkd", "selected": true }],
|
||||||
|
"hide": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "measurement",
|
||||||
|
"type": "custom",
|
||||||
|
"query": "template",
|
||||||
|
"current": { "text": "template", "value": "template", "selected": false },
|
||||||
|
"options": [{ "text": "template", "value": "template", "selected": true }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bucket",
|
||||||
|
"type": "custom",
|
||||||
|
"query": "lvl2",
|
||||||
|
"current": { "text": "lvl2", "value": "lvl2", "selected": false },
|
||||||
|
"options": [{ "text": "lvl2", "value": "lvl2", "selected": true }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"time": { "from": "now-6h", "to": "now" },
|
||||||
|
"timezone": "",
|
||||||
|
"title": "template",
|
||||||
|
"uid": null,
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
|
||||||
2144
config/machine.json
2144
config/machine.json
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2667
config/monster.json
2667
config/monster.json
File diff suppressed because it is too large
Load Diff
171
config/pumpingStation.json
Normal file
171
config/pumpingStation.json
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": { "type": "grafana", "uid": "-- Grafana --" },
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"type": "dashboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"editable": true,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"id": null,
|
||||||
|
"links": [],
|
||||||
|
"panels": [
|
||||||
|
{ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 1, "title": "Status", "type": "row" },
|
||||||
|
{
|
||||||
|
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
|
||||||
|
"fieldConfig": { "defaults": { "thresholds": { "mode": "absolute", "steps": [{ "color": "blue", "value": null }] } }, "overrides": [] },
|
||||||
|
"gridPos": { "h": 4, "w": 5, "x": 0, "y": 1 },
|
||||||
|
"id": 2,
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "none" },
|
||||||
|
"targets": [
|
||||||
|
{ "query": "from(bucket: \"${bucket}\")\n |> range(start: -7d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and r._field==\"direction\")\n |> last()", "refId": "A" }
|
||||||
|
],
|
||||||
|
"title": "Direction",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "s", "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "orange", "value": 300 }, { "color": "red", "value": 600 }] } }, "overrides": [] },
|
||||||
|
"gridPos": { "h": 4, "w": 5, "x": 5, "y": 1 },
|
||||||
|
"id": 3,
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "area" },
|
||||||
|
"targets": [
|
||||||
|
{ "query": "from(bucket: \"${bucket}\")\n |> range(start: -7d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and r._field==\"timeleft\")\n |> last()", "refId": "A" }
|
||||||
|
],
|
||||||
|
"title": "Time Left",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
|
||||||
|
"fieldConfig": { "defaults": { "thresholds": { "mode": "absolute", "steps": [{ "color": "purple", "value": null }] } }, "overrides": [] },
|
||||||
|
"gridPos": { "h": 4, "w": 4, "x": 10, "y": 1 },
|
||||||
|
"id": 4,
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "none" },
|
||||||
|
"targets": [
|
||||||
|
{ "query": "from(bucket: \"${bucket}\")\n |> range(start: -7d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and r._field==\"flowSource\")\n |> last()", "refId": "A" }
|
||||||
|
],
|
||||||
|
"title": "Flow Source",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
|
||||||
|
"fieldConfig": { "defaults": { "min": 0, "max": 100, "unit": "percent", "thresholds": { "mode": "absolute", "steps": [{ "color": "red", "value": null }, { "color": "orange", "value": 20 }, { "color": "green", "value": 40 }, { "color": "orange", "value": 80 }, { "color": "red", "value": 95 }] } }, "overrides": [] },
|
||||||
|
"gridPos": { "h": 4, "w": 5, "x": 14, "y": 1 },
|
||||||
|
"id": 5,
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "showThresholdLabels": false, "showThresholdMarkers": true },
|
||||||
|
"targets": [
|
||||||
|
{ "query": "from(bucket: \"${bucket}\")\n |> range(start: -7d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and r._field =~ /^volumePercent\\.predicted\\.atequipment/)\n |> last()", "refId": "A" }
|
||||||
|
],
|
||||||
|
"title": "Fill %",
|
||||||
|
"type": "gauge"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "m", "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] } }, "overrides": [] },
|
||||||
|
"gridPos": { "h": 4, "w": 5, "x": 19, "y": 1 },
|
||||||
|
"id": 6,
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "area" },
|
||||||
|
"targets": [
|
||||||
|
{ "query": "from(bucket: \"${bucket}\")\n |> range(start: -7d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and r._field =~ /^level\\.predicted\\.atequipment/)\n |> last()", "refId": "A" }
|
||||||
|
],
|
||||||
|
"title": "Level",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 }, "id": 7, "title": "Basin", "type": "row" },
|
||||||
|
{
|
||||||
|
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "m", "custom": { "drawStyle": "line", "lineWidth": 2, "fillOpacity": 10 } }, "overrides": [] },
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 6 },
|
||||||
|
"id": 8,
|
||||||
|
"options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } },
|
||||||
|
"targets": [
|
||||||
|
{ "query": "from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and r._field =~ /^level\\.(predicted|measured)\\.atequipment/)\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", "refId": "A" }
|
||||||
|
],
|
||||||
|
"title": "Level",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "m\u00b3", "custom": { "drawStyle": "line", "lineWidth": 2, "fillOpacity": 10 } }, "overrides": [] },
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 6 },
|
||||||
|
"id": 9,
|
||||||
|
"options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } },
|
||||||
|
"targets": [
|
||||||
|
{ "query": "from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and r._field =~ /^volume\\.predicted\\.atequipment/)\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", "refId": "A" }
|
||||||
|
],
|
||||||
|
"title": "Volume",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 14 }, "id": 10, "title": "Flow", "type": "row" },
|
||||||
|
{
|
||||||
|
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "m\u00b3/h", "custom": { "drawStyle": "line", "lineWidth": 2, "fillOpacity": 10 } }, "overrides": [] },
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 15 },
|
||||||
|
"id": 11,
|
||||||
|
"options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } },
|
||||||
|
"targets": [
|
||||||
|
{ "query": "from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and r._field =~ /^netFlowRate\\.predicted\\.atequipment/)\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", "refId": "A" }
|
||||||
|
],
|
||||||
|
"title": "Net Flow Rate",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "m\u00b3/h", "custom": { "drawStyle": "line", "lineWidth": 2, "fillOpacity": 10 } }, "overrides": [] },
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 15 },
|
||||||
|
"id": 12,
|
||||||
|
"options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } },
|
||||||
|
"targets": [
|
||||||
|
{ "query": "from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and r._field =~ /^flow\\.(predicted|measured)\\.atequipment/)\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", "refId": "A" }
|
||||||
|
],
|
||||||
|
"title": "Inflow + Outflow",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 23 }, "id": 13, "title": "Configuration", "type": "row" },
|
||||||
|
{
|
||||||
|
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "m", "thresholds": { "mode": "absolute", "steps": [{ "color": "blue", "value": null }] } }, "overrides": [] },
|
||||||
|
"gridPos": { "h": 4, "w": 12, "x": 0, "y": 24 },
|
||||||
|
"id": 14,
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "none" },
|
||||||
|
"targets": [
|
||||||
|
{ "query": "from(bucket: \"${bucket}\")\n |> range(start: -7d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and (r._field==\"heightInlet\" or r._field==\"heightOverflow\" or r._field==\"volEmptyBasin\"))\n |> last()", "refId": "A" }
|
||||||
|
],
|
||||||
|
"title": "Heights",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "m\u00b3", "thresholds": { "mode": "absolute", "steps": [{ "color": "blue", "value": null }] } }, "overrides": [] },
|
||||||
|
"gridPos": { "h": 4, "w": 12, "x": 12, "y": 24 },
|
||||||
|
"id": 15,
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "none" },
|
||||||
|
"targets": [
|
||||||
|
{ "query": "from(bucket: \"${bucket}\")\n |> range(start: -7d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and (r._field==\"maxVol\" or r._field==\"minVol\" or r._field==\"maxVolOverflow\" or r._field==\"minVolOut\" or r._field==\"minVolIn\"))\n |> last()", "refId": "A" }
|
||||||
|
],
|
||||||
|
"title": "Volume Limits",
|
||||||
|
"type": "stat"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"schemaVersion": 39,
|
||||||
|
"tags": ["EVOLV", "pumpingStation", "template"],
|
||||||
|
"templating": {
|
||||||
|
"list": [
|
||||||
|
{ "name": "dbase", "type": "custom", "label": "dbase", "query": "cdzg44tv250jkd", "current": { "text": "cdzg44tv250jkd", "value": "cdzg44tv250jkd", "selected": false }, "options": [{ "text": "cdzg44tv250jkd", "value": "cdzg44tv250jkd", "selected": true }], "hide": 2 },
|
||||||
|
{ "name": "measurement", "type": "custom", "query": "template", "current": { "text": "template", "value": "template", "selected": false }, "options": [{ "text": "template", "value": "template", "selected": true }] },
|
||||||
|
{ "name": "bucket", "type": "custom", "query": "lvl2", "current": { "text": "lvl2", "value": "lvl2", "selected": false }, "options": [{ "text": "lvl2", "value": "lvl2", "selected": true }] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"time": { "from": "now-6h", "to": "now" },
|
||||||
|
"timezone": "",
|
||||||
|
"title": "template",
|
||||||
|
"uid": null,
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
97
config/reactor.json
Normal file
97
config/reactor.json
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": { "type": "grafana", "uid": "-- Grafana --" },
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"type": "dashboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"editable": true,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"id": null,
|
||||||
|
"links": [],
|
||||||
|
"panels": [
|
||||||
|
{ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 1, "title": "Status", "type": "row" },
|
||||||
|
{
|
||||||
|
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "mg/L", "thresholds": { "mode": "absolute", "steps": [{ "color": "red", "value": null }, { "color": "yellow", "value": 0.5 }, { "color": "green", "value": 1.5 }, { "color": "yellow", "value": 4 }, { "color": "red", "value": 6 }] } }, "overrides": [] },
|
||||||
|
"gridPos": { "h": 4, "w": 6, "x": 0, "y": 1 },
|
||||||
|
"id": 2,
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "area" },
|
||||||
|
"targets": [
|
||||||
|
{ "query": "from(bucket: \"${bucket}\")\n |> range(start: -7d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and r._field =~ /^S_O/)\n |> last()", "refId": "A" }
|
||||||
|
],
|
||||||
|
"title": "DO (S_O)",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "mg/L", "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 2 }, { "color": "red", "value": 5 }] } }, "overrides": [] },
|
||||||
|
"gridPos": { "h": 4, "w": 6, "x": 6, "y": 1 },
|
||||||
|
"id": 3,
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "area" },
|
||||||
|
"targets": [
|
||||||
|
{ "query": "from(bucket: \"${bucket}\")\n |> range(start: -7d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and r._field =~ /^S_NH/)\n |> last()", "refId": "A" }
|
||||||
|
],
|
||||||
|
"title": "NH\u2084 (S_NH)",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "mg/L", "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 5 }, { "color": "red", "value": 10 }] } }, "overrides": [] },
|
||||||
|
"gridPos": { "h": 4, "w": 6, "x": 12, "y": 1 },
|
||||||
|
"id": 4,
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "area" },
|
||||||
|
"targets": [
|
||||||
|
{ "query": "from(bucket: \"${bucket}\")\n |> range(start: -7d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and r._field =~ /^S_NO/)\n |> last()", "refId": "A" }
|
||||||
|
],
|
||||||
|
"title": "NO\u2083 (S_NO)",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "mg/L", "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 3000 }, { "color": "red", "value": 5000 }] } }, "overrides": [] },
|
||||||
|
"gridPos": { "h": 4, "w": 6, "x": 18, "y": 1 },
|
||||||
|
"id": 5,
|
||||||
|
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "area" },
|
||||||
|
"targets": [
|
||||||
|
{ "query": "from(bucket: \"${bucket}\")\n |> range(start: -7d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and r._field =~ /^X_TS/)\n |> last()", "refId": "A" }
|
||||||
|
],
|
||||||
|
"title": "TSS (X_TS)",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 }, "id": 6, "title": "Trends", "type": "row" },
|
||||||
|
{
|
||||||
|
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
|
||||||
|
"fieldConfig": { "defaults": { "custom": { "drawStyle": "line", "lineWidth": 2, "fillOpacity": 5 } }, "overrides": [] },
|
||||||
|
"gridPos": { "h": 9, "w": 24, "x": 0, "y": 6 },
|
||||||
|
"id": 7,
|
||||||
|
"options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } },
|
||||||
|
"targets": [
|
||||||
|
{ "query": "from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn:(r) => r._measurement==\"${measurement}\")\n |> filter(fn:(r) => r._field =~ /^(F|S_O|S_NH|S_NO|S_S|X_TS)/)\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", "refId": "A" }
|
||||||
|
],
|
||||||
|
"title": "Core Process Signals",
|
||||||
|
"type": "timeseries"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"schemaVersion": 39,
|
||||||
|
"tags": ["EVOLV", "reactor", "template"],
|
||||||
|
"templating": {
|
||||||
|
"list": [
|
||||||
|
{ "name": "dbase", "type": "custom", "label": "dbase", "query": "cdzg44tv250jkd", "current": { "text": "cdzg44tv250jkd", "value": "cdzg44tv250jkd", "selected": false }, "options": [{ "text": "cdzg44tv250jkd", "value": "cdzg44tv250jkd", "selected": true }], "hide": 2 },
|
||||||
|
{ "name": "measurement", "type": "custom", "query": "template", "current": { "text": "template", "value": "template", "selected": false }, "options": [{ "text": "template", "value": "template", "selected": true }] },
|
||||||
|
{ "name": "bucket", "type": "custom", "query": "lvl2", "current": { "text": "lvl2", "value": "lvl2", "selected": false }, "options": [{ "text": "lvl2", "value": "lvl2", "selected": true }] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"time": { "from": "now-6h", "to": "now" },
|
||||||
|
"timezone": "",
|
||||||
|
"title": "template",
|
||||||
|
"uid": null,
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
71
config/settler.json
Normal file
71
config/settler.json
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": { "type": "grafana", "uid": "-- Grafana --" },
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"type": "dashboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"editable": true,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"id": null,
|
||||||
|
"links": [],
|
||||||
|
"panels": [
|
||||||
|
{ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 1, "title": "Settler (Simulation/Process)", "type": "row" },
|
||||||
|
{
|
||||||
|
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
|
||||||
|
"gridPos": { "h": 9, "w": 24, "x": 0, "y": 1 },
|
||||||
|
"id": 2,
|
||||||
|
"options": { "legend": { "displayMode": "list", "placement": "bottom" } },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"query": "from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn:(r) => r._measurement==\"${measurement}\")\n |> filter(fn:(r) => r._field =~ /^(F_in|F_eff|F_so|F_sr|C_TS)/)\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Flows / Solids (if logged)",
|
||||||
|
"type": "timeseries"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"schemaVersion": 39,
|
||||||
|
"tags": ["EVOLV", "settler", "template"],
|
||||||
|
"templating": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"name": "dbase",
|
||||||
|
"type": "custom",
|
||||||
|
"label": "dbase",
|
||||||
|
"query": "cdzg44tv250jkd",
|
||||||
|
"current": { "text": "cdzg44tv250jkd", "value": "cdzg44tv250jkd", "selected": false },
|
||||||
|
"options": [{ "text": "cdzg44tv250jkd", "value": "cdzg44tv250jkd", "selected": true }],
|
||||||
|
"hide": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "measurement",
|
||||||
|
"type": "custom",
|
||||||
|
"query": "template",
|
||||||
|
"current": { "text": "template", "value": "template", "selected": false },
|
||||||
|
"options": [{ "text": "template", "value": "template", "selected": true }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bucket",
|
||||||
|
"type": "custom",
|
||||||
|
"query": "lvl2",
|
||||||
|
"current": { "text": "lvl2", "value": "lvl2", "selected": false },
|
||||||
|
"options": [{ "text": "lvl2", "value": "lvl2", "selected": true }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"time": { "from": "now-6h", "to": "now" },
|
||||||
|
"timezone": "",
|
||||||
|
"title": "template",
|
||||||
|
"uid": null,
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
|
||||||
85
config/valve.json
Normal file
85
config/valve.json
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": { "type": "grafana", "uid": "-- Grafana --" },
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"type": "dashboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"editable": true,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"id": null,
|
||||||
|
"links": [],
|
||||||
|
"panels": [
|
||||||
|
{ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 1, "title": "Realtime Valve", "type": "row" },
|
||||||
|
{
|
||||||
|
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
|
||||||
|
"gridPos": { "h": 5, "w": 8, "x": 0, "y": 1 },
|
||||||
|
"id": 2,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"query": "from(bucket: \"${bucket}\")\n |> range(start: -30d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and (r._field==\"state\" or r._field==\"mode\" or r._field==\"percentageOpen\"))\n |> last()",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "State / Mode / %Open (last)",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "none" }, "overrides": [] },
|
||||||
|
"gridPos": { "h": 9, "w": 16, "x": 8, "y": 1 },
|
||||||
|
"id": 3,
|
||||||
|
"options": { "legend": { "displayMode": "list", "placement": "bottom" } },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"query": "from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and (r._field==\"downstream_predicted_flow\" or r._field==\"downstream_measured_flow\" or r._field==\"delta_predicted_pressure\"))\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Flow + ΔP",
|
||||||
|
"type": "timeseries"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"schemaVersion": 39,
|
||||||
|
"tags": ["EVOLV", "valve", "template"],
|
||||||
|
"templating": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"name": "dbase",
|
||||||
|
"type": "custom",
|
||||||
|
"label": "dbase",
|
||||||
|
"query": "cdzg44tv250jkd",
|
||||||
|
"current": { "text": "cdzg44tv250jkd", "value": "cdzg44tv250jkd", "selected": false },
|
||||||
|
"options": [{ "text": "cdzg44tv250jkd", "value": "cdzg44tv250jkd", "selected": true }],
|
||||||
|
"hide": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "measurement",
|
||||||
|
"type": "custom",
|
||||||
|
"query": "template",
|
||||||
|
"current": { "text": "template", "value": "template", "selected": false },
|
||||||
|
"options": [{ "text": "template", "value": "template", "selected": true }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bucket",
|
||||||
|
"type": "custom",
|
||||||
|
"query": "lvl2",
|
||||||
|
"current": { "text": "lvl2", "value": "lvl2", "selected": false },
|
||||||
|
"options": [{ "text": "lvl2", "value": "lvl2", "selected": true }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"time": { "from": "now-6h", "to": "now" },
|
||||||
|
"timezone": "",
|
||||||
|
"title": "template",
|
||||||
|
"uid": null,
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
|
||||||
85
config/valveGroupControl.json
Normal file
85
config/valveGroupControl.json
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": { "type": "grafana", "uid": "-- Grafana --" },
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"type": "dashboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"editable": true,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"id": null,
|
||||||
|
"links": [],
|
||||||
|
"panels": [
|
||||||
|
{ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 1, "title": "Realtime Valve Group", "type": "row" },
|
||||||
|
{
|
||||||
|
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
|
||||||
|
"gridPos": { "h": 5, "w": 8, "x": 0, "y": 1 },
|
||||||
|
"id": 2,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"query": "from(bucket: \"${bucket}\")\n |> range(start: -30d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and (r._field==\"mode\" or r._field==\"maxDeltaP\"))\n |> last()",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Mode / maxΔP (last)",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
|
||||||
|
"fieldConfig": { "defaults": { "unit": "none" }, "overrides": [] },
|
||||||
|
"gridPos": { "h": 9, "w": 16, "x": 8, "y": 1 },
|
||||||
|
"id": 3,
|
||||||
|
"options": { "legend": { "displayMode": "list", "placement": "bottom" } },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"query": "from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and (r._field =~ /predicted_flow|measured_flow/ or r._field==\"maxDeltaP\"))\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Flow + maxΔP",
|
||||||
|
"type": "timeseries"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"schemaVersion": 39,
|
||||||
|
"tags": ["EVOLV", "valveGroupControl", "template"],
|
||||||
|
"templating": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"name": "dbase",
|
||||||
|
"type": "custom",
|
||||||
|
"label": "dbase",
|
||||||
|
"query": "cdzg44tv250jkd",
|
||||||
|
"current": { "text": "cdzg44tv250jkd", "value": "cdzg44tv250jkd", "selected": false },
|
||||||
|
"options": [{ "text": "cdzg44tv250jkd", "value": "cdzg44tv250jkd", "selected": true }],
|
||||||
|
"hide": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "measurement",
|
||||||
|
"type": "custom",
|
||||||
|
"query": "template",
|
||||||
|
"current": { "text": "template", "value": "template", "selected": false },
|
||||||
|
"options": [{ "text": "template", "value": "template", "selected": true }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bucket",
|
||||||
|
"type": "custom",
|
||||||
|
"query": "lvl2",
|
||||||
|
"current": { "text": "lvl2", "value": "lvl2", "selected": false },
|
||||||
|
"options": [{ "text": "lvl2", "value": "lvl2", "selected": true }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"time": { "from": "now-6h", "to": "now" },
|
||||||
|
"timezone": "",
|
||||||
|
"title": "template",
|
||||||
|
"uid": null,
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,134 +1,94 @@
|
|||||||
<script type="module">
|
<script src="/dashboardapi/menu.js"></script>
|
||||||
|
<script src="/dashboardapi/configData.js"></script>
|
||||||
|
|
||||||
import * as menuUtils from "/generalfunctions/helper/menuUtils.js";
|
<script>
|
||||||
|
RED.nodes.registerType('dashboardapi', {
|
||||||
|
category: 'EVOLV',
|
||||||
|
color: '#4f8582',
|
||||||
|
defaults: {
|
||||||
|
name: { value: '' },
|
||||||
|
enableLog: { value: false },
|
||||||
|
logLevel: { value: 'info' },
|
||||||
|
|
||||||
RED.nodes.registerType('dashboardapi', {
|
protocol: { value: 'http' },
|
||||||
category: 'wbd typical',
|
host: { value: 'localhost' },
|
||||||
color: '#4f8582',
|
port: { value: 3000 },
|
||||||
defaults: {
|
bearerToken: { value: '' },
|
||||||
name: { value: "" },
|
defaultBucket: { value: '' },
|
||||||
// New defaults for configuration:
|
},
|
||||||
logLevel: { value: "info" },
|
inputs: 1,
|
||||||
enableLog: { value: false },
|
outputs: 1,
|
||||||
host: { value: "" },
|
inputLabels: ['Input'],
|
||||||
port: { value: 0 },
|
outputLabels: ['grafana'],
|
||||||
bearerToken: { value: "" }
|
icon: 'font-awesome/fa-area-chart',
|
||||||
},
|
|
||||||
inputs: 1,
|
|
||||||
outputs: 1,
|
|
||||||
inputLabels: "Usage see manual",
|
|
||||||
outputLabels: ["feedback"],
|
|
||||||
icon: "font-awesome/fa-area-chart",
|
|
||||||
|
|
||||||
label: function () {
|
label: function () {
|
||||||
// Show the name
|
return this.name || 'dashboardapi';
|
||||||
return this.name || "dashboardapi";
|
},
|
||||||
},
|
|
||||||
|
|
||||||
oneditprepare: function () {
|
oneditprepare: function () {
|
||||||
|
const waitForMenuData = () => {
|
||||||
const node = this;
|
if (window.EVOLV?.nodes?.dashboardapi?.loggerMenu?.initEditor) {
|
||||||
|
window.EVOLV.nodes.dashboardapi.loggerMenu.initEditor(this);
|
||||||
console.log("Edit Prepare");
|
} else {
|
||||||
|
setTimeout(waitForMenuData, 50);
|
||||||
const elements = {
|
|
||||||
// Basic fields
|
|
||||||
name: document.getElementById("node-input-name"),
|
|
||||||
number: document.getElementById("node-input-number"),
|
|
||||||
// Logging fields
|
|
||||||
logLevelSelect: document.getElementById("node-input-logLevel"),
|
|
||||||
logCheckbox: document.getElementById("node-input-enableLog"),
|
|
||||||
// Grafana connector fields
|
|
||||||
host: document.getElementById("node-input-host"),
|
|
||||||
port: document.getElementById("node-input-port"),
|
|
||||||
bearerToken: document.getElementById("node-input-bearerToken"),
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
// UI elements
|
|
||||||
menuUtils.initBasicToggles(elements);
|
|
||||||
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.log("Error fetching project settings", e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
oneditsave: function () {
|
|
||||||
const node = this;
|
|
||||||
|
|
||||||
console.log(`------------ Saving changes to node ------------`);
|
|
||||||
|
|
||||||
//save basic properties
|
|
||||||
["name", "host", "port", "bearerToken"].forEach(
|
|
||||||
(field) => {
|
|
||||||
const element = document.getElementById(`node-input-${field}`);
|
|
||||||
if (element) {
|
|
||||||
node[field] = element.value || "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const logLevelElement = document.getElementById("node-input-logLevel");
|
|
||||||
node.logLevel = logLevelElement ? logLevelElement.value || "info" : "info";
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
waitForMenuData();
|
||||||
|
},
|
||||||
|
|
||||||
});
|
oneditsave: function () {
|
||||||
|
const node = this;
|
||||||
|
|
||||||
|
if (window.EVOLV?.nodes?.dashboardapi?.loggerMenu?.saveEditor) {
|
||||||
|
window.EVOLV.nodes.dashboardapi.loggerMenu.saveEditor(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
['name', 'protocol', 'host', 'port', 'bearerToken', 'defaultBucket'].forEach((field) => {
|
||||||
|
const element = document.getElementById(`node-input-${field}`);
|
||||||
|
if (!element) return;
|
||||||
|
node[field] = field === 'port' ? parseInt(element.value, 10) || 3000 : element.value || '';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Main UI Template -->
|
<!-- Main UI Template -->
|
||||||
<script type="text/html" data-template-name="dashboardapi">
|
<script type="text/html" data-template-name="dashboardapi">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||||
<input
|
<input type="text" id="node-input-name" placeholder="name" style="width:70%;" />
|
||||||
type="text"
|
</div>
|
||||||
id="node-input-name"
|
|
||||||
placeholder="name"
|
<div class="form-row">
|
||||||
style="width:70%;"
|
<label for="node-input-protocol"><i class="fa fa-exchange"></i> Protocol</label>
|
||||||
/>
|
<select id="node-input-protocol" style="width:70%;">
|
||||||
</div>
|
<option value="http">http</option>
|
||||||
|
<option value="https">https</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-host"><i class="fa fa-server"></i> Grafana Host</label>
|
<label for="node-input-host"><i class="fa fa-server"></i> Grafana Host</label>
|
||||||
<input type="text" id="node-input-host" placeholder="Host">
|
<input type="text" id="node-input-host" placeholder="localhost" style="width:70%;" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-port"><i class="fa fa-plug"></i> Grafana Port</label>
|
<label for="node-input-port"><i class="fa fa-plug"></i> Grafana Port</label>
|
||||||
<input type="number" id="node-input-port" placeholder="Port">
|
<input type="number" id="node-input-port" placeholder="3000" style="width:70%;" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-bearerToken"><i class="fa fa-key"></i> Bearer Token</label>
|
<label for="node-input-bearerToken"><i class="fa fa-key"></i> Bearer Token</label>
|
||||||
<input type="text" id="node-input-bearerToken" placeholder="Bearer Token">
|
<input type="password" id="node-input-bearerToken" placeholder="optional" style="width:70%;" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr />
|
<div class="form-row">
|
||||||
|
<label for="node-input-defaultBucket"><i class="fa fa-database"></i> InfluxDB Bucket</label>
|
||||||
<!-- loglevel checkbox -->
|
<input type="text" id="node-input-defaultBucket" placeholder="env INFLUXDB_BUCKET or lvl2" style="width:70%;" />
|
||||||
<div class="form-row">
|
|
||||||
<label for="node-input-enableLog"
|
|
||||||
><i class="fa fa-cog"></i> Enable Log</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="node-input-enableLog"
|
|
||||||
style="width:20px; vertical-align:baseline;"
|
|
||||||
/>
|
|
||||||
<span>Enable logging</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-row" id="row-logLevel">
|
|
||||||
<label for="node-input-logLevel"><i class="fa fa-cog"></i> Log Level</label>
|
|
||||||
<select id="node-input-logLevel" style="width:60%;">
|
|
||||||
<option value="info">Info</option>
|
|
||||||
<option value="debug">Debug</option>
|
|
||||||
<option value="warn">Warn</option>
|
|
||||||
<option value="error">Error</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="logger-fields-placeholder"></div>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script type="text/html" data-help-name="dashboardapi">
|
<script type="text/html" data-help-name="dashboardapi">
|
||||||
@@ -146,4 +106,4 @@
|
|||||||
|
|
||||||
These features provide flexible and controlled interactions with the Grafana API.
|
These features provide flexible and controlled interactions with the Grafana API.
|
||||||
</p>
|
</p>
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
133
dashboardapi.js
133
dashboardapi.js
@@ -1,108 +1,41 @@
|
|||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const nameOfNode = 'dashboardapi';
|
||||||
|
const nodeClass = require('./src/nodeClass.js');
|
||||||
|
const { MenuManager } = require('generalFunctions');
|
||||||
|
|
||||||
module.exports = function (RED) {
|
module.exports = function (RED) {
|
||||||
function dashboardapi(config) {
|
RED.nodes.registerType(nameOfNode, function (config) {
|
||||||
// create node
|
|
||||||
RED.nodes.createNode(this, config);
|
RED.nodes.createNode(this, config);
|
||||||
|
this.nodeClass = new nodeClass(config, RED, this, nameOfNode);
|
||||||
|
});
|
||||||
|
|
||||||
//call this => node so whenver you want to call a node function type node and the function behind it
|
const menuMgr = new MenuManager();
|
||||||
var node = this;
|
|
||||||
|
|
||||||
|
RED.httpAdmin.get(`/${nameOfNode}/menu.js`, (req, res) => {
|
||||||
try {
|
try {
|
||||||
//fetch obj
|
const script = menuMgr.createEndpoint(nameOfNode, ['logger']);
|
||||||
const Dashboardapi = require("./dependencies/dashboardapi/dashboardapi_class");
|
res.type('application/javascript').send(script);
|
||||||
|
} catch (err) {
|
||||||
//load user defined config in the node-red UI
|
res.status(500).send(`// Error generating menu: ${err.message}`);
|
||||||
const dConfig = {
|
|
||||||
general: {
|
|
||||||
name: config.name,
|
|
||||||
id: node.id,
|
|
||||||
logging: {
|
|
||||||
logLevel: config.logLevel,
|
|
||||||
enabled: config.enableLog,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
grafanaConnector: {
|
|
||||||
host: config.host,
|
|
||||||
port: config.port,
|
|
||||||
bearerToken: config.bearerToken,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
//make new measurement on creation to work with.
|
|
||||||
const d = new Dashboardapi(dConfig);
|
|
||||||
|
|
||||||
// put m on node memory as source
|
|
||||||
node.source = d;
|
|
||||||
|
|
||||||
function updateNodeStatus(val) {
|
|
||||||
if (val && val.grafanaResponse) {
|
|
||||||
// Check for a successful response from the Grafana API call
|
|
||||||
if (val.grafanaResponse.status === 200) {
|
|
||||||
node.status({
|
|
||||||
fill: "green",
|
|
||||||
shape: "dot",
|
|
||||||
text: "Grafana API: Success",
|
|
||||||
});
|
|
||||||
node.log("Grafana API call completed successfully.");
|
|
||||||
} else {
|
|
||||||
node.status({
|
|
||||||
fill: "red",
|
|
||||||
shape: "ring",
|
|
||||||
text: "Grafana API: Error",
|
|
||||||
});
|
|
||||||
node.error(
|
|
||||||
"Grafana API call failed with status: " +
|
|
||||||
val.grafanaResponse.status
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//-------------------------------------------------------------------->>what to do on input
|
|
||||||
node.on("input", async function (msg, send, done) {
|
|
||||||
try {
|
|
||||||
switch(msg.topic) {
|
|
||||||
//on start make dashboard
|
|
||||||
case 'registerChild':
|
|
||||||
|
|
||||||
const childId = msg.payload;
|
|
||||||
const childObj = RED.nodes.getNode(childId);
|
|
||||||
if (!childObj || !childObj.source) {
|
|
||||||
throw new Error("Missing or invalid child node");
|
|
||||||
}
|
|
||||||
const child = childObj.source;
|
|
||||||
|
|
||||||
msg.payload = await d.generateDashB(child.config);
|
|
||||||
|
|
||||||
msg.topic = "create";
|
|
||||||
msg.headers = {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': 'Bearer glsa_gI7fOMEd844p1gZt9iaDeEFpeYtejRj7_cf1c41f8'// + config.bearerToken
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(`Child registered: ${childId}`);
|
|
||||||
send(msg);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
done();
|
|
||||||
} catch (err) {
|
|
||||||
node.status({ fill: "red", shape: "ring", text: "Bad request data" });
|
|
||||||
node.error("Bad request data: " + err.message, msg);
|
|
||||||
done(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// tidy up any async code here - shutdown connections and so on.
|
|
||||||
node.on("close", function () {
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
RED.nodes.registerType("dashboardapi", dashboardapi);
|
// Provide config metadata for the editor (local, no dependency on generalFunctions configs).
|
||||||
|
RED.httpAdmin.get(`/${nameOfNode}/configData.js`, (req, res) => {
|
||||||
|
try {
|
||||||
|
const configPath = path.join(__dirname, 'dependencies', 'dashboardapi', 'dashboardapiConfig.json');
|
||||||
|
const json = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||||
|
const script = `
|
||||||
|
window.EVOLV = window.EVOLV || {};
|
||||||
|
window.EVOLV.nodes = window.EVOLV.nodes || {};
|
||||||
|
window.EVOLV.nodes.${nameOfNode} = window.EVOLV.nodes.${nameOfNode} || {};
|
||||||
|
window.EVOLV.nodes.${nameOfNode}.config = ${JSON.stringify(json, null, 2)};
|
||||||
|
`;
|
||||||
|
res.type('application/javascript').send(script);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).send(`// Error generating configData: ${err.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
8
examples/README.md
Normal file
8
examples/README.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# dashboardAPI Example Flows
|
||||||
|
|
||||||
|
Import-ready Node-RED examples for dashboardAPI.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
- basic.flow.json
|
||||||
|
- integration.flow.json
|
||||||
|
- edge.flow.json
|
||||||
6
examples/basic.flow.json
Normal file
6
examples/basic.flow.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[
|
||||||
|
{"id":"dashboardAPI_basic_tab","type":"tab","label":"dashboardAPI basic","disabled":false,"info":"dashboardAPI basic example"},
|
||||||
|
{"id":"dashboardAPI_basic_node","type":"dashboardapi","z":"dashboardAPI_basic_tab","name":"dashboardAPI basic","x":420,"y":180,"wires":[["dashboardAPI_basic_dbg"]]},
|
||||||
|
{"id":"dashboardAPI_basic_inj","type":"inject","z":"dashboardAPI_basic_tab","name":"basic trigger","props":[{"p":"topic","vt":"str"},{"p":"payload","vt":"str"}],"topic":"ping","payload":"1","payloadType":"str","x":160,"y":180,"wires":[["dashboardAPI_basic_node"]]},
|
||||||
|
{"id":"dashboardAPI_basic_dbg","type":"debug","z":"dashboardAPI_basic_tab","name":"dashboardAPI basic debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":660,"y":180,"wires":[]}
|
||||||
|
]
|
||||||
6
examples/edge.flow.json
Normal file
6
examples/edge.flow.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[
|
||||||
|
{"id":"dashboardAPI_edge_tab","type":"tab","label":"dashboardAPI edge","disabled":false,"info":"dashboardAPI edge example"},
|
||||||
|
{"id":"dashboardAPI_edge_node","type":"dashboardapi","z":"dashboardAPI_edge_tab","name":"dashboardAPI edge","x":420,"y":180,"wires":[["dashboardAPI_edge_dbg"]]},
|
||||||
|
{"id":"dashboardAPI_edge_inj","type":"inject","z":"dashboardAPI_edge_tab","name":"unknown topic","props":[{"p":"topic","vt":"str"},{"p":"payload","vt":"str"}],"topic":"doesNotExist","payload":"x","payloadType":"str","x":170,"y":180,"wires":[["dashboardAPI_edge_node"]]},
|
||||||
|
{"id":"dashboardAPI_edge_dbg","type":"debug","z":"dashboardAPI_edge_tab","name":"dashboardAPI edge debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":660,"y":180,"wires":[]}
|
||||||
|
]
|
||||||
6
examples/integration.flow.json
Normal file
6
examples/integration.flow.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[
|
||||||
|
{"id":"dashboardAPI_int_tab","type":"tab","label":"dashboardAPI integration","disabled":false,"info":"dashboardAPI integration example"},
|
||||||
|
{"id":"dashboardAPI_int_node","type":"dashboardapi","z":"dashboardAPI_int_tab","name":"dashboardAPI integration","x":420,"y":180,"wires":[["dashboardAPI_int_dbg"]]},
|
||||||
|
{"id":"dashboardAPI_int_inj","type":"inject","z":"dashboardAPI_int_tab","name":"registerChild","props":[{"p":"topic","vt":"str"},{"p":"payload","vt":"str"}],"topic":"registerChild","payload":"example-child-id","payloadType":"str","x":170,"y":180,"wires":[["dashboardAPI_int_node"]]},
|
||||||
|
{"id":"dashboardAPI_int_dbg","type":"debug","z":"dashboardAPI_int_tab","name":"dashboardAPI integration debug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":680,"y":180,"wires":[]}
|
||||||
|
]
|
||||||
25
package.json
Normal file
25
package.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "dashboardAPI",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "EVOLV Grafana dashboard generator (Node-RED node).",
|
||||||
|
"main": "dashboardapi.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "node --test test/basic/*.test.js test/integration/*.test.js test/edge/*.test.js"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"dashboard",
|
||||||
|
"grafana",
|
||||||
|
"node-red",
|
||||||
|
"EVOLV"
|
||||||
|
],
|
||||||
|
"author": "EVOLV",
|
||||||
|
"license": "SEE LICENSE",
|
||||||
|
"dependencies": {
|
||||||
|
"generalFunctions": "git+https://gitea.centraal.wbd-rd.nl/RnD/generalFunctions.git"
|
||||||
|
},
|
||||||
|
"node-red": {
|
||||||
|
"nodes": {
|
||||||
|
"dashboardapi": "dashboardapi.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
134
src/nodeClass.js
Normal file
134
src/nodeClass.js
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
const { configManager } = require('generalFunctions');
|
||||||
|
const DashboardApi = 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) {
|
||||||
|
const cfgMgr = new configManager();
|
||||||
|
this.config = cfgMgr.buildConfig(this.name, uiConfig, this.node.id, {
|
||||||
|
functionality: {
|
||||||
|
softwareType: this.name.toLowerCase(),
|
||||||
|
role: 'auto ui generator',
|
||||||
|
},
|
||||||
|
grafanaConnector: {
|
||||||
|
protocol: uiConfig.protocol || 'http',
|
||||||
|
host: uiConfig.host || 'localhost',
|
||||||
|
port: Number(uiConfig.port || 3000),
|
||||||
|
bearerToken: uiConfig.bearerToken || '',
|
||||||
|
},
|
||||||
|
defaultBucket: uiConfig.defaultBucket || process.env.INFLUXDB_BUCKET || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupSpecificClass() {
|
||||||
|
this.source = new DashboardApi(this.config);
|
||||||
|
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() {
|
||||||
|
this.node.on('input', async (msg, send, done) => {
|
||||||
|
try {
|
||||||
|
if (msg.topic !== 'registerChild') {
|
||||||
|
if (typeof done === 'function') done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: 'dashboardapi error' });
|
||||||
|
this.node.error(error?.message || error, msg);
|
||||||
|
if (typeof done === 'function') done(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_attachCloseHandler() {
|
||||||
|
this.node.on('close', (done) => {
|
||||||
|
if (typeof done === 'function') done();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nodeClass;
|
||||||
211
src/specificClass.js
Normal file
211
src/specificClass.js
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dashboard domain service.
|
||||||
|
* Builds Grafana dashboard payloads from EVOLV node config and child topology.
|
||||||
|
*/
|
||||||
|
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 || '',
|
||||||
|
},
|
||||||
|
defaultBucket: config?.defaultBucket || '',
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.warn(`No dashboard template found for softwareType=${st}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadTemplate(softwareType) {
|
||||||
|
const templatePath = this._templateFileForSoftwareType(softwareType);
|
||||||
|
if (!templatePath) return null;
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Missing templates are treated as non-fatal: we skip only that dashboard.
|
||||||
|
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}`);
|
||||||
|
|
||||||
|
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.defaultBucket ||
|
||||||
|
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) {
|
||||||
|
this.logger.warn('generateDashboardsForGraph skipped: root source missing config');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootPosition = rootSource?.positionVsParent || rootSource?.config?.functionality?.positionVsParent;
|
||||||
|
const rootDash = this.buildDashboard({ nodeConfig: rootSource.config, positionVsParent: rootPosition });
|
||||||
|
if (!rootDash) return [];
|
||||||
|
|
||||||
|
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 });
|
||||||
|
if (childDash) 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;
|
||||||
12
test/README.md
Normal file
12
test/README.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# dashboardAPI Test Suite Layout
|
||||||
|
|
||||||
|
Required EVOLV layout:
|
||||||
|
- basic/
|
||||||
|
- integration/
|
||||||
|
- edge/
|
||||||
|
- helpers/
|
||||||
|
|
||||||
|
Baseline structure tests:
|
||||||
|
- basic/structure-module-load.basic.test.js
|
||||||
|
- integration/structure-examples.integration.test.js
|
||||||
|
- edge/structure-examples-node-type.edge.test.js
|
||||||
0
test/basic/.gitkeep
Normal file
0
test/basic/.gitkeep
Normal file
7
test/basic/structure-module-load.basic.test.js
Normal file
7
test/basic/structure-module-load.basic.test.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
describe('dashboardAPI basic structure', () => {
|
||||||
|
it('module load smoke', () => {
|
||||||
|
expect(() => {
|
||||||
|
require('../../dashboardapi.js');
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
81
test/dashboardapi.test.js
Normal file
81
test/dashboardapi.test.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
const DashboardApi = require('../src/specificClass');
|
||||||
|
|
||||||
|
function makeNodeSource({ id, name, softwareType, positionVsParent, children = [] }) {
|
||||||
|
const registeredChildren = new Map();
|
||||||
|
for (const child of children) {
|
||||||
|
registeredChildren.set(child.config.general.id, {
|
||||||
|
child,
|
||||||
|
softwareType: child.config.functionality.softwareType,
|
||||||
|
position: child.positionVsParent || child.config.functionality.positionVsParent,
|
||||||
|
registeredAt: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
config: {
|
||||||
|
general: { id, name },
|
||||||
|
functionality: { softwareType, positionVsParent },
|
||||||
|
},
|
||||||
|
positionVsParent,
|
||||||
|
childRegistrationUtils: { registeredChildren },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
0
test/edge/.gitkeep
Normal file
0
test/edge/.gitkeep
Normal file
11
test/edge/structure-examples-node-type.edge.test.js
Normal file
11
test/edge/structure-examples-node-type.edge.test.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const flow = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../../examples/basic.flow.json'), 'utf8'));
|
||||||
|
|
||||||
|
describe('dashboardAPI edge example structure', () => {
|
||||||
|
it('basic example includes node type dashboardapi', () => {
|
||||||
|
const count = flow.filter((n) => n && n.type === 'dashboardapi').length;
|
||||||
|
expect(count).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
0
test/helpers/.gitkeep
Normal file
0
test/helpers/.gitkeep
Normal file
0
test/integration/.gitkeep
Normal file
0
test/integration/.gitkeep
Normal file
23
test/integration/structure-examples.integration.test.js
Normal file
23
test/integration/structure-examples.integration.test.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const dir = path.resolve(__dirname, '../../examples');
|
||||||
|
|
||||||
|
function loadJson(file) {
|
||||||
|
return JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
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