From 1232bf56064a456a9b42010d775a5c7d33330c5e Mon Sep 17 00:00:00 2001 From: Mark Herwege Date: Fri, 12 Apr 2024 14:45:29 +0200 Subject: [PATCH] Allow future periods in sitemap configuration (#2529) Chart widget sitemap configuration has been extended with support for future periods in https://github.com/openhab/openhab-core/pull/4172. This adds configuration in the UI. See #2518. Signed-off-by: Mark Herwege --- .../web/src/assets/sitemap-lexer.nearley | 10 +- .../__tests__/sitemap-code_jest.spec.js | 99 +++++++++++++++++++ .../pagedesigner/sitemap/widget-details.vue | 3 +- .../__tests__/sitemap-edit_jest.spec.js | 24 +++++ .../settings/pages/sitemap/sitemap-edit.vue | 2 +- 5 files changed, 134 insertions(+), 4 deletions(-) diff --git a/bundles/org.openhab.ui/web/src/assets/sitemap-lexer.nearley b/bundles/org.openhab.ui/web/src/assets/sitemap-lexer.nearley index 34fff7c2af..237119c752 100644 --- a/bundles/org.openhab.ui/web/src/assets/sitemap-lexer.nearley +++ b/bundles/org.openhab.ui/web/src/assets/sitemap-lexer.nearley @@ -9,7 +9,7 @@ item: 'item=', staticIcon: 'staticIcon=', icon: 'icon=', - widgetattr: ['url=', 'refresh=', 'service=', 'period=', 'height=', 'minValue=', 'maxValue=', 'step=', 'encoding=', 'yAxisDecimalPattern=', 'inputHint=', 'columns='], + widgetattr: ['url=', 'refresh=', 'service=', 'height=', 'minValue=', 'maxValue=', 'step=', 'encoding=', 'yAxisDecimalPattern=', 'inputHint=', 'columns='], widgetboolattr: ['legend='], widgetfreqattr: 'sendFrequency=', widgetfrcitmattr: 'forceasitem=', @@ -19,6 +19,7 @@ widgetcolorattr: ['labelcolor=', 'valuecolor=', 'iconcolor='], widgetswitchattr: 'switchSupport', widgetronlyattr: 'releaseOnly', + widgetperiodattr: 'period=', nlwidget: ['Switch ', 'Selection ', 'Slider ', 'Setpoint ', 'Input ', 'Video ', 'Chart ', 'Webview ', 'Colorpicker ', 'Mapview ', 'Buttongrid ', 'Default '], lwidget: ['Text ', 'Group ', 'Image ', 'Frame '], lparen: '(', @@ -40,10 +41,10 @@ ML_COMMENT: /\/\*[\s\S]*?\*\//, boolean: /(?:true)|(?:false)/, identifier: /(?:[A-Za-z_][A-Za-z0-9_]*)|(?:[0-9]+[A-Za-z_][A-Za-z0-9_]*)/, - number: /-?[0-9]+(?:\.[0-9]*)?/, comma: ',', colon: ':', hyphen: '-', + number: /-?[0-9]+(?:\.[0-9]*)?/, string: { match: /"(?:\\["\\]|[^\n"\\])*"/, value: x => x.slice(1, -1) } }) const requiresItem = ['Group', 'Chart', 'Switch', 'Mapview', 'Slider', 'Selection', 'Setpoint', 'Input ', 'Colorpicker', 'Default'] @@ -116,6 +117,7 @@ WidgetAttr -> %widgetswitchattr | %widgetfrcitmattr _ WidgetBooleanAttrValue {% (d) => ['forceAsItem', d[2]] %} | %widgetboolattr _ WidgetBooleanAttrValue {% (d) => [d[0].value, d[2]] %} | %widgetfreqattr _ WidgetAttrValue {% (d) => ['frequency', d[2]] %} + | %widgetperiodattr _ WidgetPeriodAttrValue {% (d) => ['period', d[2]] %} | %icon _ WidgetIconRulesAttrValue {% (d) => ['iconrules', d[2]] %} | %icon _ WidgetIconAttrValue {% (d) => [d[0].value, d[2].join("")] %} | %staticIcon _ WidgetIconAttrValue {% (d) => [d[0].value, d[2].join("")] %} @@ -134,6 +136,10 @@ WidgetIconAttrValue -> %string WidgetIconRulesAttrValue -> %lbracket _ IconRules _ %rbracket {% (d) => d[2] %} WidgetIconName -> %identifier | WidgetIconName %hyphen %identifier {% (d) => d[0] + "-" + d[2].value %} +WidgetPeriodAttrValue -> %identifier %hyphen %identifier {% (d) => d[0].value + "-" + d[2].value %} + | %hyphen %identifier {% (d) => "-" + d[1].value %} + | %identifier {% (d) => d[0].value %} + | %string {% (d) => d[0].value %} WidgetAttrValue -> %number {% (d) => { return parseFloat(d[0].value) } %} | %identifier {% (d) => d[0].value %} | %string {% (d) => d[0].value %} diff --git a/bundles/org.openhab.ui/web/src/components/pagedesigner/sitemap/__tests__/sitemap-code_jest.spec.js b/bundles/org.openhab.ui/web/src/components/pagedesigner/sitemap/__tests__/sitemap-code_jest.spec.js index 14ec623e25..883cebe45d 100644 --- a/bundles/org.openhab.ui/web/src/components/pagedesigner/sitemap/__tests__/sitemap-code_jest.spec.js +++ b/bundles/org.openhab.ui/web/src/components/pagedesigner/sitemap/__tests__/sitemap-code_jest.spec.js @@ -452,4 +452,103 @@ describe('SitemapCode', () => { } }) }) + + it('parses a chart widget correctly', async () => { + expect(wrapper.vm.sitemapDsl).toBeDefined() + // simulate updating the sitemap in code + const sitemap = [ + 'sitemap test label="Test" {', + ' Chart item=Temperature period=4h', + '}', + '' + ].join('\n') + wrapper.vm.updateSitemap(sitemap) + expect(wrapper.vm.sitemapDsl).toMatch(/^sitemap test label="Test"/) + expect(wrapper.vm.parsedSitemap.error).toBeFalsy() + + await wrapper.vm.$nextTick() + + // check whether an 'updated' event was emitted and its payload + // (should contain the parsing result for the new sitemap definition) + const events = wrapper.emitted().updated + expect(events).toBeTruthy() + expect(events.length).toBe(1) + const payload = events[0][0] + expect(payload.slots).toBeDefined() + expect(payload.slots.widgets).toBeDefined() + expect(payload.slots.widgets.length).toBe(1) + expect(payload.slots.widgets[0]).toEqual({ + component: 'Chart', + config: { + item: 'Temperature', + period: '4h' + } + }) + }) + + it('parses a chart widget with future period correctly', async () => { + expect(wrapper.vm.sitemapDsl).toBeDefined() + // simulate updating the sitemap in code + const sitemap = [ + 'sitemap test label="Test" {', + ' Chart item=Temperature period=-4h', + '}', + '' + ].join('\n') + wrapper.vm.updateSitemap(sitemap) + expect(wrapper.vm.sitemapDsl).toMatch(/^sitemap test label="Test"/) + expect(wrapper.vm.parsedSitemap.error).toBeFalsy() + + await wrapper.vm.$nextTick() + + // check whether an 'updated' event was emitted and its payload + // (should contain the parsing result for the new sitemap definition) + const events = wrapper.emitted().updated + expect(events).toBeTruthy() + expect(events.length).toBe(1) + const payload = events[0][0] + expect(payload.slots).toBeDefined() + expect(payload.slots.widgets).toBeDefined() + expect(payload.slots.widgets.length).toBe(1) + expect(payload.slots.widgets[0]).toEqual({ + component: 'Chart', + config: { + item: 'Temperature', + period: '-4h' + } + }) + }) + + it('parses a chart widget with past and ISO-8601 future period correctly', async () => { + expect(wrapper.vm.sitemapDsl).toBeDefined() + // simulate updating the sitemap in code + const sitemap = [ + 'sitemap test label="Test" {', + ' Chart item=Temperature period=4h-P1DT12H', + '}', + '' + ].join('\n') + wrapper.vm.updateSitemap(sitemap) + expect(wrapper.vm.sitemapDsl).toMatch(/^sitemap test label="Test"/) + expect(wrapper.vm.parsedSitemap.error).toBeFalsy() + + await wrapper.vm.$nextTick() + + // check whether an 'updated' event was emitted and its payload + // (should contain the parsing result for the new sitemap definition) + const events = wrapper.emitted().updated + expect(events).toBeTruthy() + expect(events.length).toBe(1) + const payload = events[0][0] + expect(payload.slots).toBeDefined() + expect(payload.slots.widgets).toBeDefined() + expect(payload.slots.widgets.length).toBe(1) + expect(payload.slots.widgets[0]).toEqual({ + component: 'Chart', + config: { + item: 'Temperature', + period: '4h-P1DT12H' + } + }) + }) }) diff --git a/bundles/org.openhab.ui/web/src/components/pagedesigner/sitemap/widget-details.vue b/bundles/org.openhab.ui/web/src/components/pagedesigner/sitemap/widget-details.vue index 584a330e57..5624a9898b 100644 --- a/bundles/org.openhab.ui/web/src/components/pagedesigner/sitemap/widget-details.vue +++ b/bundles/org.openhab.ui/web/src/components/pagedesigner/sitemap/widget-details.vue @@ -24,7 +24,8 @@ diff --git a/bundles/org.openhab.ui/web/src/pages/settings/pages/sitemap/__tests__/sitemap-edit_jest.spec.js b/bundles/org.openhab.ui/web/src/pages/settings/pages/sitemap/__tests__/sitemap-edit_jest.spec.js index 3699d0a832..fa2fb0335c 100644 --- a/bundles/org.openhab.ui/web/src/pages/settings/pages/sitemap/__tests__/sitemap-edit_jest.spec.js +++ b/bundles/org.openhab.ui/web/src/pages/settings/pages/sitemap/__tests__/sitemap-edit_jest.spec.js @@ -200,6 +200,22 @@ describe('SitemapEdit', () => { wrapper.vm.validateWidgets() expect(lastDialogConfig).toBeFalsy() + // configure a future period for the Chart and check that there are no validation errors + lastDialogConfig = null + wrapper.vm.selectWidget([wrapper.vm.sitemap.slots.widgets[0], wrapper.vm.sitemap]) + await wrapper.vm.$nextTick() + localVue.set(wrapper.vm.selectedWidget.config, 'period', '-4h') + wrapper.vm.validateWidgets() + expect(lastDialogConfig).toBeFalsy() + + // configure a combined past and future period for the Chart and check that there are no validation errors + lastDialogConfig = null + wrapper.vm.selectWidget([wrapper.vm.sitemap.slots.widgets[0], wrapper.vm.sitemap]) + await wrapper.vm.$nextTick() + localVue.set(wrapper.vm.selectedWidget.config, 'period', '4h-4h') + wrapper.vm.validateWidgets() + expect(lastDialogConfig).toBeFalsy() + // configure an ISO-8601 period for the Chart and check that there are no validation errors lastDialogConfig = null wrapper.vm.selectWidget([wrapper.vm.sitemap.slots.widgets[0], wrapper.vm.sitemap]) @@ -207,6 +223,14 @@ describe('SitemapEdit', () => { localVue.set(wrapper.vm.selectedWidget.config, 'period', 'P10M2W1DT12H30M') wrapper.vm.validateWidgets() expect(lastDialogConfig).toBeFalsy() + + // configure a combined past and future ISO-8601 and classic period for the Chart and check that there are no validation errors + lastDialogConfig = null + wrapper.vm.selectWidget([wrapper.vm.sitemap.slots.widgets[0], wrapper.vm.sitemap]) + await wrapper.vm.$nextTick() + localVue.set(wrapper.vm.selectedWidget.config, 'period', '4h-P10M2W1DT12H30M') + wrapper.vm.validateWidgets() + expect(lastDialogConfig).toBeFalsy() }) it('validates step is positive', async () => { diff --git a/bundles/org.openhab.ui/web/src/pages/settings/pages/sitemap/sitemap-edit.vue b/bundles/org.openhab.ui/web/src/pages/settings/pages/sitemap/sitemap-edit.vue index 74267d62a8..cceb3cc25a 100644 --- a/bundles/org.openhab.ui/web/src/pages/settings/pages/sitemap/sitemap-edit.vue +++ b/bundles/org.openhab.ui/web/src/pages/settings/pages/sitemap/sitemap-edit.vue @@ -460,7 +460,7 @@ export default { } }) widgetList.filter(widget => widget.component === 'Chart').forEach(widget => { - if (!(widget.config && widget.config.period && /^P(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(\d+H)?(\d+M)?(\d+S)?)?$|^\d*[YMWDh]$/.test(widget.config.period))) { + if (!(widget.config && widget.config.period && /^((P(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(\d+H)?(\d+M)?(\d+S)?)?|\d*[YMWDh])-)?-?(P(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(\d+H)?(\d+M)?(\d+S)?)?|\d*[YMWDh])$/.test(widget.config.period))) { let label = widget.config && widget.config.label ? widget.config.label : 'without label' validationWarnings.push(widget.component + ' widget ' + label + ', invalid period configured: ' + widget.config.period) }