Compare commits
10 Commits
3529c9f970
...
slice/43-o
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de957cb971 | ||
|
|
533f74fe7e | ||
|
|
a16f526964 | ||
|
|
8afc6b9779 | ||
|
|
193f913eb1 | ||
|
|
41a20d4679 | ||
|
|
8a26e17780 | ||
|
|
3cd749bf37 | ||
|
|
70151e52ec | ||
|
|
b3972d4a2f |
@@ -171,7 +171,7 @@
|
|||||||
},
|
},
|
||||||
"overrides": []
|
"overrides": []
|
||||||
},
|
},
|
||||||
"gridPos": { "h": 10, "w": 4, "x": 0, "y": 6 },
|
"gridPos": { "h": 20, "w": 4, "x": 0, "y": 6 },
|
||||||
"id": 16,
|
"id": 16,
|
||||||
"options": {
|
"options": {
|
||||||
"displayMode": "basic",
|
"displayMode": "basic",
|
||||||
@@ -205,19 +205,19 @@
|
|||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"matcher": { "id": "byRegexp", "options": "^(outflowLevel|inflowLevel|overflowLevel|heightBasin|dryRunLevel|highVolumeSafetyLevel|level)$" },
|
"matcher": { "id": "byRegexp", "options": "^(outflowLevel|inflowLevel|overflowLevel|heightBasin|dryRunLevel|highVolumeSafetyLevel|level)$" },
|
||||||
"properties": [{ "id": "unit", "value": "lengthm" }]
|
"properties": [{ "id": "unit", "value": "lengthm" }, { "id": "decimals", "value": 2 }]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matcher": { "id": "byRegexp", "options": "^(volume|maxVol|minVol|maxVolAtOverflow|minVolAtOutflow|minVolAtInflow)$" },
|
"matcher": { "id": "byRegexp", "options": "^(volume|maxVol|minVol|maxVolAtOverflow|minVolAtOutflow|minVolAtInflow)$" },
|
||||||
"properties": [{ "id": "unit", "value": "m3" }]
|
"properties": [{ "id": "unit", "value": "m3" }, { "id": "decimals", "value": 2 }]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matcher": { "id": "byRegexp", "options": "Percent$" },
|
"matcher": { "id": "byRegexp", "options": "^volumePercent$" },
|
||||||
"properties": [{ "id": "unit", "value": "percent" }]
|
"properties": [{ "id": "unit", "value": "percent" }, { "id": "decimals", "value": 1 }]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"gridPos": { "h": 10, "w": 8, "x": 4, "y": 6 },
|
"gridPos": { "h": 20, "w": 6, "x": 4, "y": 6 },
|
||||||
"id": 17,
|
"id": 17,
|
||||||
"options": {
|
"options": {
|
||||||
"inlineEditing": false,
|
"inlineEditing": false,
|
||||||
@@ -227,14 +227,15 @@
|
|||||||
"root": {
|
"root": {
|
||||||
"name": "Basin",
|
"name": "Basin",
|
||||||
"type": "frame",
|
"type": "frame",
|
||||||
"placement": { "left": 0, "top": 0, "width": 480, "height": 300 },
|
"placement": { "left": 0, "top": 0, "right": 0, "bottom": 0 },
|
||||||
"background": { "color": { "fixed": "transparent" } },
|
"background": { "color": { "fixed": "transparent" } },
|
||||||
"border": { "color": { "fixed": "dark-green" } },
|
"border": { "color": { "fixed": "dark-green" } },
|
||||||
"elements": [
|
"elements": [
|
||||||
{
|
{
|
||||||
"name": "Zone Spill",
|
"name": "Zone Spill",
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"placement": { "top": 20, "left": 80, "width": 200, "height": {{h_spill}} },
|
"constraint": { "horizontal": "scale", "vertical": "scale" },
|
||||||
|
"placement": { "top": 6.32, "left": 2.5, "right": 2.5, "bottom": {{zb_spill}} },
|
||||||
"background": { "color": { "fixed": "rgba(229, 67, 67, 0.18)" } },
|
"background": { "color": { "fixed": "rgba(229, 67, 67, 0.18)" } },
|
||||||
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
||||||
"config": { "text": { "mode": "fixed", "fixed": "" } }
|
"config": { "text": { "mode": "fixed", "fixed": "" } }
|
||||||
@@ -242,7 +243,8 @@
|
|||||||
{
|
{
|
||||||
"name": "Zone HighSafety",
|
"name": "Zone HighSafety",
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"placement": { "top": {{y_overflow}}, "left": 80, "width": 200, "height": {{h_highSafety}} },
|
"constraint": { "horizontal": "scale", "vertical": "scale" },
|
||||||
|
"placement": { "top": {{y_overflow}}, "left": 2.5, "right": 2.5, "bottom": {{zb_highSafety}} },
|
||||||
"background": { "color": { "fixed": "rgba(242, 165, 67, 0.16)" } },
|
"background": { "color": { "fixed": "rgba(242, 165, 67, 0.16)" } },
|
||||||
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
||||||
"config": { "text": { "mode": "fixed", "fixed": "" } }
|
"config": { "text": { "mode": "fixed", "fixed": "" } }
|
||||||
@@ -250,7 +252,8 @@
|
|||||||
{
|
{
|
||||||
"name": "Zone Operating",
|
"name": "Zone Operating",
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"placement": { "top": {{y_highSafety}}, "left": 80, "width": 200, "height": {{h_operating}} },
|
"constraint": { "horizontal": "scale", "vertical": "scale" },
|
||||||
|
"placement": { "top": {{y_highSafety}}, "left": 2.5, "right": 2.5, "bottom": {{zb_operating}} },
|
||||||
"background": { "color": { "fixed": "rgba(95, 179, 122, 0.14)" } },
|
"background": { "color": { "fixed": "rgba(95, 179, 122, 0.14)" } },
|
||||||
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
||||||
"config": { "text": { "mode": "fixed", "fixed": "" } }
|
"config": { "text": { "mode": "fixed", "fixed": "" } }
|
||||||
@@ -258,7 +261,8 @@
|
|||||||
{
|
{
|
||||||
"name": "Zone Dead",
|
"name": "Zone Dead",
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"placement": { "top": {{y_outflow}}, "left": 80, "width": 200, "height": {{h_dead}} },
|
"constraint": { "horizontal": "scale", "vertical": "scale" },
|
||||||
|
"placement": { "top": {{y_outflow}}, "left": 2.5, "right": 2.5, "bottom": {{zb_dead}} },
|
||||||
"background": { "color": { "fixed": "rgba(128, 128, 128, 0.20)" } },
|
"background": { "color": { "fixed": "rgba(128, 128, 128, 0.20)" } },
|
||||||
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
||||||
"config": { "text": { "mode": "fixed", "fixed": "" } }
|
"config": { "text": { "mode": "fixed", "fixed": "" } }
|
||||||
@@ -266,7 +270,8 @@
|
|||||||
{
|
{
|
||||||
"name": "Tank Outline",
|
"name": "Tank Outline",
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"placement": { "top": 20, "left": 80, "width": 200, "height": 240 },
|
"constraint": { "horizontal": "scale", "vertical": "scale" },
|
||||||
|
"placement": { "top": 6.32, "left": 2.5, "right": 2.5, "bottom": 6.32 },
|
||||||
"background": { "color": { "fixed": "transparent" } },
|
"background": { "color": { "fixed": "transparent" } },
|
||||||
"border": { "color": { "fixed": "#8a8a8a" }, "width": 2 },
|
"border": { "color": { "fixed": "#8a8a8a" }, "width": 2 },
|
||||||
"config": { "text": { "mode": "fixed", "fixed": "" } }
|
"config": { "text": { "mode": "fixed", "fixed": "" } }
|
||||||
@@ -274,181 +279,150 @@
|
|||||||
{
|
{
|
||||||
"name": "Line Overflow",
|
"name": "Line Overflow",
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"placement": { "top": {{y_overflow}}, "left": 80, "width": 200, "height": 1 },
|
"constraint": { "horizontal": "scale", "vertical": "scale" },
|
||||||
|
"placement": { "top": {{y_overflow}}, "left": 2.5, "right": 2.5, "bottom": {{yb_overflow}} },
|
||||||
"background": { "color": { "fixed": "#e54343" } },
|
"background": { "color": { "fixed": "#e54343" } },
|
||||||
"border": { "color": { "fixed": "#e54343" }, "width": 0 }
|
"border": { "color": { "fixed": "#e54343" }, "width": 0 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Line HighSafety",
|
"name": "Line HighSafety",
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"placement": { "top": {{y_highSafety}}, "left": 80, "width": 200, "height": 1 },
|
"constraint": { "horizontal": "scale", "vertical": "scale" },
|
||||||
|
"placement": { "top": {{y_highSafety}}, "left": 2.5, "right": 2.5, "bottom": {{yb_highSafety}} },
|
||||||
"background": { "color": { "fixed": "#f2a543" } },
|
"background": { "color": { "fixed": "#f2a543" } },
|
||||||
"border": { "color": { "fixed": "#f2a543" }, "width": 0 }
|
"border": { "color": { "fixed": "#f2a543" }, "width": 0 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Line Inflow",
|
"name": "Line Inflow",
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"placement": { "top": {{y_inflow}}, "left": 80, "width": 200, "height": 1 },
|
"constraint": { "horizontal": "scale", "vertical": "scale" },
|
||||||
|
"placement": { "top": {{y_inflow}}, "left": 2.5, "right": 2.5, "bottom": {{yb_inflow}} },
|
||||||
"background": { "color": { "fixed": "#5fb37a" } },
|
"background": { "color": { "fixed": "#5fb37a" } },
|
||||||
"border": { "color": { "fixed": "#5fb37a" }, "width": 0 }
|
"border": { "color": { "fixed": "#5fb37a" }, "width": 0 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Line DryRun",
|
"name": "Line DryRun",
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"placement": { "top": {{y_dryRun}}, "left": 80, "width": 200, "height": 1 },
|
"constraint": { "horizontal": "scale", "vertical": "scale" },
|
||||||
|
"placement": { "top": {{y_dryRun}}, "left": 2.5, "right": 2.5, "bottom": {{yb_dryRun}} },
|
||||||
"background": { "color": { "fixed": "#5b9bd5" } },
|
"background": { "color": { "fixed": "#5b9bd5" } },
|
||||||
"border": { "color": { "fixed": "#5b9bd5" }, "width": 0 }
|
"border": { "color": { "fixed": "#5b9bd5" }, "width": 0 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Line Outflow",
|
"name": "Line Outflow",
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"placement": { "top": {{y_outflow}}, "left": 80, "width": 200, "height": 1 },
|
"constraint": { "horizontal": "scale", "vertical": "scale" },
|
||||||
|
"placement": { "top": {{y_outflow}}, "left": 2.5, "right": 2.5, "bottom": {{yb_outflow}} },
|
||||||
"background": { "color": { "fixed": "#bfbfbf" } },
|
"background": { "color": { "fixed": "#bfbfbf" } },
|
||||||
"border": { "color": { "fixed": "#bfbfbf" }, "width": 0 }
|
"border": { "color": { "fixed": "#bfbfbf" }, "width": 0 }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Label Overflow Name",
|
"name": "Label Overflow Name",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placement": { "top": {{ty_overflow}}, "left": 4, "width": 76, "height": 16 },
|
"constraint": { "horizontal": "scale", "vertical": "scale" },
|
||||||
|
"placement": { "top": {{ty_overflow}}, "left": 15, "right": 53, "bottom": {{tyb_overflow}} },
|
||||||
"background": { "color": { "fixed": "transparent" } },
|
"background": { "color": { "fixed": "transparent" } },
|
||||||
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
||||||
"config": { "text": { "mode": "fixed", "fixed": "overflowLevel" }, "color": { "fixed": "#e54343" }, "size": 11, "align": "right", "valign": "middle" }
|
"config": { "text": { "mode": "fixed", "fixed": "overflowLevel" }, "color": { "fixed": "#c92020" }, "size": 14, "align": "right", "valign": "middle" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Label HighSafety Name",
|
"name": "Label HighSafety Name",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placement": { "top": {{ty_highSafety}}, "left": 0, "width": 80, "height": 16 },
|
"constraint": { "horizontal": "scale", "vertical": "scale" },
|
||||||
|
"placement": { "top": {{ty_highSafety}}, "left": 15, "right": 53, "bottom": {{tyb_highSafety}} },
|
||||||
"background": { "color": { "fixed": "transparent" } },
|
"background": { "color": { "fixed": "transparent" } },
|
||||||
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
||||||
"config": { "text": { "mode": "fixed", "fixed": "highSafe" }, "color": { "fixed": "#f2a543" }, "size": 11, "align": "right", "valign": "middle" }
|
"config": { "text": { "mode": "fixed", "fixed": "highSafety" }, "color": { "fixed": "#cf7e20" }, "size": 14, "align": "right", "valign": "middle" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Label Inflow Name",
|
"name": "Label Inflow Name",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placement": { "top": {{ty_inflow}}, "left": 4, "width": 76, "height": 16 },
|
"constraint": { "horizontal": "scale", "vertical": "scale" },
|
||||||
|
"placement": { "top": {{ty_inflow}}, "left": 15, "right": 53, "bottom": {{tyb_inflow}} },
|
||||||
"background": { "color": { "fixed": "transparent" } },
|
"background": { "color": { "fixed": "transparent" } },
|
||||||
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
||||||
"config": { "text": { "mode": "fixed", "fixed": "inflowLevel" }, "color": { "fixed": "#5fb37a" }, "size": 11, "align": "right", "valign": "middle" }
|
"config": { "text": { "mode": "fixed", "fixed": "inflowLevel" }, "color": { "fixed": "#3d8a5a" }, "size": 14, "align": "right", "valign": "middle" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Label DryRun Name",
|
"name": "Label DryRun Name",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placement": { "top": {{ty_dryRun}}, "left": 4, "width": 76, "height": 16 },
|
"constraint": { "horizontal": "scale", "vertical": "scale" },
|
||||||
|
"placement": { "top": {{ty_dryRun}}, "left": 15, "right": 53, "bottom": {{tyb_dryRun}} },
|
||||||
"background": { "color": { "fixed": "transparent" } },
|
"background": { "color": { "fixed": "transparent" } },
|
||||||
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
||||||
"config": { "text": { "mode": "fixed", "fixed": "dryRunLevel" }, "color": { "fixed": "#5b9bd5" }, "size": 11, "align": "right", "valign": "middle" }
|
"config": { "text": { "mode": "fixed", "fixed": "dryRunLevel" }, "color": { "fixed": "#3a76a8" }, "size": 14, "align": "right", "valign": "middle" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Label Outflow Name",
|
"name": "Label Outflow Name",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placement": { "top": {{ty_outflow}}, "left": 4, "width": 76, "height": 16 },
|
"constraint": { "horizontal": "scale", "vertical": "scale" },
|
||||||
|
"placement": { "top": {{ty_outflow}}, "left": 15, "right": 53, "bottom": {{tyb_outflow}} },
|
||||||
"background": { "color": { "fixed": "transparent" } },
|
"background": { "color": { "fixed": "transparent" } },
|
||||||
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
||||||
"config": { "text": { "mode": "fixed", "fixed": "outflowLevel" }, "color": { "fixed": "#bfbfbf" }, "size": 11, "align": "right", "valign": "middle" }
|
"config": { "text": { "mode": "fixed", "fixed": "outflowLevel" }, "color": { "fixed": "#6a6a6a" }, "size": 14, "align": "right", "valign": "middle" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Value Overflow",
|
"name": "Value Overflow",
|
||||||
"type": "metric-value",
|
"type": "metric-value",
|
||||||
"placement": { "top": {{ty_overflow}}, "left": 286, "width": 90, "height": 16 },
|
"constraint": { "horizontal": "scale", "vertical": "scale" },
|
||||||
|
"placement": { "top": {{ty_overflow}}, "left": 53, "right": 12, "bottom": {{tyb_overflow}} },
|
||||||
"background": { "color": { "fixed": "transparent" } },
|
"background": { "color": { "fixed": "transparent" } },
|
||||||
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
||||||
"config": { "text": { "mode": "field", "fixed": "", "field": "overflowLevel" }, "color": { "fixed": "#e54343" }, "size": 11, "align": "left", "valign": "middle" }
|
"config": { "text": { "mode": "field", "fixed": "", "field": "overflowLevel" }, "color": { "fixed": "#c92020" }, "size": 14, "align": "left", "valign": "middle" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Value HighSafety",
|
"name": "Value HighSafety",
|
||||||
"type": "metric-value",
|
"type": "metric-value",
|
||||||
"placement": { "top": {{ty_highSafety}}, "left": 286, "width": 90, "height": 16 },
|
"constraint": { "horizontal": "scale", "vertical": "scale" },
|
||||||
|
"placement": { "top": {{ty_highSafety}}, "left": 53, "right": 12, "bottom": {{tyb_highSafety}} },
|
||||||
"background": { "color": { "fixed": "transparent" } },
|
"background": { "color": { "fixed": "transparent" } },
|
||||||
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
||||||
"config": { "text": { "mode": "field", "fixed": "", "field": "highVolumeSafetyLevel" }, "color": { "fixed": "#f2a543" }, "size": 11, "align": "left", "valign": "middle" }
|
"config": { "text": { "mode": "field", "fixed": "", "field": "highVolumeSafetyLevel" }, "color": { "fixed": "#cf7e20" }, "size": 14, "align": "left", "valign": "middle" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Value Inflow",
|
"name": "Value Inflow",
|
||||||
"type": "metric-value",
|
"type": "metric-value",
|
||||||
"placement": { "top": {{ty_inflow}}, "left": 286, "width": 90, "height": 16 },
|
"constraint": { "horizontal": "scale", "vertical": "scale" },
|
||||||
|
"placement": { "top": {{ty_inflow}}, "left": 53, "right": 12, "bottom": {{tyb_inflow}} },
|
||||||
"background": { "color": { "fixed": "transparent" } },
|
"background": { "color": { "fixed": "transparent" } },
|
||||||
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
||||||
"config": { "text": { "mode": "field", "fixed": "", "field": "inflowLevel" }, "color": { "fixed": "#5fb37a" }, "size": 11, "align": "left", "valign": "middle" }
|
"config": { "text": { "mode": "field", "fixed": "", "field": "inflowLevel" }, "color": { "fixed": "#3d8a5a" }, "size": 14, "align": "left", "valign": "middle" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Value DryRun",
|
"name": "Value DryRun",
|
||||||
"type": "metric-value",
|
"type": "metric-value",
|
||||||
"placement": { "top": {{ty_dryRun}}, "left": 286, "width": 90, "height": 16 },
|
"constraint": { "horizontal": "scale", "vertical": "scale" },
|
||||||
|
"placement": { "top": {{ty_dryRun}}, "left": 53, "right": 12, "bottom": {{tyb_dryRun}} },
|
||||||
"background": { "color": { "fixed": "transparent" } },
|
"background": { "color": { "fixed": "transparent" } },
|
||||||
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
||||||
"config": { "text": { "mode": "field", "fixed": "", "field": "dryRunLevel" }, "color": { "fixed": "#5b9bd5" }, "size": 11, "align": "left", "valign": "middle" }
|
"config": { "text": { "mode": "field", "fixed": "", "field": "dryRunLevel" }, "color": { "fixed": "#3a76a8" }, "size": 14, "align": "left", "valign": "middle" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Value Outflow",
|
"name": "Value Outflow",
|
||||||
"type": "metric-value",
|
"type": "metric-value",
|
||||||
"placement": { "top": {{ty_outflow}}, "left": 286, "width": 90, "height": 16 },
|
"constraint": { "horizontal": "scale", "vertical": "scale" },
|
||||||
|
"placement": { "top": {{ty_outflow}}, "left": 53, "right": 12, "bottom": {{tyb_outflow}} },
|
||||||
"background": { "color": { "fixed": "transparent" } },
|
"background": { "color": { "fixed": "transparent" } },
|
||||||
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
||||||
"config": { "text": { "mode": "field", "fixed": "", "field": "outflowLevel" }, "color": { "fixed": "#bfbfbf" }, "size": 11, "align": "left", "valign": "middle" }
|
"config": { "text": { "mode": "field", "fixed": "", "field": "outflowLevel" }, "color": { "fixed": "#6a6a6a" }, "size": 14, "align": "left", "valign": "middle" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Header Rim",
|
"name": "Header Rim",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placement": { "top": 2, "left": 80, "width": 200, "height": 16 },
|
"constraint": { "horizontal": "scale", "vertical": "scale" },
|
||||||
|
"placement": { "top": 1, "left": 2.5, "right": 2.5, "bottom": 95 },
|
||||||
"background": { "color": { "fixed": "transparent" } },
|
"background": { "color": { "fixed": "transparent" } },
|
||||||
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
||||||
"config": { "text": { "mode": "fixed", "fixed": "rim ({{heightBasin}} m)" }, "color": { "fixed": "#8a8a8a" }, "size": 10, "align": "center", "valign": "middle" }
|
"config": { "text": { "mode": "fixed", "fixed": "rim ({{heightBasin}} m)" }, "color": { "fixed": "#6a6a6a" }, "size": 14, "align": "center", "valign": "middle" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Footer Floor",
|
"name": "Footer Floor",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"placement": { "top": 262, "left": 80, "width": 200, "height": 16 },
|
"constraint": { "horizontal": "scale", "vertical": "scale" },
|
||||||
|
"placement": { "top": 95, "left": 2.5, "right": 2.5, "bottom": 1 },
|
||||||
"background": { "color": { "fixed": "transparent" } },
|
"background": { "color": { "fixed": "transparent" } },
|
||||||
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
||||||
"config": { "text": { "mode": "fixed", "fixed": "floor (0.00 m)" }, "color": { "fixed": "#8a8a8a" }, "size": 10, "align": "center", "valign": "middle" }
|
"config": { "text": { "mode": "fixed", "fixed": "floor (0.00 m)" }, "color": { "fixed": "#6a6a6a" }, "size": 14, "align": "center", "valign": "middle" }
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Readout Level Label",
|
|
||||||
"type": "text",
|
|
||||||
"placement": { "top": 280, "left": 8, "width": 70, "height": 16 },
|
|
||||||
"background": { "color": { "fixed": "transparent" } },
|
|
||||||
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
|
||||||
"config": { "text": { "mode": "fixed", "fixed": "level" }, "color": { "fixed": "#888888" }, "size": 10, "align": "right", "valign": "middle" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Readout Level",
|
|
||||||
"type": "metric-value",
|
|
||||||
"placement": { "top": 280, "left": 82, "width": 86, "height": 16 },
|
|
||||||
"background": { "color": { "fixed": "transparent" } },
|
|
||||||
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
|
||||||
"config": { "text": { "mode": "field", "fixed": "", "field": "level" }, "color": { "fixed": "#1a1a1a" }, "size": 12, "align": "left", "valign": "middle" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Readout Volume Label",
|
|
||||||
"type": "text",
|
|
||||||
"placement": { "top": 280, "left": 168, "width": 60, "height": 16 },
|
|
||||||
"background": { "color": { "fixed": "transparent" } },
|
|
||||||
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
|
||||||
"config": { "text": { "mode": "fixed", "fixed": "volume" }, "color": { "fixed": "#888888" }, "size": 10, "align": "right", "valign": "middle" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Readout Volume",
|
|
||||||
"type": "metric-value",
|
|
||||||
"placement": { "top": 280, "left": 232, "width": 70, "height": 16 },
|
|
||||||
"background": { "color": { "fixed": "transparent" } },
|
|
||||||
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
|
||||||
"config": { "text": { "mode": "field", "fixed": "", "field": "volume" }, "color": { "fixed": "#1a1a1a" }, "size": 12, "align": "left", "valign": "middle" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Readout Fill Label",
|
|
||||||
"type": "text",
|
|
||||||
"placement": { "top": 280, "left": 302, "width": 50, "height": 16 },
|
|
||||||
"background": { "color": { "fixed": "transparent" } },
|
|
||||||
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
|
||||||
"config": { "text": { "mode": "fixed", "fixed": "fill" }, "color": { "fixed": "#888888" }, "size": 10, "align": "right", "valign": "middle" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Readout Fill",
|
|
||||||
"type": "metric-value",
|
|
||||||
"placement": { "top": 280, "left": 356, "width": 70, "height": 16 },
|
|
||||||
"background": { "color": { "fixed": "transparent" } },
|
|
||||||
"border": { "color": { "fixed": "transparent" }, "width": 0 },
|
|
||||||
"config": { "text": { "mode": "field", "fixed": "", "field": "volumePercent" }, "color": { "fixed": "#1a1a1a" }, "size": 12, "align": "left", "valign": "middle" }
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -472,7 +446,7 @@
|
|||||||
},
|
},
|
||||||
"overrides": []
|
"overrides": []
|
||||||
},
|
},
|
||||||
"gridPos": { "h": 5, "w": 12, "x": 12, "y": 6 },
|
"gridPos": { "h": 10, "w": 14, "x": 10, "y": 6 },
|
||||||
"id": 8,
|
"id": 8,
|
||||||
"options": {
|
"options": {
|
||||||
"legend": { "displayMode": "list", "placement": "bottom" },
|
"legend": { "displayMode": "list", "placement": "bottom" },
|
||||||
@@ -497,7 +471,7 @@
|
|||||||
},
|
},
|
||||||
"overrides": []
|
"overrides": []
|
||||||
},
|
},
|
||||||
"gridPos": { "h": 5, "w": 12, "x": 12, "y": 11 },
|
"gridPos": { "h": 10, "w": 14, "x": 10, "y": 16 },
|
||||||
"id": 9,
|
"id": 9,
|
||||||
"options": {
|
"options": {
|
||||||
"legend": { "displayMode": "list", "placement": "bottom" },
|
"legend": { "displayMode": "list", "placement": "bottom" },
|
||||||
@@ -514,7 +488,7 @@
|
|||||||
"meta": { "emittedFields": ["volume"] }
|
"meta": { "emittedFields": ["volume"] }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 16 },
|
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 26 },
|
||||||
"id": 10,
|
"id": 10,
|
||||||
"title": "Flow",
|
"title": "Flow",
|
||||||
"type": "row"
|
"type": "row"
|
||||||
@@ -528,7 +502,7 @@
|
|||||||
},
|
},
|
||||||
"overrides": []
|
"overrides": []
|
||||||
},
|
},
|
||||||
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 17 },
|
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 27 },
|
||||||
"id": 11,
|
"id": 11,
|
||||||
"options": {
|
"options": {
|
||||||
"legend": { "displayMode": "list", "placement": "bottom" },
|
"legend": { "displayMode": "list", "placement": "bottom" },
|
||||||
@@ -553,7 +527,7 @@
|
|||||||
},
|
},
|
||||||
"overrides": []
|
"overrides": []
|
||||||
},
|
},
|
||||||
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 17 },
|
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 27 },
|
||||||
"id": 12,
|
"id": 12,
|
||||||
"options": {
|
"options": {
|
||||||
"legend": { "displayMode": "list", "placement": "bottom" },
|
"legend": { "displayMode": "list", "placement": "bottom" },
|
||||||
|
|||||||
@@ -41,7 +41,20 @@ async function emitDashboardsFor(source, childSource, ctx, msg, trigger) {
|
|||||||
? await source.resolveFolderUid()
|
? await source.resolveFolderUid()
|
||||||
: (source.config?.grafanaConnector?.folderUid || undefined);
|
: (source.config?.grafanaConnector?.folderUid || undefined);
|
||||||
|
|
||||||
|
// Resolve the InfluxDB datasource uid by querying the target Grafana, then
|
||||||
|
// rewrite every panel/target/variable on each dashboard. Templates ship a
|
||||||
|
// hardcoded uid that only matches the Grafana they were authored against;
|
||||||
|
// without this rewrite a fresh Grafana renders every panel as
|
||||||
|
// "Datasource <uid> not found". Failure is non-fatal: rewriteDatasourceUid
|
||||||
|
// is a no-op when uid is empty, so panels keep their template uid.
|
||||||
|
const datasourceUid = typeof source.resolveDatasourceUid === 'function'
|
||||||
|
? await source.resolveDatasourceUid()
|
||||||
|
: '';
|
||||||
|
|
||||||
for (const dash of dashboards) {
|
for (const dash of dashboards) {
|
||||||
|
if (datasourceUid && typeof source.rewriteDatasourceUid === 'function') {
|
||||||
|
source.rewriteDatasourceUid(dash.dashboard, datasourceUid);
|
||||||
|
}
|
||||||
ctx.send({
|
ctx.send({
|
||||||
...msg,
|
...msg,
|
||||||
topic: 'create',
|
topic: 'create',
|
||||||
|
|||||||
@@ -189,39 +189,107 @@ class DashboardApi {
|
|||||||
const dryRunLevel = outflowLevel * (1 + dryRunPct / 100);
|
const dryRunLevel = outflowLevel * (1 + dryRunPct / 100);
|
||||||
const highSafetyLevel = overflowLevel * (highPct / 100);
|
const highSafetyLevel = overflowLevel * (highPct / 100);
|
||||||
|
|
||||||
// Canvas tank: rim at y=20px, floor at y=260px (240px tall). Must match
|
// Reference frame: 400 (logical w) x 760 (logical h) px. With every
|
||||||
// hard-coded tank rectangle placement in config/pumpingStation.json.
|
// canvas element using `constraint: { horizontal: scale, vertical: scale }`,
|
||||||
const TANK_TOP = 20, TANK_BOT = 260, TANK_H = TANK_BOT - TANK_TOP;
|
// Grafana interprets placement values as PERCENTAGES of the panel size,
|
||||||
|
// not pixels — so the basin stretches to fill the card at any viewport
|
||||||
|
// and stays centered without letterboxing.
|
||||||
|
// Tank reference: rim at y=48px (6.32%), floor at y=712px (93.68%),
|
||||||
|
// centred vertically with 48px top/bottom margins. Margins are sized
|
||||||
|
// so the size-14 'rim (X m)' and 'floor (0.00 m)' captions fit with
|
||||||
|
// ~10 px clearance from the topmost/bottommost threshold line — labels
|
||||||
|
// can never collide with a line at any basin geometry.
|
||||||
|
const FRAME_W = 400, FRAME_H = 760;
|
||||||
|
const TANK_TOP = 48, TANK_BOT = 712, TANK_H = TANK_BOT - TANK_TOP;
|
||||||
|
const yp = (v) => +(v / FRAME_H * 100).toFixed(2);
|
||||||
|
const xp = (v) => +(v / FRAME_W * 100).toFixed(2);
|
||||||
|
const hp = (v) => +(v / FRAME_H * 100).toFixed(2);
|
||||||
|
const wp = (v) => +(v / FRAME_W * 100).toFixed(2);
|
||||||
const yFor = (v) => +(TANK_BOT - (v / heightBasin) * TANK_H).toFixed(2);
|
const yFor = (v) => +(TANK_BOT - (v / heightBasin) * TANK_H).toFixed(2);
|
||||||
const tyFor = (yLine) => +(yLine - 8).toFixed(2); // centre 16px text on the line
|
const tyFor = (yLine) => +(yLine - 8).toFixed(2); // centre 16px text on the line
|
||||||
|
|
||||||
const y_overflow = yFor(overflowLevel);
|
let y_overflow = yFor(overflowLevel);
|
||||||
const y_highSafety = yFor(highSafetyLevel);
|
let y_highSafety = yFor(highSafetyLevel);
|
||||||
const y_inflow = yFor(inflowLevel);
|
let y_inflow = yFor(inflowLevel);
|
||||||
const y_dryRun = yFor(dryRunLevel);
|
let y_dryRun = yFor(dryRunLevel);
|
||||||
const y_outflow = yFor(outflowLevel);
|
let y_outflow = yFor(outflowLevel);
|
||||||
|
|
||||||
// Label y-positions get min-gap enforcement so labels never overlap even
|
// Enforce a minimum visual gap between adjacent threshold lines so labels
|
||||||
// when thresholds sit nearly on top of each other (e.g. dryRun=2 % means
|
// can always sit cleanly between them — independent of how close the
|
||||||
// dryRunLevel sits right on outflowLevel; highSafety=98 % puts it under
|
// underlying physical thresholds are. Slight geometric distortion is
|
||||||
// overflow). Lines stay at proportional y; only the label text moves.
|
// acceptable: the tank visual conveys ORDERING and ZONE STRUCTURE, not
|
||||||
// Two-pass (down + up) mirrors editor's basin-diagram.js placement logic.
|
// exact-scale level measurement. Dashed/value labels carry the true
|
||||||
const GAP = 20;
|
// numeric values.
|
||||||
const labels = [
|
const MIN_LINE_GAP = 28; // px (≈3.7% of 760-tall frame, > LABEL_H + 2)
|
||||||
{ id: 'overflow', y: tyFor(y_overflow) },
|
const sorted = [
|
||||||
{ id: 'highSafety', y: tyFor(y_highSafety) },
|
{ id: 'overflow', get: () => y_overflow, set: (v) => (y_overflow = v) },
|
||||||
{ id: 'inflow', y: tyFor(y_inflow) },
|
{ id: 'highSafety', get: () => y_highSafety, set: (v) => (y_highSafety = v) },
|
||||||
{ id: 'dryRun', y: tyFor(y_dryRun) },
|
{ id: 'inflow', get: () => y_inflow, set: (v) => (y_inflow = v) },
|
||||||
{ id: 'outflow', y: tyFor(y_outflow) },
|
{ id: 'dryRun', get: () => y_dryRun, set: (v) => (y_dryRun = v) },
|
||||||
].sort((a, b) => a.y - b.y);
|
{ id: 'outflow', get: () => y_outflow, set: (v) => (y_outflow = v) },
|
||||||
for (let i = 1; i < labels.length; i++) {
|
].sort((a, b) => a.get() - b.get());
|
||||||
if (labels[i].y < labels[i - 1].y + GAP) labels[i].y = labels[i - 1].y + GAP;
|
// Push down to enforce min gap (anchor: topmost line)
|
||||||
|
for (let i = 1; i < sorted.length; i++) {
|
||||||
|
const minY = sorted[i - 1].get() + MIN_LINE_GAP;
|
||||||
|
if (sorted[i].get() < minY) sorted[i].set(minY);
|
||||||
}
|
}
|
||||||
for (let i = labels.length - 2; i >= 0; i--) {
|
// If the last (lowest) line went past the floor, shift the whole stack up.
|
||||||
if (labels[i].y > labels[i + 1].y - GAP) labels[i].y = labels[i + 1].y - GAP;
|
const overshoot = sorted[sorted.length - 1].get() - TANK_BOT;
|
||||||
|
if (overshoot > 0) {
|
||||||
|
for (const item of sorted) item.set(item.get() - overshoot);
|
||||||
}
|
}
|
||||||
const ty = Object.fromEntries(labels.map((l) => [l.id, +l.y.toFixed(2)]));
|
|
||||||
|
|
||||||
|
// Label y-positions: labels sit either ABOVE or BELOW their threshold
|
||||||
|
// line, never on it. Each label is offset by ABOVE_OFFSET=22 px above
|
||||||
|
// its line by default (16 px tall label + 6 px clear above the line).
|
||||||
|
// If two thresholds are too close together for both labels to fit ABOVE
|
||||||
|
// their lines (label of the lower one would cross the upper line), the
|
||||||
|
// lower one's label flips BELOW its line instead. With the current
|
||||||
|
// basin (dryRun=2% means dryRunLevel sits right on outflowLevel; high-
|
||||||
|
// Safety=98% puts it just under overflowLevel) this naturally puts
|
||||||
|
// highSafety BELOW and outflow BELOW.
|
||||||
|
const LABEL_H = 16;
|
||||||
|
const ABOVE_OFFSET = 22; // label_top = line_y - 22 (6 px clear above line)
|
||||||
|
const BELOW_OFFSET = 6; // label_top = line_y + 6 (6 px clear below line)
|
||||||
|
const MIN_DIST_FOR_ABOVE = 24; // if distance to upper line < this, try below
|
||||||
|
const lines = [
|
||||||
|
{ id: 'overflow', line: y_overflow },
|
||||||
|
{ id: 'highSafety', line: y_highSafety },
|
||||||
|
{ id: 'inflow', line: y_inflow },
|
||||||
|
{ id: 'dryRun', line: y_dryRun },
|
||||||
|
{ id: 'outflow', line: y_outflow },
|
||||||
|
].sort((a, b) => a.line - b.line);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const prev = i > 0 ? lines[i - 1] : null;
|
||||||
|
const tooClose = prev && (lines[i].line - prev.line) < MIN_DIST_FOR_ABOVE;
|
||||||
|
if (tooClose) {
|
||||||
|
// Default to BELOW unless the label would be clipped by the tank
|
||||||
|
// floor (thresholds at the very bottom — dryRun=tiny% means
|
||||||
|
// dryRunLevel sits right on the floor). Then stack ABOVE the
|
||||||
|
// previous label instead, even if it slightly crowds its own line.
|
||||||
|
const belowY = lines[i].line + BELOW_OFFSET;
|
||||||
|
if (belowY + LABEL_H <= TANK_BOT) {
|
||||||
|
lines[i].y = belowY;
|
||||||
|
} else {
|
||||||
|
lines[i].y = prev.y + LABEL_H + 2; // stack above with 2 px gap
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lines[i].y = lines[i].line - ABOVE_OFFSET;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const ty = Object.fromEntries(lines.map((l) => [l.id, +l.y.toFixed(2)]));
|
||||||
|
|
||||||
|
// Canvas elements use `constraint: { horizontal: scale, vertical: scale }`
|
||||||
|
// with margin-style placement (top + bottom + left + right, all %s of the
|
||||||
|
// panel). Bottom = % from panel bottom, top = % from panel top. Width and
|
||||||
|
// height are derived as 100 - top - bottom, etc.
|
||||||
|
// We emit *all* placement margins precomputed so the JSON template stays
|
||||||
|
// declarative.
|
||||||
|
const LABEL_H_PCT = hp(16); // 16 px label height as % of frame
|
||||||
|
const LINE_H_PCT = hp(1); // 1 px line height as % of frame
|
||||||
|
const bMargin = (top, h) => +(100 - top - h).toFixed(2);
|
||||||
|
const lineBottom = (lineY) => +(100 - yp(lineY) - LINE_H_PCT).toFixed(2);
|
||||||
|
const labelBottom = (lblY) => +(100 - yp(lblY) - LABEL_H_PCT).toFixed(2);
|
||||||
return {
|
return {
|
||||||
heightBasin: +heightBasin.toFixed(2),
|
heightBasin: +heightBasin.toFixed(2),
|
||||||
outflowLevel: +outflowLevel.toFixed(3),
|
outflowLevel: +outflowLevel.toFixed(3),
|
||||||
@@ -229,16 +297,34 @@ class DashboardApi {
|
|||||||
overflowLevel: +overflowLevel.toFixed(3),
|
overflowLevel: +overflowLevel.toFixed(3),
|
||||||
dryRunLevel: +dryRunLevel.toFixed(3),
|
dryRunLevel: +dryRunLevel.toFixed(3),
|
||||||
highSafetyLevel: +highSafetyLevel.toFixed(3),
|
highSafetyLevel: +highSafetyLevel.toFixed(3),
|
||||||
y_overflow, y_highSafety, y_inflow, y_dryRun, y_outflow,
|
// Threshold line top margins (% from panel top)
|
||||||
h_spill: +(y_overflow - TANK_TOP).toFixed(2),
|
y_overflow: yp(y_overflow),
|
||||||
h_highSafety: +(y_highSafety - y_overflow).toFixed(2),
|
y_highSafety: yp(y_highSafety),
|
||||||
h_operating: +(y_outflow - y_highSafety).toFixed(2),
|
y_inflow: yp(y_inflow),
|
||||||
h_dead: +(TANK_BOT - y_outflow).toFixed(2),
|
y_dryRun: yp(y_dryRun),
|
||||||
ty_overflow: ty.overflow,
|
y_outflow: yp(y_outflow),
|
||||||
ty_highSafety: ty.highSafety,
|
// Threshold line bottom margins (% from panel bottom)
|
||||||
ty_inflow: ty.inflow,
|
yb_overflow: lineBottom(y_overflow),
|
||||||
ty_dryRun: ty.dryRun,
|
yb_highSafety: lineBottom(y_highSafety),
|
||||||
ty_outflow: ty.outflow,
|
yb_inflow: lineBottom(y_inflow),
|
||||||
|
yb_dryRun: lineBottom(y_dryRun),
|
||||||
|
yb_outflow: lineBottom(y_outflow),
|
||||||
|
// Zone bottom margins (zones end at the next line below)
|
||||||
|
zb_spill: +(100 - yp(y_overflow)).toFixed(2), // ends at overflow line
|
||||||
|
zb_highSafety: +(100 - yp(y_highSafety)).toFixed(2), // ends at highSafety line
|
||||||
|
zb_operating: +(100 - yp(y_outflow)).toFixed(2), // ends at outflow line
|
||||||
|
zb_dead: +(100 - yp(TANK_BOT)).toFixed(2), // ends at floor
|
||||||
|
// Label top margins (% from panel top) and bottom margins (% from panel bottom)
|
||||||
|
ty_overflow: yp(ty.overflow),
|
||||||
|
ty_highSafety: yp(ty.highSafety),
|
||||||
|
ty_inflow: yp(ty.inflow),
|
||||||
|
ty_dryRun: yp(ty.dryRun),
|
||||||
|
ty_outflow: yp(ty.outflow),
|
||||||
|
tyb_overflow: labelBottom(ty.overflow),
|
||||||
|
tyb_highSafety: labelBottom(ty.highSafety),
|
||||||
|
tyb_inflow: labelBottom(ty.inflow),
|
||||||
|
tyb_dryRun: labelBottom(ty.dryRun),
|
||||||
|
tyb_outflow: labelBottom(ty.outflow),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,6 +350,11 @@ class DashboardApi {
|
|||||||
return `${protocol}://${host}:${port}/api/folders`;
|
return `${protocol}://${host}:${port}/api/folders`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
grafanaDatasourcesUrl() {
|
||||||
|
const { protocol, host, port } = this.config.grafanaConnector;
|
||||||
|
return `${protocol}://${host}:${port}/api/datasources`;
|
||||||
|
}
|
||||||
|
|
||||||
_grafanaJsonHeaders() {
|
_grafanaJsonHeaders() {
|
||||||
const headers = { Accept: 'application/json', 'Content-Type': 'application/json' };
|
const headers = { Accept: 'application/json', 'Content-Type': 'application/json' };
|
||||||
const token = this.config.grafanaConnector.bearerToken;
|
const token = this.config.grafanaConnector.bearerToken;
|
||||||
@@ -329,6 +420,90 @@ class DashboardApi {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve the target Grafana InfluxDB datasource uid at push time. Templates
|
||||||
|
// ship with a hardcoded uid baked into every panel; that uid only matches the
|
||||||
|
// Grafana instance the templates were authored against. Any other Grafana
|
||||||
|
// (fresh laptop, VPS, rebuilt instance) renders the panels as
|
||||||
|
// "Datasource <uid> not found". Resolution is done once per process and
|
||||||
|
// cached.
|
||||||
|
//
|
||||||
|
// Degradation contract: any failure (no fetch, network error, non-OK
|
||||||
|
// response, no influxdb datasource present) returns '' and the caller leaves
|
||||||
|
// the template's baked-in uid alone. Worst-case behavior is unchanged from
|
||||||
|
// before this resolver existed.
|
||||||
|
async resolveDatasourceUid({ fetchImpl = globalThis.fetch } = {}) {
|
||||||
|
if (this._resolvedDatasourceUid) return this._resolvedDatasourceUid;
|
||||||
|
if (typeof fetchImpl !== 'function') {
|
||||||
|
this.logger.warn('resolveDatasourceUid: no fetch implementation available; leaving template uid intact');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const uid = await this._lookupInfluxDatasource(fetchImpl);
|
||||||
|
if (uid) {
|
||||||
|
this._resolvedDatasourceUid = uid;
|
||||||
|
return uid;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.warn(`resolveDatasourceUid failed (${err?.message || err}); leaving template uid intact`);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async _lookupInfluxDatasource(fetchImpl) {
|
||||||
|
const url = this.grafanaDatasourcesUrl();
|
||||||
|
const headers = this._grafanaJsonHeaders();
|
||||||
|
const res = await fetchImpl(url, { method: 'GET', headers });
|
||||||
|
if (!res?.ok) {
|
||||||
|
this.logger.warn(`resolveDatasourceUid: GET /api/datasources -> ${res?.status}`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const list = await res.json();
|
||||||
|
const match = Array.isArray(list) && list.find((d) => String(d?.type || '').toLowerCase() === 'influxdb');
|
||||||
|
if (match?.uid) {
|
||||||
|
this.logger.info({ event: 'datasource-resolved', outcome: 'found', name: match.name, uid: match.uid });
|
||||||
|
return match.uid;
|
||||||
|
}
|
||||||
|
this.logger.warn('resolveDatasourceUid: no influxdb datasource on target Grafana');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewrite every influxdb datasource.uid on a dashboard (panels, nested row
|
||||||
|
// panels, panel.targets, templating variables) to `uid`. No-op for any
|
||||||
|
// datasource whose type isn't 'influxdb' (e.g. the '-- Grafana --' annotation
|
||||||
|
// datasource) or whose uid is a template variable reference (e.g.
|
||||||
|
// '${datasource}'). No-op when `uid` is falsy.
|
||||||
|
rewriteDatasourceUid(dashboard, uid) {
|
||||||
|
if (!uid || !dashboard) return;
|
||||||
|
const visit = (panels) => {
|
||||||
|
if (!Array.isArray(panels)) return;
|
||||||
|
for (const panel of panels) {
|
||||||
|
if (panel?.datasource && String(panel.datasource.type || '').toLowerCase() === 'influxdb'
|
||||||
|
&& typeof panel.datasource.uid === 'string' && !panel.datasource.uid.startsWith('$')) {
|
||||||
|
panel.datasource.uid = uid;
|
||||||
|
}
|
||||||
|
if (Array.isArray(panel?.targets)) {
|
||||||
|
for (const t of panel.targets) {
|
||||||
|
if (t?.datasource && String(t.datasource.type || '').toLowerCase() === 'influxdb'
|
||||||
|
&& typeof t.datasource.uid === 'string' && !t.datasource.uid.startsWith('$')) {
|
||||||
|
t.datasource.uid = uid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visit(panel?.panels);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
visit(dashboard.panels);
|
||||||
|
const tplList = dashboard?.templating?.list;
|
||||||
|
if (Array.isArray(tplList)) {
|
||||||
|
for (const v of tplList) {
|
||||||
|
if (v?.datasource && String(v.datasource.type || '').toLowerCase() === 'influxdb'
|
||||||
|
&& typeof v.datasource.uid === 'string' && !v.datasource.uid.startsWith('$')) {
|
||||||
|
v.datasource.uid = uid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildDashboard({ nodeConfig, positionVsParent }) {
|
buildDashboard({ nodeConfig, positionVsParent }) {
|
||||||
const softwareType =
|
const softwareType =
|
||||||
nodeConfig?.functionality?.softwareType ||
|
nodeConfig?.functionality?.softwareType ||
|
||||||
|
|||||||
174
test/basic/slice49-datasource-resolve.basic.test.js
Normal file
174
test/basic/slice49-datasource-resolve.basic.test.js
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const DashboardApi = require('../../src/specificClass.js');
|
||||||
|
const { registerChild } = require('../../src/commands/handlers.js');
|
||||||
|
|
||||||
|
function makeFetch(routes) {
|
||||||
|
const calls = [];
|
||||||
|
const fetchImpl = async (url, opts = {}) => {
|
||||||
|
const method = opts.method || 'GET';
|
||||||
|
const { pathname } = new URL(url);
|
||||||
|
calls.push({ method, pathname, body: opts.body });
|
||||||
|
const r = routes[`${method} ${pathname}`];
|
||||||
|
if (!r) return { ok: false, status: 404, json: async () => ({ message: 'not found' }) };
|
||||||
|
if (typeof r === 'function') return r();
|
||||||
|
return { ok: r.ok ?? true, status: r.status ?? 200, json: async () => r.body };
|
||||||
|
};
|
||||||
|
fetchImpl.calls = calls;
|
||||||
|
return fetchImpl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function api(grafanaConnector = {}) {
|
||||||
|
return new DashboardApi({ grafanaConnector });
|
||||||
|
}
|
||||||
|
|
||||||
|
test('resolveDatasourceUid returns the first influxdb datasource uid', async () => {
|
||||||
|
const a = api();
|
||||||
|
const fetchImpl = makeFetch({
|
||||||
|
'GET /api/datasources': {
|
||||||
|
body: [
|
||||||
|
{ type: 'prometheus', uid: 'p1' },
|
||||||
|
{ type: 'influxdb', uid: 'dfmpjg9jjvym8b', name: 'influxdb' },
|
||||||
|
{ type: 'influxdb', uid: 'second-one' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const uid = await a.resolveDatasourceUid({ fetchImpl });
|
||||||
|
assert.equal(uid, 'dfmpjg9jjvym8b');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveDatasourceUid is cached → second call makes no further fetch', async () => {
|
||||||
|
const a = api();
|
||||||
|
const fetchImpl = makeFetch({
|
||||||
|
'GET /api/datasources': { body: [{ type: 'influxdb', uid: 'u1' }] },
|
||||||
|
});
|
||||||
|
await a.resolveDatasourceUid({ fetchImpl });
|
||||||
|
await a.resolveDatasourceUid({ fetchImpl });
|
||||||
|
assert.equal(fetchImpl.calls.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveDatasourceUid returns empty string when no influxdb datasource exists', async () => {
|
||||||
|
const a = api();
|
||||||
|
const fetchImpl = makeFetch({
|
||||||
|
'GET /api/datasources': { body: [{ type: 'prometheus', uid: 'p1' }] },
|
||||||
|
});
|
||||||
|
const uid = await a.resolveDatasourceUid({ fetchImpl });
|
||||||
|
assert.equal(uid, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveDatasourceUid: fetch throws → returns empty string (template uid preserved)', async () => {
|
||||||
|
const a = api();
|
||||||
|
const fetchImpl = async () => { throw new Error('ECONNREFUSED'); };
|
||||||
|
const uid = await a.resolveDatasourceUid({ fetchImpl });
|
||||||
|
assert.equal(uid, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveDatasourceUid: no fetch available → returns empty string', async () => {
|
||||||
|
const a = api();
|
||||||
|
const uid = await a.resolveDatasourceUid({ fetchImpl: null });
|
||||||
|
assert.equal(uid, '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rewriteDatasourceUid: rewrites panel.datasource.uid for influxdb only', () => {
|
||||||
|
const a = api();
|
||||||
|
const dashboard = {
|
||||||
|
panels: [
|
||||||
|
{ datasource: { type: 'influxdb', uid: 'OLD' } },
|
||||||
|
{ datasource: { type: 'grafana', uid: '-- Grafana --' } },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
a.rewriteDatasourceUid(dashboard, 'NEW');
|
||||||
|
assert.equal(dashboard.panels[0].datasource.uid, 'NEW');
|
||||||
|
assert.equal(dashboard.panels[1].datasource.uid, '-- Grafana --');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rewriteDatasourceUid: rewrites panel.targets[].datasource.uid', () => {
|
||||||
|
const a = api();
|
||||||
|
const dashboard = {
|
||||||
|
panels: [
|
||||||
|
{
|
||||||
|
datasource: { type: 'influxdb', uid: 'OLD' },
|
||||||
|
targets: [
|
||||||
|
{ datasource: { type: 'influxdb', uid: 'OLD' }, query: 'a' },
|
||||||
|
{ datasource: { type: 'influxdb', uid: 'OLD' }, query: 'b' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
a.rewriteDatasourceUid(dashboard, 'NEW');
|
||||||
|
for (const t of dashboard.panels[0].targets) assert.equal(t.datasource.uid, 'NEW');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rewriteDatasourceUid: descends into nested row panels', () => {
|
||||||
|
const a = api();
|
||||||
|
const dashboard = {
|
||||||
|
panels: [
|
||||||
|
{
|
||||||
|
type: 'row',
|
||||||
|
panels: [
|
||||||
|
{ datasource: { type: 'influxdb', uid: 'OLD' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
a.rewriteDatasourceUid(dashboard, 'NEW');
|
||||||
|
assert.equal(dashboard.panels[0].panels[0].datasource.uid, 'NEW');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rewriteDatasourceUid: rewrites templating.list[] influxdb variables', () => {
|
||||||
|
const a = api();
|
||||||
|
const dashboard = {
|
||||||
|
panels: [],
|
||||||
|
templating: {
|
||||||
|
list: [
|
||||||
|
{ type: 'query', datasource: { type: 'influxdb', uid: 'OLD' } },
|
||||||
|
{ type: 'constant', datasource: { type: 'prometheus', uid: 'OLD' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
a.rewriteDatasourceUid(dashboard, 'NEW');
|
||||||
|
assert.equal(dashboard.templating.list[0].datasource.uid, 'NEW');
|
||||||
|
assert.equal(dashboard.templating.list[1].datasource.uid, 'OLD');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rewriteDatasourceUid: leaves template-variable references alone (${datasource})', () => {
|
||||||
|
const a = api();
|
||||||
|
const dashboard = {
|
||||||
|
panels: [{ datasource: { type: 'influxdb', uid: '${datasource}' } }],
|
||||||
|
};
|
||||||
|
a.rewriteDatasourceUid(dashboard, 'NEW');
|
||||||
|
assert.equal(dashboard.panels[0].datasource.uid, '${datasource}');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rewriteDatasourceUid: no-op when uid is falsy (preserves template)', () => {
|
||||||
|
const a = api();
|
||||||
|
const dashboard = { panels: [{ datasource: { type: 'influxdb', uid: 'KEEP' } }] };
|
||||||
|
a.rewriteDatasourceUid(dashboard, '');
|
||||||
|
assert.equal(dashboard.panels[0].datasource.uid, 'KEEP');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('emit path rewrites every upsert dashboard with the resolved datasource uid', async () => {
|
||||||
|
const a = api({ folderTitle: 'EVOLV' });
|
||||||
|
a.resolveFolderUid = async () => 'fld';
|
||||||
|
a.resolveDatasourceUid = async () => 'resolved-ds-uid';
|
||||||
|
|
||||||
|
const childSource = {
|
||||||
|
config: { general: { id: 'm1', name: 'Level' }, functionality: { softwareType: 'measurement' } },
|
||||||
|
};
|
||||||
|
const sent = [];
|
||||||
|
const ctx = { node: { id: 'dapi' }, send: (m) => sent.push(m) };
|
||||||
|
await registerChild(a, { payload: childSource }, ctx);
|
||||||
|
|
||||||
|
assert.ok(sent.length >= 1);
|
||||||
|
for (const m of sent) {
|
||||||
|
const panels = m.payload?.dashboard?.panels || [];
|
||||||
|
for (const p of panels) {
|
||||||
|
if (p?.datasource?.type === 'influxdb') {
|
||||||
|
assert.equal(p.datasource.uid, 'resolved-ds-uid');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user