diff --git a/config/machineGroup.json b/config/machineGroup.json index 69ec2e2..9f916a0 100644 --- a/config/machineGroup.json +++ b/config/machineGroup.json @@ -122,7 +122,12 @@ } ], "title": "Scaling", - "type": "stat" + "type": "stat", + "meta": { + "emittedFields": [ + "scaling" + ] + } }, { "datasource": { @@ -174,7 +179,12 @@ } ], "title": "Abs Dist Peak", - "type": "stat" + "type": "stat", + "meta": { + "emittedFields": [ + "absDistFromPeak" + ] + } }, { "datasource": { @@ -227,7 +237,12 @@ } ], "title": "Rel Dist Peak", - "type": "stat" + "type": "stat", + "meta": { + "emittedFields": [ + "relDistFromPeak" + ] + } }, { "gridPos": { @@ -253,7 +268,58 @@ "fillOpacity": 10 } }, - "overrides": [] + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": ".+\\.min$" + }, + "properties": [ + { + "id": "custom.lineStyle", + "value": { + "fill": "dash", + "dash": [ + 10, + 10 + ] + } + }, + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "orange" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": ".+\\.max$" + }, + "properties": [ + { + "id": "custom.lineStyle", + "value": { + "fill": "dash", + "dash": [ + 10, + 10 + ] + } + }, + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "red" + } + } + ] + } + ] }, "gridPos": { "h": 8, @@ -278,7 +344,13 @@ } ], "title": "Total Flow", - "type": "timeseries" + "type": "timeseries", + "meta": { + "emittedFields": [ + "flow.total", + "flow.group" + ] + } }, { "datasource": { @@ -293,7 +365,58 @@ "fillOpacity": 10 } }, - "overrides": [] + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": ".+\\.min$" + }, + "properties": [ + { + "id": "custom.lineStyle", + "value": { + "fill": "dash", + "dash": [ + 10, + 10 + ] + } + }, + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "orange" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": ".+\\.max$" + }, + "properties": [ + { + "id": "custom.lineStyle", + "value": { + "fill": "dash", + "dash": [ + 10, + 10 + ] + } + }, + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "red" + } + } + ] + } + ] }, "gridPos": { "h": 8, @@ -318,7 +441,13 @@ } ], "title": "Total Power", - "type": "timeseries" + "type": "timeseries", + "meta": { + "emittedFields": [ + "power.total", + "power.group" + ] + } } ], "schemaVersion": 39, diff --git a/test/basic/slice40-mgc-template.basic.test.js b/test/basic/slice40-mgc-template.basic.test.js new file mode 100644 index 0000000..1f8fa60 --- /dev/null +++ b/test/basic/slice40-mgc-template.basic.test.js @@ -0,0 +1,69 @@ +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const DashboardApi = require('../../src/specificClass.js'); + +test('MGC template panels are all group-level (no per-pump fields)', () => { + const api = new DashboardApi({}); + const dash = api.loadTemplate('machineGroup'); + const PER_PUMP = new Set(['ctrl', 'state', 'runtime', 'pressure.upstream', 'pressure.downstream', 'temperature']); + for (const panel of dash.panels || []) { + if (panel.type === 'row') continue; + const fields = panel?.meta?.emittedFields || []; + for (const f of fields) { + assert.ok(!PER_PUMP.has(f), + `MGC panel "${panel.title}" emits ${f}, which belongs to rotatingMachine (per-pump). Move to children.`); + } + } +}); + +test('MGC group panels are annotated (mode, scaling, abs/rel peak, totals)', () => { + const api = new DashboardApi({}); + const dash = api.loadTemplate('machineGroup'); + const non = dash.panels.filter((p) => p.type !== 'row'); + const annotated = non.filter((p) => p?.meta?.emittedFields); + assert.equal(annotated.length, non.length, 'every non-row MGC panel annotated'); +}); + +test('MGC timeseries panels carry dashed-bounds overrides for .min/.max', () => { + const api = new DashboardApi({}); + const dash = api.loadTemplate('machineGroup'); + const ts = dash.panels.filter((p) => p.type === 'timeseries'); + for (const panel of ts) { + const ov = panel?.fieldConfig?.overrides || []; + const hasMin = ov.some((o) => o.matcher?.id === 'byRegexp' && /\.min\$/.test(o.matcher?.options || '')); + const hasMax = ov.some((o) => o.matcher?.id === 'byRegexp' && /\.max\$/.test(o.matcher?.options || '')); + assert.ok(hasMin && hasMax, `MGC ts panel "${panel.title}" missing .min/.max dashed override`); + } +}); + +test('MGC composer dedups parent panels covered by pump children', () => { + // If a rotatingMachine child claims to emit `flow.total` (it shouldn't, but + // suppose), the parent MGC's "Total Flow" panel would be removed. Verify + // the composer applies the same dedup rule to MGC parents. + const api = new DashboardApi({}); + function makeChildSrc(id) { + return { config: { general: { id }, functionality: { softwareType: 'machine', positionVsParent: 'downstream' } } }; + } + const child = makeChildSrc('pump-1'); + const root = { + config: { general: { id: 'mgc-1', name: 'MGC' }, functionality: { softwareType: 'machineGroupControl', positionVsParent: 'atequipment' } }, + childRegistrationUtils: { registeredChildren: new Map([['pump-1', { child, position: 'downstream', softwareType: 'machine' }]]) }, + }; + const origLoad = api.loadTemplate.bind(api); + api.loadTemplate = function (t) { + const dash = origLoad(t); + if (t === 'machine') { + // Make the pump's template falsely claim it emits flow.total/flow.group + const firstPanel = dash.panels.find((p) => p.type !== 'row'); + if (firstPanel) (firstPanel.meta ||= {}).emittedFields = ['flow.total', 'flow.group']; + } + return dash; + }; + const results = api.generateDashboardsForGraph(root); + const mgcDash = results[0].dashboard; + const totalFlowPanel = mgcDash.panels.find((p) => p.title === 'Total Flow'); + assert.ok(!totalFlowPanel, 'MGC Total Flow panel should be removed when child claims flow.total/flow.group'); +});