Compare commits

...

4 Commits

Author SHA1 Message Date
znetsixe
869ba4fca5 docs: add CLAUDE.md with S88 classification and superproject rule reference
References the flow-layout rule set in the EVOLV superproject
(.claude/rules/node-red-flow-layout.md) so Claude Code sessions working
in this repo know the S88 level, colour, and placement lane for this node.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 07:48:20 +02:00
Rene De Ren
66b91883ac Fix dashboardapi adapter and Jest coverage 2026-03-12 16:46:50 +01:00
Rene De Ren
c5272fcc24 Adopt buildConfig in dashboardapi adapter 2026-03-12 16:43:29 +01:00
znetsixe
89d2260351 updates 2026-03-11 11:13:44 +01:00
15 changed files with 760 additions and 334 deletions

23
CLAUDE.md Normal file
View File

@@ -0,0 +1,23 @@
# dashboardAPI — Claude Code context
InfluxDB telemetry and FlowFuse chart endpoints.
Part of the [EVOLV](https://gitea.wbd-rd.nl/RnD/EVOLV) wastewater-automation platform.
## S88 classification
| Level | Colour | Placement lane |
|---|---|---|
| **Utility (no S88 level)** | `none` | n/a |
## Flow layout rules
When wiring this node into a multi-node demo or production flow, follow the
placement rule set in the **EVOLV superproject**:
> `.claude/rules/node-red-flow-layout.md` (in the EVOLV repo root)
Key points for this node:
- Place on lane **n/a** (x-position per the lane table in the rule).
- Stack same-level siblings vertically.
- Parent/children sit on adjacent lanes (children one lane left, parent one lane right).
- Wrap in a Node-RED group box coloured `none` (Utility (no S88 level)).

View File

@@ -17,106 +17,174 @@
"id": null, "id": null,
"links": [], "links": [],
"panels": [ "panels": [
{ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 1, "title": "Realtime Machine", "type": "row" }, { "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 1, "title": "Status", "type": "row" },
{ {
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" }, "datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
"gridPos": { "h": 5, "w": 6, "x": 0, "y": 1 }, "fieldConfig": { "defaults": { "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }, "mappings": [{ "type": "value", "options": { "off": { "color": "red", "text": "OFF" }, "idle": { "color": "blue", "text": "IDLE" }, "operational": { "color": "green", "text": "RUNNING" }, "starting": { "color": "yellow", "text": "STARTING" }, "stopping": { "color": "yellow", "text": "STOPPING" } } }] }, "overrides": [] },
"gridPos": { "h": 4, "w": 5, "x": 0, "y": 1 },
"id": 2, "id": 2,
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "none" },
"targets": [ "targets": [
{ { "query": "from(bucket: \"${bucket}\")\n |> range(start: -7d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and r._field==\"state\")\n |> last()", "refId": "A" }
"query": "from(bucket: \"${bucket}\")\n |> range(start: -7d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and (r._field==\"state\" or r._field==\"mode\"))\n |> last()",
"refId": "A"
}
], ],
"title": "State / Mode (last)", "title": "State",
"type": "stat" "type": "stat"
}, },
{ {
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" }, "datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
"gridPos": { "h": 5, "w": 6, "x": 6, "y": 1 }, "fieldConfig": { "defaults": { "thresholds": { "mode": "absolute", "steps": [{ "color": "purple", "value": null }] } }, "overrides": [] },
"gridPos": { "h": 4, "w": 5, "x": 5, "y": 1 },
"id": 3, "id": 3,
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "none" },
"targets": [ "targets": [
{ { "query": "from(bucket: \"${bucket}\")\n |> range(start: -7d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and r._field==\"mode\")\n |> last()", "refId": "A" }
"query": "from(bucket: \"${bucket}\")\n |> range(start: -7d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and (r._field==\"ctrl\" or r._field==\"runtime\"))\n |> last()",
"refId": "A"
}
], ],
"title": "Ctrl / Runtime (last)", "title": "Mode",
"type": "stat" "type": "stat"
}, },
{ {
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" }, "datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
"fieldConfig": { "defaults": { "unit": "none" }, "overrides": [] }, "fieldConfig": { "defaults": { "min": 0, "max": 100, "unit": "percent", "thresholds": { "mode": "absolute", "steps": [{ "color": "blue", "value": null }, { "color": "green", "value": 20 }, { "color": "yellow", "value": 80 }, { "color": "red", "value": 95 }] } }, "overrides": [] },
"gridPos": { "h": 9, "w": 12, "x": 12, "y": 1 }, "gridPos": { "h": 4, "w": 5, "x": 10, "y": 1 },
"id": 4, "id": 4,
"options": { "legend": { "displayMode": "list", "placement": "bottom" } }, "options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "showThresholdLabels": false, "showThresholdMarkers": true },
"targets": [ "targets": [
{ "query": "from(bucket: \"${bucket}\")\n |> range(start: -7d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and r._field==\"ctrl\")\n |> last()", "refId": "A" }
],
"title": "Ctrl %",
"type": "gauge"
},
{ {
"query": "from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and (r._field==\"flow.predicted.downstream\" or r._field==\"flow.predicted.atEquipment\"))\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", "datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
"refId": "A" "fieldConfig": { "defaults": { "unit": "h", "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] } }, "overrides": [] },
} "gridPos": { "h": 4, "w": 5, "x": 15, "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==\"runtime\")\n |> last()", "refId": "A" }
],
"title": "Runtime",
"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": 30 }, { "color": "yellow", "value": 60 }, { "color": "green", "value": 80 }] } }, "overrides": [] },
"gridPos": { "h": 4, "w": 4, "x": 20, "y": 1 },
"id": 6,
"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==\"NCogPercent\")\n |> last()", "refId": "A" }
],
"title": "NCog %",
"type": "gauge"
},
{ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 }, "id": 7, "title": "Flow & Efficiency", "type": "row" },
{
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
"fieldConfig": { "defaults": { "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 =~ /^flow\\.predicted\\.(downstream|atequipment)/)\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", "refId": "A" }
], ],
"title": "Flow (predicted)", "title": "Flow (predicted)",
"type": "timeseries" "type": "timeseries"
}, },
{ {
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" }, "datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
"fieldConfig": { "defaults": { "unit": "none" }, "overrides": [] }, "fieldConfig": { "defaults": { "custom": { "drawStyle": "line", "lineWidth": 2, "fillOpacity": 10 } }, "overrides": [] },
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 6 }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 6 },
"id": 5, "id": 9,
"options": { "legend": { "displayMode": "list", "placement": "bottom" } }, "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } },
"targets": [ "targets": [
{ { "query": "from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and (r._field==\"cog\" or r._field==\"NCogPercent\"))\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", "refId": "A" }
"query": "from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and r._field==\"power.predicted.atEquipment\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)",
"refId": "A"
}
], ],
"title": "Power (predicted @ atEquipment)", "title": "Efficiency (CoG + NCog%)",
"type": "timeseries"
},
{ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 14 }, "id": 10, "title": "Pressure & Temperature", "type": "row" },
{
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
"fieldConfig": { "defaults": { "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 =~ /^pressure\\.(predicted|measured)\\.(upstream|downstream)/)\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", "refId": "A" }
],
"title": "Pressure (upstream / downstream)",
"type": "timeseries" "type": "timeseries"
}, },
{ {
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" }, "datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
"fieldConfig": { "defaults": { "unit": "none" }, "overrides": [] }, "fieldConfig": { "defaults": { "custom": { "drawStyle": "line", "lineWidth": 2, "fillOpacity": 10 } }, "overrides": [] },
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 10 }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 15 },
"id": 6, "id": 12,
"options": { "legend": { "displayMode": "list", "placement": "bottom" } }, "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } },
"targets": [ "targets": [
{ { "query": "from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and r._field =~ /^temperature/)\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", "refId": "A" }
"query": "from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and (r._field==\"pressure.measured.upstream\" or r._field==\"pressure.measured.downstream\"))\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)",
"refId": "A"
}
], ],
"title": "Pressure (measured)", "title": "Temperature",
"type": "timeseries" "type": "timeseries"
},
{ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 23 }, "id": 13, "title": "Diagnostics", "type": "row" },
{
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
"fieldConfig": { "defaults": { "thresholds": { "mode": "absolute", "steps": [{ "color": "red", "value": null }, { "color": "yellow", "value": 0.3 }, { "color": "green", "value": 0.7 }] } }, "overrides": [] },
"gridPos": { "h": 4, "w": 6, "x": 0, "y": 24 },
"id": 14,
"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==\"predictionQuality\")\n |> last()", "refId": "A" }
],
"title": "Prediction Quality",
"type": "stat"
},
{
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
"fieldConfig": { "defaults": { "thresholds": { "mode": "absolute", "steps": [{ "color": "red", "value": null }, { "color": "yellow", "value": 0.3 }, { "color": "green", "value": 0.7 }] } }, "overrides": [] },
"gridPos": { "h": 4, "w": 6, "x": 6, "y": 24 },
"id": 15,
"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==\"predictionConfidence\")\n |> last()", "refId": "A" }
],
"title": "Confidence",
"type": "stat"
},
{
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
"fieldConfig": { "defaults": { "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 1 }, { "color": "red", "value": 2 }] } }, "overrides": [] },
"gridPos": { "h": 4, "w": 6, "x": 12, "y": 24 },
"id": 16,
"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==\"pressureDriftLevel\")\n |> last()", "refId": "A" }
],
"title": "Pressure Drift",
"type": "stat"
},
{
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
"fieldConfig": { "defaults": { "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 5 }, { "color": "red", "value": 15 }] } }, "overrides": [] },
"gridPos": { "h": 4, "w": 6, "x": 18, "y": 24 },
"id": 17,
"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==\"effDistFromPeak\" or r._field==\"effRelDistFromPeak\"))\n |> last()", "refId": "A" }
],
"title": "Distance from Peak",
"type": "stat"
} }
], ],
"schemaVersion": 39, "schemaVersion": 39,
"tags": ["EVOLV", "machine", "template"], "tags": ["EVOLV", "machine", "template"],
"templating": { "templating": {
"list": [ "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": "dbase", { "name": "measurement", "type": "custom", "query": "template", "current": { "text": "template", "value": "template", "selected": false }, "options": [{ "text": "template", "value": "template", "selected": true }] },
"type": "custom", { "name": "bucket", "type": "custom", "query": "lvl2", "current": { "text": "lvl2", "value": "lvl2", "selected": false }, "options": [{ "text": "lvl2", "value": "lvl2", "selected": true }] }
"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" }, "time": { "from": "now-6h", "to": "now" },
@@ -125,4 +193,3 @@
"uid": null, "uid": null,
"version": 1 "version": 1
} }

View File

@@ -17,46 +17,78 @@
"id": null, "id": null,
"links": [], "links": [],
"panels": [ "panels": [
{ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 1, "title": "Realtime Machine Group", "type": "row" }, { "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 1, "title": "Status", "type": "row" },
{ {
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" }, "datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
"gridPos": { "h": 5, "w": 8, "x": 0, "y": 1 }, "fieldConfig": { "defaults": { "thresholds": { "mode": "absolute", "steps": [{ "color": "purple", "value": null }] } }, "overrides": [] },
"gridPos": { "h": 4, "w": 6, "x": 0, "y": 1 },
"id": 2, "id": 2,
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "none" },
"targets": [ "targets": [
{ { "query": "from(bucket: \"${bucket}\")\n |> range(start: -7d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and r._field==\"mode\")\n |> last()", "refId": "A" }
"query": "from(bucket: \"${bucket}\")\n |> range(start: -7d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and (r._field==\"mode\" or r._field==\"scaling\"))\n |> last()",
"refId": "A"
}
], ],
"title": "Mode / Scaling (last)", "title": "Mode",
"type": "stat" "type": "stat"
}, },
{ {
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" }, "datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
"gridPos": { "h": 5, "w": 8, "x": 8, "y": 1 }, "fieldConfig": { "defaults": { "thresholds": { "mode": "absolute", "steps": [{ "color": "blue", "value": null }] } }, "overrides": [] },
"gridPos": { "h": 4, "w": 6, "x": 6, "y": 1 },
"id": 3, "id": 3,
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "none" },
"targets": [ "targets": [
{ { "query": "from(bucket: \"${bucket}\")\n |> range(start: -7d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and r._field==\"scaling\")\n |> last()", "refId": "A" }
"query": "from(bucket: \"${bucket}\")\n |> range(start: -7d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and (r._field==\"NCog\" or r._field==\"absDistFromPeak\" or r._field==\"relDistFromPeak\"))\n |> last()",
"refId": "A"
}
], ],
"title": "Efficiency KPIs (last)", "title": "Scaling",
"type": "stat" "type": "stat"
}, },
{ {
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" }, "datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
"fieldConfig": { "defaults": { "unit": "none" }, "overrides": [] }, "fieldConfig": { "defaults": { "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 5 }, { "color": "red", "value": 15 }] } }, "overrides": [] },
"gridPos": { "h": 9, "w": 24, "x": 0, "y": 6 }, "gridPos": { "h": 4, "w": 6, "x": 12, "y": 1 },
"id": 4, "id": 4,
"options": { "legend": { "displayMode": "list", "placement": "bottom" } }, "options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "area" },
"targets": [ "targets": [
{ { "query": "from(bucket: \"${bucket}\")\n |> range(start: -7d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and r._field==\"absDistFromPeak\")\n |> last()", "refId": "A" }
"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_predicted_power\"))\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)",
"refId": "A"
}
], ],
"title": "Totals (flow / power)", "title": "Abs Dist Peak",
"type": "stat"
},
{
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
"fieldConfig": { "defaults": { "unit": "percent", "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 10 }, { "color": "red", "value": 25 }] } }, "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==\"relDistFromPeak\")\n |> last()", "refId": "A" }
],
"title": "Rel Dist Peak",
"type": "stat"
},
{ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 }, "id": 6, "title": "Totals", "type": "row" },
{
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
"fieldConfig": { "defaults": { "custom": { "drawStyle": "line", "lineWidth": 2, "fillOpacity": 10 } }, "overrides": [] },
"gridPos": { "h": 8, "w": 12, "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}\" and r._field =~ /predicted_flow|flow/)\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", "refId": "A" }
],
"title": "Total Flow",
"type": "timeseries"
},
{
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
"fieldConfig": { "defaults": { "custom": { "drawStyle": "line", "lineWidth": 2, "fillOpacity": 10 } }, "overrides": [] },
"gridPos": { "h": 8, "w": 12, "x": 12, "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 =~ /predicted_power|power/)\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", "refId": "A" }
],
"title": "Total Power",
"type": "timeseries" "type": "timeseries"
} }
], ],
@@ -64,29 +96,9 @@
"tags": ["EVOLV", "machineGroup", "template"], "tags": ["EVOLV", "machineGroup", "template"],
"templating": { "templating": {
"list": [ "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": "dbase", { "name": "measurement", "type": "custom", "query": "template", "current": { "text": "template", "value": "template", "selected": false }, "options": [{ "text": "template", "value": "template", "selected": true }] },
"type": "custom", { "name": "bucket", "type": "custom", "query": "lvl2", "current": { "text": "lvl2", "value": "lvl2", "selected": false }, "options": [{ "text": "lvl2", "value": "lvl2", "selected": true }] }
"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" }, "time": { "from": "now-6h", "to": "now" },
@@ -95,4 +107,3 @@
"uid": null, "uid": null,
"version": 1 "version": 1
} }

View File

@@ -17,70 +17,76 @@
"id": null, "id": null,
"links": [], "links": [],
"panels": [ "panels": [
{ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 1, "title": "Sensor", "type": "row" },
{ {
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
"id": 1, "fieldConfig": { "defaults": { "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] } }, "overrides": [] },
"title": "Realtime Sensor", "gridPos": { "h": 5, "w": 4, "x": 0, "y": 1 },
"type": "row" "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==\"mAbs\")\n |> last()", "refId": "A" }
],
"title": "mAbs (current)",
"type": "stat"
}, },
{ {
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" }, "datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
"fieldConfig": { "defaults": { "unit": "none" }, "overrides": [] }, "fieldConfig": { "defaults": { "min": 0, "max": 100, "unit": "percent", "thresholds": { "mode": "absolute", "steps": [{ "color": "red", "value": null }, { "color": "orange", "value": 10 }, { "color": "green", "value": 20 }, { "color": "yellow", "value": 80 }, { "color": "red", "value": 90 }] } }, "overrides": [] },
"gridPos": { "h": 8, "w": 16, "x": 0, "y": 1 }, "gridPos": { "h": 5, "w": 4, "x": 4, "y": 1 },
"id": 2, "id": 3,
"options": { "legend": { "displayMode": "list", "placement": "bottom" } }, "options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "showThresholdLabels": false, "showThresholdMarkers": true },
"targets": [ "targets": [
{ { "query": "from(bucket: \"${bucket}\")\n |> range(start: -7d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and r._field==\"mPercent\")\n |> last()", "refId": "A" }
"query": "from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and (r._field==\"mAbs\" or r._field==\"mPercent\"))\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)",
"refId": "A"
}
], ],
"title": "Measured Value (mAbs / mPercent)", "title": "mPercent",
"type": "gauge"
},
{
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
"fieldConfig": { "defaults": { "custom": { "drawStyle": "line", "lineWidth": 2, "fillOpacity": 10 } }, "overrides": [] },
"gridPos": { "h": 8, "w": 16, "x": 8, "y": 1 },
"id": 4,
"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==\"mAbs\" or r._field==\"mPercent\"))\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", "refId": "A" }
],
"title": "mAbs over Time",
"type": "timeseries"
},
{ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 9 }, "id": 5, "title": "Bounds", "type": "row" },
{
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
"fieldConfig": { "defaults": { "custom": { "drawStyle": "line", "lineWidth": 2, "fillOpacity": 5 } }, "overrides": [] },
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 10 },
"id": 6,
"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==\"mAbs\" or r._field==\"totalMinSmooth\" or r._field==\"totalMaxSmooth\"))\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", "refId": "A" }
],
"title": "mAbs + Smooth Bounds",
"type": "timeseries" "type": "timeseries"
}, },
{ {
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" }, "datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
"fieldConfig": { "defaults": { "unit": "none" }, "overrides": [] }, "fieldConfig": { "defaults": { "custom": { "drawStyle": "line", "lineWidth": 2, "fillOpacity": 5 } }, "overrides": [] },
"gridPos": { "h": 8, "w": 8, "x": 16, "y": 1 }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 10 },
"id": 3, "id": 7,
"options": { "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false } }, "options": { "legend": { "displayMode": "list", "placement": "bottom" }, "tooltip": { "mode": "multi" } },
"targets": [ "targets": [
{ { "query": "from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and (r._field==\"totalMinValue\" or r._field==\"totalMaxValue\"))\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", "refId": "A" }
"query": "from(bucket: \"${bucket}\")\n |> range(start: -7d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and r._field==\"mAbs\")\n |> last()",
"refId": "A"
}
], ],
"title": "mAbs (last)", "title": "Absolute Min / Max",
"type": "stat" "type": "timeseries"
} }
], ],
"schemaVersion": 39, "schemaVersion": 39,
"tags": ["EVOLV", "measurement", "template"], "tags": ["EVOLV", "measurement", "template"],
"templating": { "templating": {
"list": [ "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": "dbase", { "name": "measurement", "type": "custom", "query": "template", "current": { "text": "template", "value": "template", "selected": false }, "options": [{ "text": "template", "value": "template", "selected": true }] },
"type": "custom", { "name": "bucket", "type": "custom", "query": "lvl2", "current": { "text": "lvl2", "value": "lvl2", "selected": false }, "options": [{ "text": "lvl2", "value": "lvl2", "selected": true }] }
"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" }, "time": { "from": "now-6h", "to": "now" },
@@ -89,4 +95,3 @@
"uid": null, "uid": null,
"version": 1 "version": 1
} }

View File

@@ -17,63 +17,150 @@
"id": null, "id": null,
"links": [], "links": [],
"panels": [ "panels": [
{ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 1, "title": "Realtime Pumping Station", "type": "row" }, { "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 1, "title": "Status", "type": "row" },
{ {
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" }, "datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
"gridPos": { "h": 5, "w": 8, "x": 0, "y": 1 }, "fieldConfig": { "defaults": { "thresholds": { "mode": "absolute", "steps": [{ "color": "blue", "value": null }] } }, "overrides": [] },
"gridPos": { "h": 4, "w": 5, "x": 0, "y": 1 },
"id": 2, "id": 2,
"options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "none" },
"targets": [ "targets": [
{ { "query": "from(bucket: \"${bucket}\")\n |> range(start: -7d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and r._field==\"direction\")\n |> last()", "refId": "A" }
"query": "from(bucket: \"${bucket}\")\n |> range(start: -7d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and (r._field==\"direction\" or r._field==\"flowSource\" or r._field==\"timeleft\"))\n |> last()",
"refId": "A"
}
], ],
"title": "Direction / Source / Timeleft (last)", "title": "Direction",
"type": "stat" "type": "stat"
}, },
{ {
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" }, "datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
"fieldConfig": { "defaults": { "unit": "none" }, "overrides": [] }, "fieldConfig": { "defaults": { "unit": "s", "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "orange", "value": 300 }, { "color": "red", "value": 600 }] } }, "overrides": [] },
"gridPos": { "h": 9, "w": 16, "x": 8, "y": 1 }, "gridPos": { "h": 4, "w": 5, "x": 5, "y": 1 },
"id": 3, "id": 3,
"options": { "legend": { "displayMode": "list", "placement": "bottom" } }, "options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "area" },
"targets": [ "targets": [
{ { "query": "from(bucket: \"${bucket}\")\n |> range(start: -7d)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and r._field==\"timeleft\")\n |> last()", "refId": "A" }
"query": "from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and (r._field==\"level.measured.atequipment\" or r._field==\"level.predicted.atequipment\" or r._field==\"volume.predicted.atequipment\" or r._field==\"netFlowRate.predicted.atequipment\"))\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)",
"refId": "A"
}
], ],
"title": "Level / Volume / Net Flow", "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" "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, "schemaVersion": 39,
"tags": ["EVOLV", "pumpingStation", "template"], "tags": ["EVOLV", "pumpingStation", "template"],
"templating": { "templating": {
"list": [ "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": "dbase", { "name": "measurement", "type": "custom", "query": "template", "current": { "text": "template", "value": "template", "selected": false }, "options": [{ "text": "template", "value": "template", "selected": true }] },
"type": "custom", { "name": "bucket", "type": "custom", "query": "lvl2", "current": { "text": "lvl2", "value": "lvl2", "selected": false }, "options": [{ "text": "lvl2", "value": "lvl2", "selected": true }] }
"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" }, "time": { "from": "now-6h", "to": "now" },
@@ -82,4 +169,3 @@
"uid": null, "uid": null,
"version": 1 "version": 1
} }

View File

@@ -17,19 +17,66 @@
"id": null, "id": null,
"links": [], "links": [],
"panels": [ "panels": [
{ "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 1, "title": "Reactor (Simulation/Process)", "type": "row" }, { "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 1, "title": "Status", "type": "row" },
{ {
"datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" }, "datasource": { "type": "influxdb", "uid": "cdzg44tv250jkd" },
"gridPos": { "h": 9, "w": 24, "x": 0, "y": 1 }, "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, "id": 2,
"options": { "legend": { "displayMode": "list", "placement": "bottom" } }, "options": { "reduceOptions": { "calcs": ["lastNotNull"] }, "colorMode": "value", "graphMode": "area" },
"targets": [ "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" }
"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|temperature)/)\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)",
"refId": "A"
}
], ],
"title": "Core Process Signals (if logged)", "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" "type": "timeseries"
} }
], ],
@@ -37,29 +84,9 @@
"tags": ["EVOLV", "reactor", "template"], "tags": ["EVOLV", "reactor", "template"],
"templating": { "templating": {
"list": [ "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": "dbase", { "name": "measurement", "type": "custom", "query": "template", "current": { "text": "template", "value": "template", "selected": false }, "options": [{ "text": "template", "value": "template", "selected": true }] },
"type": "custom", { "name": "bucket", "type": "custom", "query": "lvl2", "current": { "text": "lvl2", "value": "lvl2", "selected": false }, "options": [{ "text": "lvl2", "value": "lvl2", "selected": true }] }
"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" }, "time": { "from": "now-6h", "to": "now" },
@@ -68,4 +95,3 @@
"uid": null, "uid": null,
"version": 1 "version": 1
} }

View File

@@ -39,7 +39,7 @@
"options": { "legend": { "displayMode": "list", "placement": "bottom" } }, "options": { "legend": { "displayMode": "list", "placement": "bottom" } },
"targets": [ "targets": [
{ {
"query": "from(bucket: \"${bucket}\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn:(r) => r._measurement==\"${measurement}\" and (r._field==\"atequipment_measured_flow\" or r._field==\"atequipment_predicted_flow\" or r._field==\"maxDeltaP\"))\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", "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" "refId": "A"
} }
], ],

View File

@@ -14,6 +14,7 @@
host: { value: 'localhost' }, host: { value: 'localhost' },
port: { value: 3000 }, port: { value: 3000 },
bearerToken: { value: '' }, bearerToken: { value: '' },
defaultBucket: { value: '' },
}, },
inputs: 1, inputs: 1,
outputs: 1, outputs: 1,
@@ -43,7 +44,7 @@
window.EVOLV.nodes.dashboardapi.loggerMenu.saveEditor(node); window.EVOLV.nodes.dashboardapi.loggerMenu.saveEditor(node);
} }
['name', 'protocol', 'host', 'port', 'bearerToken'].forEach((field) => { ['name', 'protocol', 'host', 'port', 'bearerToken', 'defaultBucket'].forEach((field) => {
const element = document.getElementById(`node-input-${field}`); const element = document.getElementById(`node-input-${field}`);
if (!element) return; if (!element) return;
node[field] = field === 'port' ? parseInt(element.value, 10) || 3000 : element.value || ''; node[field] = field === 'port' ? parseInt(element.value, 10) || 3000 : element.value || '';
@@ -82,6 +83,11 @@
<input type="password" id="node-input-bearerToken" placeholder="optional" style="width:70%;" /> <input type="password" id="node-input-bearerToken" placeholder="optional" style="width:70%;" />
</div> </div>
<div class="form-row">
<label for="node-input-defaultBucket"><i class="fa fa-database"></i> InfluxDB Bucket</label>
<input type="text" id="node-input-defaultBucket" placeholder="env INFLUXDB_BUCKET or lvl2" style="width:70%;" />
</div>
<div id="logger-fields-placeholder"></div> <div id="logger-fields-placeholder"></div>
</script> </script>

View File

@@ -1,10 +1,6 @@
const { outputUtils } = require('generalFunctions'); const { configManager } = require('generalFunctions');
const Specific = require('./specificClass'); const DashboardApi = require('./specificClass');
/**
* Node-RED wrapper for dashboard generation requests.
* It listens for `registerChild` messages and emits Grafana upsert requests.
*/
class nodeClass { class nodeClass {
constructor(uiConfig, RED, nodeInstance, nameOfNode) { constructor(uiConfig, RED, nodeInstance, nameOfNode) {
this.node = nodeInstance; this.node = nodeInstance;
@@ -20,13 +16,11 @@ class nodeClass {
} }
_loadConfig(uiConfig) { _loadConfig(uiConfig) {
this.config = { const cfgMgr = new configManager();
general: { this.config = cfgMgr.buildConfig(this.name, uiConfig, this.node.id, {
name: uiConfig.name || this.name, functionality: {
logging: { softwareType: this.name.toLowerCase(),
enabled: uiConfig.enableLog, role: 'auto ui generator',
logLevel: uiConfig.logLevel || 'info',
},
}, },
grafanaConnector: { grafanaConnector: {
protocol: uiConfig.protocol || 'http', protocol: uiConfig.protocol || 'http',
@@ -34,16 +28,45 @@ class nodeClass {
port: Number(uiConfig.port || 3000), port: Number(uiConfig.port || 3000),
bearerToken: uiConfig.bearerToken || '', bearerToken: uiConfig.bearerToken || '',
}, },
}; defaultBucket: uiConfig.defaultBucket || process.env.INFLUXDB_BUCKET || '',
});
this._output = new outputUtils();
} }
_setupSpecificClass() { _setupSpecificClass() {
this.source = new Specific(this.config); this.source = new DashboardApi(this.config);
this.node.source = this.source; this.node.source = this.source;
} }
_resolveChildNode(childId) {
const runtimeNode = this.RED.nodes.getNode(childId);
if (runtimeNode?.source?.config) {
return runtimeNode;
}
const flowNode = this.node._flow?.getNode?.(childId);
if (flowNode?.source?.config) {
return flowNode;
}
return runtimeNode || flowNode || null;
}
_resolveChildSource(payload) {
if (payload?.source?.config) {
return payload.source;
}
if (payload?.config) {
return { config: payload.config };
}
if (typeof payload === 'string') {
return this._resolveChildNode(payload)?.source || null;
}
return null;
}
_attachInputHandler() { _attachInputHandler() {
this.node.on('input', async (msg, send, done) => { this.node.on('input', async (msg, send, done) => {
try { try {
@@ -52,16 +75,11 @@ class nodeClass {
return; return;
} }
const childId = msg.payload; const childSource = this._resolveChildSource(msg.payload);
const childObj = this.RED.nodes.getNode(childId);
const childSource = childObj?.source;
if (!childSource?.config) { if (!childSource?.config) {
this.node.warn(`registerChild skipped: missing child source/config for id=${childId}`); throw new Error('Missing or invalid child node');
if (typeof done === 'function') done();
return;
} }
// Generate one dashboard for the root source and optionally its registered children.
const dashboards = this.source.generateDashboardsForGraph(childSource, { const dashboards = this.source.generateDashboardsForGraph(childSource, {
includeChildren: Boolean(msg.includeChildren ?? true), includeChildren: Boolean(msg.includeChildren ?? true),
}); });
@@ -71,19 +89,23 @@ class nodeClass {
Accept: 'application/json', Accept: 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}; };
if (this.config.grafanaConnector.bearerToken) { if (this.config.grafanaConnector.bearerToken) {
headers.Authorization = `Bearer ${this.config.grafanaConnector.bearerToken}`; headers.Authorization = `Bearer ${this.config.grafanaConnector.bearerToken}`;
} }
for (const dash of dashboards) { for (const dash of dashboards) {
// Forward dashboard definitions to an HTTP request node configured for Grafana API.
const payload = this.source.buildUpsertRequest({ dashboard: dash.dashboard, folderId: 0, overwrite: true });
send({ send({
topic: 'grafana.dashboard.upsert', ...msg,
topic: 'create',
url, url,
method: 'POST', method: 'POST',
headers, headers,
payload, payload: this.source.buildUpsertRequest({
dashboard: dash.dashboard,
folderId: 0,
overwrite: true,
}),
meta: { meta: {
nodeId: dash.nodeId, nodeId: dash.nodeId,
softwareType: dash.softwareType, softwareType: dash.softwareType,
@@ -97,7 +119,7 @@ class nodeClass {
} catch (error) { } catch (error) {
this.node.status({ fill: 'red', shape: 'ring', text: 'dashboardapi error' }); this.node.status({ fill: 'red', shape: 'ring', text: 'dashboardapi error' });
this.node.error(error?.message || error, msg); this.node.error(error?.message || error, msg);
if (typeof done === 'function') done(); if (typeof done === 'function') done(error);
} }
}); });
} }

View File

@@ -65,6 +65,7 @@ class DashboardApi {
port: Number(config?.grafanaConnector?.port || 3000), port: Number(config?.grafanaConnector?.port || 3000),
bearerToken: config?.grafanaConnector?.bearerToken || '', bearerToken: config?.grafanaConnector?.bearerToken || '',
}, },
defaultBucket: config?.defaultBucket || '',
bucketMap: config?.bucketMap || {}, bucketMap: config?.bucketMap || {},
}; };
@@ -133,7 +134,9 @@ class DashboardApi {
); );
const bucket = const bucket =
this.config.bucketMap[String(positionVsParent)] || defaultBucketForPosition(positionVsParent); this.config.defaultBucket ||
this.config.bucketMap[String(positionVsParent)] ||
defaultBucketForPosition(positionVsParent);
updateTemplatingVar(dashboard, 'measurement', measurementName); updateTemplatingVar(dashboard, 'measurement', measurementName);
updateTemplatingVar(dashboard, 'bucket', bucket); updateTemplatingVar(dashboard, 'bucket', bucket);

View File

@@ -1,8 +1,7 @@
const test = require('node:test'); describe('dashboardAPI basic structure', () => {
const assert = require('node:assert/strict'); it('module load smoke', () => {
expect(() => {
test('dashboardAPI module load smoke', () => {
assert.doesNotThrow(() => {
require('../../dashboardapi.js'); require('../../dashboardapi.js');
}).not.toThrow();
}); });
}); });

View File

@@ -1,6 +1,3 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const DashboardApi = require('../src/specificClass'); const DashboardApi = require('../src/specificClass');
function makeNodeSource({ id, name, softwareType, positionVsParent, children = [] }) { function makeNodeSource({ id, name, softwareType, positionVsParent, children = [] }) {
@@ -24,7 +21,8 @@ function makeNodeSource({ id, name, softwareType, positionVsParent, children = [
}; };
} }
test('buildDashboard sets id=null, stable uid, title, measurement and bucket vars', () => { describe('DashboardApi specificClass', () => {
it('buildDashboard sets id=null, stable uid, title, measurement and bucket vars', () => {
const api = new DashboardApi({ const api = new DashboardApi({
general: { name: 'dashboardapi-test', logging: { enabled: false, logLevel: 'error' } }, general: { name: 'dashboardapi-test', logging: { enabled: false, logLevel: 'error' } },
grafanaConnector: { protocol: 'http', host: 'localhost', port: 3000, bearerToken: '' }, grafanaConnector: { protocol: 'http', host: 'localhost', port: 3000, bearerToken: '' },
@@ -39,20 +37,20 @@ test('buildDashboard sets id=null, stable uid, title, measurement and bucket var
const dash = api.buildDashboard({ nodeConfig: nodeSource.config, positionVsParent: 'downstream' }); const dash = api.buildDashboard({ nodeConfig: nodeSource.config, positionVsParent: 'downstream' });
assert.equal(dash.dashboard.id, null); expect(dash.dashboard.id).toBeNull();
assert.equal(dash.uid.length, 12); expect(dash.uid).toHaveLength(12);
assert.equal(dash.dashboard.uid, dash.uid); expect(dash.dashboard.uid).toBe(dash.uid);
assert.equal(dash.dashboard.title, 'PT-1'); expect(dash.dashboard.title).toBe('PT-1');
const templ = dash.dashboard.templating.list; const templ = dash.dashboard.templating.list;
const measurement = templ.find((v) => v.name === 'measurement'); const measurement = templ.find((v) => v.name === 'measurement');
const bucket = templ.find((v) => v.name === 'bucket'); const bucket = templ.find((v) => v.name === 'bucket');
assert.equal(measurement.current.value, 'measurement_m-1'); expect(measurement.current.value).toBe('measurement_m-1');
assert.equal(bucket.current.value, 'lvl3'); expect(bucket.current.value).toBe('lvl3');
}); });
test('generateDashboardsForGraph returns root + direct child dashboards and adds links', () => { it('generateDashboardsForGraph returns root + direct child dashboards and adds links', () => {
const api = new DashboardApi({ const api = new DashboardApi({
general: { name: 'dashboardapi-test', logging: { enabled: false, logLevel: 'error' } }, general: { name: 'dashboardapi-test', logging: { enabled: false, logLevel: 'error' } },
grafanaConnector: { protocol: 'http', host: 'localhost', port: 3000, bearerToken: '' }, grafanaConnector: { protocol: 'http', host: 'localhost', port: 3000, bearerToken: '' },
@@ -74,9 +72,10 @@ test('generateDashboardsForGraph returns root + direct child dashboards and adds
}); });
const results = api.generateDashboardsForGraph(root, { includeChildren: true }); const results = api.generateDashboardsForGraph(root, { includeChildren: true });
assert.equal(results.length, 2); expect(results).toHaveLength(2);
const rootDash = results[0]; const rootDash = results[0];
assert.ok(Array.isArray(rootDash.dashboard.links)); expect(Array.isArray(rootDash.dashboard.links)).toBe(true);
assert.ok(rootDash.dashboard.links.some((l) => l.url && l.url.includes('/d/'))); expect(rootDash.dashboard.links.some((l) => l.url && l.url.includes('/d/'))).toBe(true);
});
}); });

View File

@@ -1,11 +1,11 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs'); const fs = require('node:fs');
const path = require('node:path'); const path = require('node:path');
const flow = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../../examples/basic.flow.json'), 'utf8')); const flow = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../../examples/basic.flow.json'), 'utf8'));
test('basic example includes node type dashboardapi', () => { describe('dashboardAPI edge example structure', () => {
it('basic example includes node type dashboardapi', () => {
const count = flow.filter((n) => n && n.type === 'dashboardapi').length; const count = flow.filter((n) => n && n.type === 'dashboardapi').length;
assert.equal(count >= 1, true); expect(count).toBeGreaterThanOrEqual(1);
});
}); });

View File

@@ -1,5 +1,3 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs'); const fs = require('node:fs');
const path = require('node:path'); const path = require('node:path');
@@ -9,15 +7,17 @@ function loadJson(file) {
return JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8')); return JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
} }
test('examples package exists for dashboardAPI', () => { describe('dashboardAPI integration examples', () => {
it('examples package exists for dashboardAPI', () => {
for (const file of ['README.md', 'basic.flow.json', 'integration.flow.json', 'edge.flow.json']) { for (const file of ['README.md', 'basic.flow.json', 'integration.flow.json', 'edge.flow.json']) {
assert.equal(fs.existsSync(path.join(dir, file)), true, file + ' missing'); expect(fs.existsSync(path.join(dir, file))).toBe(true);
} }
}); });
test('example flows are parseable arrays for dashboardAPI', () => { it('example flows are parseable arrays for dashboardAPI', () => {
for (const file of ['basic.flow.json', 'integration.flow.json', 'edge.flow.json']) { for (const file of ['basic.flow.json', 'integration.flow.json', 'edge.flow.json']) {
const parsed = loadJson(file); const parsed = loadJson(file);
assert.equal(Array.isArray(parsed), true); expect(Array.isArray(parsed)).toBe(true);
} }
});
}); });

179
test/nodeClass.test.js Normal file
View 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();
});
});