Skip to content

Commit

Permalink
Allow future periods in sitemap configuration (openhab#2529)
Browse files Browse the repository at this point in the history
Chart widget sitemap configuration has been extended with support for
future periods in openhab/openhab-core#4172.
This adds configuration in the UI.

See openhab#2518.

Signed-off-by: Mark Herwege <[email protected]>
  • Loading branch information
mherwege authored Apr 12, 2024
1 parent c546534 commit 1232bf5
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 4 deletions.
10 changes: 8 additions & 2 deletions bundles/org.openhab.ui/web/src/assets/sitemap-lexer.nearley
Original file line number Diff line number Diff line change
Expand Up @@ -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=',
Expand All @@ -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: '(',
Expand All @@ -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']
Expand Down Expand Up @@ -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("")] %}
Expand All @@ -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 %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
<f7-list-input v-if="supports('encoding')" label="Encoding" type="text" :value="widget.config.encoding" @input="updateParameter('encoding', $event)" clear-button />
<f7-list-input v-if="supports('service')" label="Service" type="text" :value="widget.config.service" @input="updateParameter('service', $event)" clear-button />
<f7-list-input v-if="supports('period')" label="Period" type="text"
placeholder="PnYnMnDTnHnMnS" validate pattern="^P(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(\d+H)?(\d+M)?(\d+S)?)?$|^\d*[YMWDh]$"
placeholder="PnYnMnDTnHnMnS-PnYnMnDTnHnMnS"
validate pattern="^((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])$"
:value="widget.config.period" @input="updateParameter('period', $event)" clear-button />
<f7-list-input v-if="supports('height')" label="Height" type="number" :value="widget.config.height" @input="updateParameter('height', $event)" clear-button />
<f7-list-input v-if="supports('sendFrequency')" label="Frequency" type="number" :value="widget.config.sendFrequency" @input="updateParameter('sendFrequency', $event)" clear-button />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,13 +200,37 @@ 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])
await wrapper.vm.$nextTick()
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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down

0 comments on commit 1232bf5

Please sign in to comment.