Skip to content

Commit

Permalink
Add a persistence configuration page (#1917)
Browse files Browse the repository at this point in the history
Closes #1463.
Refs openhab/openhab-core#2871.
Refs openhab/openhab-core#3642.

It is accessible from the add-on settings page and has both a design and
a code tab.

The design tab allows to set persistence strategies for Items, define
cron strategies and set the default strategies. It does not duplicate
names for (cron) persistence strategies and filters as well as configs
for the same set of Items.
All four filters provided by openHAB core (treshold, time, equals/not
equals, include/exclude) can be configured.
When the user removes a cron strategy or a filter, it is automatically
removed from all configs so that there is no API failure (400 Bad
Request).

No code completion is not provided, but required attributes for filters
are automatically set on save to avoid API failure (500 Internal Server
Error).

A few words about order and sorting:
- openHAB Core seems to sort the cron strategies.
- Configurations itself are unsorted, they could be sorted
alphabetically by the UI.
- Items of configuration are sorted by their type (groups before normal
Items) as well as alphabetically.

--
Signed-off-by: Florian Hotze <[email protected]>
Co-authored-by: J-N-K <[email protected]>
  • Loading branch information
florian-h05 authored Jun 28, 2023
1 parent 7cb786a commit 376f036
Show file tree
Hide file tree
Showing 8 changed files with 1,079 additions and 0 deletions.
13 changes: 13 additions & 0 deletions bundles/org.openhab.ui/web/src/js/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ const InboxListPage = () => import(/* webpackChunkName: "admin-config" */ '../pa
const TransformationsListPage = () => import(/* webpackChunkName: "admin-config" */ '../pages/settings/transformations/transformations-list.vue')
const TransformationsEditPage = () => import(/* webpackChunkName: "admin-rules" */ '../pages/settings/transformations/transformation-edit.vue')

const PersistenceEditPage = () => import(/* webpackChunkName: "admin-config" */ '../pages/settings/persistence/persistence-edit.vue')

const SemanticModelPage = () => import(/* webpackChunkName: "admin-config" */ '../pages/settings/model/model.vue')

const PagesListPage = () => import(/* webpackChunkName: "admin-pages" */ '../pages/settings/pages/pages-list.vue')
Expand Down Expand Up @@ -237,6 +239,17 @@ export default [
beforeEnter: [enforceAdminForRoute],
async: loadAsync(SemanticModelPage)
},
{
path: 'persistence/',
routes: [
{
path: ':serviceId',
beforeEnter: [enforceAdminForRoute],
beforeLeave: [checkDirtyBeforeLeave],
async: loadAsync(PersistenceEditPage)
}
]
},
{
path: 'rules/',
beforeEnter: [enforceAdminForRoute],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@
</f7-link>
</f7-nav-right>
</f7-navbar>
<f7-block v-if="type === 'persistence'" class="service-config block-narrow">
<f7-col>
<f7-block-title medium>
<f7-link color="blue" :href="'/settings/persistence/' + name">
Persistence configuration
</f7-link>
</f7-block-title>
</f7-col>
</f7-block>
<f7-block form v-if="configDescription && config" class="service-config block-narrow">
<f7-col>
<f7-block-title medium>
Expand Down Expand Up @@ -74,6 +83,14 @@ export default {
strippedAddonId: ''
}
},
computed: {
type () {
return this.addonId.split('-')[0]
},
name () {
return this.addonId.split('-')[1]
}
},
methods: {
save () {
let promises = []
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<template>
<f7-popup ref="modulePopup" class="moduleconfig-popup">
<f7-page>
<f7-navbar>
<f7-nav-left>
<f7-link icon-ios="f7:arrow_left" icon-md="material:arrow_back" icon-aurora="f7:arrow_left" popup-close />
</f7-nav-left>
<f7-nav-title>
Configure strategies and filters for Item(s)
</f7-nav-title>
<f7-nav-right>
<f7-link v-show="currentConfiguration.items.length > 0" @click="updateModuleConfig">
Done
</f7-link>
</f7-nav-right>
</f7-navbar>
<f7-block class="no-margin no-padding">
<f7-col>
<f7-block-title medium class="padding-bottom">
Items
</f7-block-title>
<f7-list>
<item-picker title="Select groups" name="groupItems" multiple="true"
filterType="Group" :value="groupItems" @input="selectGroupItems" />
<f7-list-item>... whose members are to be persisted.</f7-list-item>
</f7-list>
<f7-list>
<item-picker title="Select Items" name="items" multiple="true" :value="items"
@input="selectItems" />
<f7-list-item>... to be persisted.</f7-list-item>
</f7-list>
</f7-col>
<f7-col>
<f7-block-title medium class="padding-bottom">
Strategies
</f7-block-title>
<strategy-picker title="Select strategies" name="strategies" :strategies="strategies"
:value="currentConfiguration.strategies"
@strategiesSelected="currentConfiguration.strategies = $event" />
</f7-col>
<f7-col>
<f7-block-title medium class="padding-bottom">
Filters
</f7-block-title>
<filter-picker :filters="filters"
:value="currentConfiguration.filters"
@filtersSelected="currentConfiguration.filters = $event" />
</f7-col>
</f7-block>
</f7-page>
</f7-popup>
</template>

<script>
import ItemPicker from '@/components/config/controls/item-picker.vue'
import StrategyPicker from '@/pages/settings/persistence/strategy-picker.vue'
import FilterPicker from '@/pages/settings/persistence/filter-picker.vue'
export default {
components: { FilterPicker, StrategyPicker, ItemPicker },
props: ['configuration', 'strategies', 'filters'],
emits: ['configurationUpdate'],
data () {
return {
currentConfiguration: this.configuration || {
items: [],
strategies: [
'everyChange'
],
filters: []
}
}
},
computed: {
groupItems () {
return this.currentConfiguration.items.filter((i) => i.endsWith('*')).map((i) => i.slice(0, -1))
},
items () {
return this.currentConfiguration.items.filter((i) => !i.endsWith('*'))
}
},
methods: {
selectGroupItems (ev) {
this.currentConfiguration.items = ev.sort((a, b) => a.localeCompare(b)).map((i) => i + '*').concat(this.items)
},
selectItems (ev) {
this.currentConfiguration.items = this.groupItems.map((i) => i + '*').concat(ev.sort((a, b) => a.localeCompare(b)))
},
updateModuleConfig () {
if (this.currentConfiguration.items.length === 0) {
this.$f7.dialog.alert('Please select Items')
return
}
this.$f7.emit('configurationUpdate', this.currentConfiguration)
this.$refs.modulePopup.close()
}
}
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<template>
<f7-popup ref="modulePopup" class="moduleconfig-popup">
<f7-page>
<f7-navbar>
<f7-nav-left>
<f7-link icon-ios="f7:arrow_left" icon-md="material:arrow_back" icon-aurora="f7:arrow_left" popup-close />
</f7-nav-left>
<f7-nav-title>
Configure cron strategy
</f7-nav-title>
<f7-nav-right>
<f7-link v-show="currentCronStrategy.name && currentCronStrategy.cronExpression" @click="updateModuleConfig">
Done
</f7-link>
</f7-nav-right>
</f7-navbar>
<f7-block class="no-margin no-padding">
<f7-col>
<f7-list>
<f7-list-input ref="name" label="Name" type="text" placeholder="Required" :value="currentCronStrategy.name"
@input="currentCronStrategy.name = $event.target.value"
:disabled="!createMode"
:info="(createMode) ? 'Note: cannot be changed after the creation' : ''"
required validate pattern="[A-Za-z0-9_]+" error-message="Required. A-Z,a-z only" />
</f7-list>
</f7-col>
<f7-col>
<f7-block-title medium class="padding-bottom">
Configuration
</f7-block-title>
<f7-list>
<parameter-cronexpression ref="cronExpression" :configDescription="cronExpressionConfigDescription"
:value="currentCronStrategy.cronExpression"
@input="currentCronStrategy.cronExpression = $event" />
</f7-list>
</f7-col>
</f7-block>
</f7-page>
</f7-popup>
</template>

<script>
import ParameterCronexpression from '@/components/config/controls/parameter-cronexpression.vue'
export default {
components: {
ParameterCronexpression
},
props: ['cronStrategy'],
emits: ['cronStrategyConfigUpdate'],
data () {
return {
createMode: !this.cronStrategy,
currentCronStrategy: this.cronStrategy || {
name: null,
cronExpression: null
},
cronExpressionConfigDescription: {
label: 'Cron Expression',
name: 'cronExpression',
required: true
}
}
},
methods: {
updateModuleConfig () {
if (!this.$f7.input.validateInputs(this.$refs.name.$el) && !this.$f7.input.validateInputs(this.$refs.cronExpression.$el)) {
this.$f7.dialog.alert('Please review the configuration and correct validation errors')
return
}
this.$f7.emit('cronStrategyConfigUpdate', this.currentCronStrategy)
this.$refs.modulePopup.close()
}
}
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<template>
<f7-list class="strategy-picker-container" v-if="filters">
<f7-list-item title="Select filters" :smart-select="disabled !== true && filters.length > 0"
:smart-select-params="smartSelectParams"
ref="smartSelect" class="defaults-picker">
<select v-if="disabled !== true && filters.length > 0" name="filters" multiple @change="select">
<option v-for="s in filters" :key="s" :value="s"
:selected="value.includes(s)">
{{ s }}
</option>
</select>
<div v-else-if="disabled === true">
{{ value.join(', ') }}
</div>
<div v-else-if="disabled !== true && filters.length === 0">
No filters available. Please add them first.
</div>
</f7-list-item>
</f7-list>
</template>

<style lang="stylus">
.strategy-picker-container
.item-content
padding-left calc(var(--f7-list-item-padding-horizontal) / 2 + var(--f7-safe-area-left))
.item-media
padding 0
.item-inner:after
display none
</style>

<script>
export default {
props: ['filters', 'value', 'disabled'],
emits: ['filtersSelected'],
data () {
return {
smartSelectParams: {
view: this.$f7.view.main,
openIn: 'popup',
virtualList: true,
virtualListHeight: (this.$theme.aurora) ? 32 : undefined
}
}
},
methods: {
select () {
this.$f7.input.validateInputs(this.$refs.smartSelect.$el)
const value = this.$refs.smartSelect.f7SmartSelect.getValue()
this.$emit('filtersSelected', value)
}
}
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<template>
<f7-popup ref="modulePopup" class="moduleconfig-popup">
<f7-page>
<f7-navbar>
<f7-nav-left>
<f7-link icon-ios="f7:arrow_left" icon-md="material:arrow_back" icon-aurora="f7:arrow_left" popup-close />
</f7-nav-left>
<f7-nav-title>
Configure {{ filterType.label.toLowerCase() }} filter
</f7-nav-title>
<f7-nav-right>
<f7-link v-show="currentFilter.name" @click="updateModuleConfig">
Done
</f7-link>
</f7-nav-right>
</f7-navbar>
<f7-block class="no-margin no-padding">
<f7-col>
<f7-list>
<f7-list-input ref="name" label="Name" type="text" placeholder="Required" :value="currentFilter.name"
@input="currentFilter.name = $event.target.value"
:disabled="!createMode"
:info="(createMode) ? 'Note: cannot be changed after the creation' : ''"
required validate pattern="[A-Za-z0-9_]+" error-message="Required. A-Z,a-z only" />
</f7-list>
</f7-col>
<f7-col>
<f7-block-title medium>
Configuration
</f7-block-title>
<config-sheet ref="config-sheet" :parameter-groups="[]" :parameters="filterConfigDescriptionParameters"
:configuration="currentFilter" />
</f7-col>
</f7-block>
</f7-page>
</f7-popup>
</template>

<script>
import ConfigSheet from '@/components/config/config-sheet.vue'
export default {
components: { ConfigSheet },
props: ['filter', 'filterType', 'filterConfigDescriptionParameters'],
emits: ['filterUpdate'],
data () {
return {
createMode: !this.filter,
currentFilter: this.filter || {
name: null
}
}
},
methods: {
updateModuleConfig () {
if (!this.$refs['config-sheet'].isValid()) {
this.$f7.dialog.alert('Please review the configuration and correct validation errors')
return
}
if (this.filterType.name === 'includeFilters') {
if (this.currentFilter.upper <= this.currentFilter.lower) {
this.$f7.dialog.alert('The lower bound value must be less than the upper bound value')
return
}
}
this.$f7.emit('filterUpdate', this.currentFilter, this.filterType.name)
this.$refs.modulePopup.close()
}
}
}
</script>
Loading

0 comments on commit 376f036

Please sign in to comment.