Skip to content

Commit

Permalink
Merge pull request FlowFuse#1465 from FlowFuse/1294-data-storage
Browse files Browse the repository at this point in the history
Repurpose the datastores to merge values, not replace
  • Loading branch information
Steve-Mcl authored Dec 18, 2024
2 parents c9e45ff + fa255c0 commit 357ebba
Show file tree
Hide file tree
Showing 8 changed files with 69 additions and 194 deletions.
7 changes: 4 additions & 3 deletions nodes/config/ui_base.js
Original file line number Diff line number Diff line change
Expand Up @@ -1052,12 +1052,13 @@ module.exports = function (RED) {
} else {
// msg could be null if the beforeSend errors and returns null
if (msg) {
// store the latest msg passed to node
datastore.save(n, widgetNode, msg)

if (widgetConfig.topic || widgetConfig.topicType) {
msg = await appendTopic(RED, widgetConfig, wNode, msg)
}

// store the latest msg passed to node
datastore.save(n, widgetNode, msg)

if (hasProperty(widgetConfig, 'passthru')) {
if (widgetConfig.passthru) {
send(msg)
Expand Down
16 changes: 15 additions & 1 deletion nodes/store/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ function canSaveInStore (base, node, msg) {
return checks.length === 0 || !checks.includes(false)
}

// Strip msg of properties that are not needed for storage
function stripMsg (msg) {
const newMsg = config.RED.util.cloneMessage(msg)

// don't need to store ui_updates in the datastore, as this is handled in statestore
delete newMsg.ui_update

return newMsg
}

const getters = {
RED () {
return config.RED
Expand Down Expand Up @@ -75,7 +85,11 @@ const setters = {
data[node.id] = filtered
} else {
if (canSaveInStore(base, node, msg)) {
data[node.id] = config.RED.util.cloneMessage(msg)
const newMsg = stripMsg(msg)
data[node.id] = {
...data[node.id],
...newMsg
}
}
}
},
Expand Down
23 changes: 5 additions & 18 deletions ui/src/store/data.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Vuex store for tracking data bound to each widget
*/

import { getDeepValue, hasProperty } from '../util.mjs'
import { getDeepValue } from '../util.mjs'

// initial state is empty - we don't know if we have any widgets
const state = () => ({
Expand All @@ -11,29 +11,16 @@ const state = () => ({
properties: {}
})

// map of supported property messages
// Any msg received with a topic matching a key in this object will be stored in the properties object under the value of the key
// e.g. { topic: 'ui-property:class', payload: 'my-class' } will be stored as { class: 'my-class' }
const supportedPropertyMessages = {
'ui-property:class': 'class'
}

const mutations = {
bind (state, data) {
const widgetId = data.widgetId
// if packet contains a msg, then we process it
if ('msg' in data) {
// first, if the msg.topic is a supported property message, then we store it in the properties object
// but do not store it in the messages object.
// This permits the widget to receive property messages without affecting the widget's value
if (data.msg?.topic && supportedPropertyMessages[data.msg.topic] && hasProperty(data.msg, 'payload')) {
const controlProperty = supportedPropertyMessages[data.msg.topic]
state.properties[widgetId] = state.properties[widgetId] || {}
state.properties[widgetId][controlProperty] = data.msg.payload
return // do not store in messages object
// merge with any existing data and override relevant properties
state.messages[widgetId] = {
...state.messages[widgetId],
...data.msg
}
// if the msg was not a property message, then we store it in the messages object
state.messages[widgetId] = data.msg
}
},
append (state, data) {
Expand Down
67 changes: 21 additions & 46 deletions ui/src/widgets/ui-button-group/UIButtonGroup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@ export default {
props: { type: Object, default: () => ({}) },
state: { type: Object, default: () => ({}) }
},
data () {
return {
selection: null
}
},
computed: {
...mapState('data', ['messages']),
selectedColor: function () {
Expand Down Expand Up @@ -59,63 +54,43 @@ export default {
})
}
return options
},
selection: {
get () {
const msg = this.messages[this.id]
let selection = null
if (msg) {
if (Array.isArray(msg.payload) && msg.payload.length === 0) {
selection = null
} else if (this.findOptionByValue(msg.payload) !== null) {
selection = msg.payload
}
}
return selection
},
set (value) {
if (!this.messages[this.id]) {
this.messages[this.id] = {}
}
this.messages[this.id].payload = value
}
}
},
created () {
// can't do this in setup as we are using custom onInput function that needs access to 'this'
this.$dataTracker(this.id, this.onInput, this.onLoad, this.onDynamicProperty, this.onSync)
this.$dataTracker(this.id, null, null, this.onDynamicProperty, null)
// let Node-RED know that this widget has loaded
this.$socket.emit('widget-load', this.id)
},
methods: {
onInput (msg) {
// update our vuex store with the value retrieved from Node-RED
this.$store.commit('data/bind', {
widgetId: this.id,
msg
})
// make sure our v-model is updated to reflect the value from Node-RED
if (msg.payload !== undefined) {
if (Array.isArray(msg.payload) && msg.payload.length === 0) {
this.selection = null
} else {
if (this.findOptionByValue(msg.payload) !== null) {
this.selection = msg.payload
}
}
}
},
onLoad (msg) {
if (msg) {
// update vuex store to reflect server-state
this.$store.commit('data/bind', {
widgetId: this.id,
msg
})
// make sure we've got the relevant option selected on load of the page
if (msg.payload !== undefined) {
if (Array.isArray(msg.payload) && msg.payload.length === 0) {
this.selection = null
} else {
if (this.findOptionByValue(msg.payload) !== null) {
this.selection = msg.payload
}
}
}
}
},
onDynamicProperty (msg) {
const updates = msg.ui_update
if (updates) {
this.updateDynamicProperty('label', updates.label)
this.updateDynamicProperty('options', updates.options)
}
},
onSync (msg) {
this.selection = msg.payload
},
onChange (value) {
if (value !== null && typeof value !== 'undefined') {
// Tell Node-RED a new value has been selected
Expand Down
52 changes: 6 additions & 46 deletions ui/src/widgets/ui-number-input/UINumberInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ export default {
data () {
return {
delayTimer: null,
textValue: null,
previousValue: null,
isCompressed: false
}
},
Expand Down Expand Up @@ -109,18 +107,18 @@ export default {
},
value: {
get () {
if (this.textValue === null || this.textValue === undefined || this.textValue === '') {
return this.textValue
const val = this.messages[this.id]?.payload
if (val === null || val === undefined || val === '') {
return val
} else {
return Number(this.textValue)
return Number(val)
}
},
set (val) {
if (this.value === val) {
return // no change
}
const msg = this.messages[this.id] || {}
this.textValue = val
msg.payload = val
this.messages[this.id] = msg
}
Expand Down Expand Up @@ -170,52 +168,14 @@ export default {
},
created () {
// can't do this in setup as we are using custom onInput function that needs access to 'this'
this.$dataTracker(this.id, this.onInput, this.onLoad, this.onDynamicProperties, this.onSync)
this.$dataTracker(this.id, null, null, this.onDynamicProperties, null)
},
methods: {
onInput (msg) {
// update our vuex store with the value retrieved from Node-RED
this.$store.commit('data/bind', {
widgetId: this.id,
msg
})
// make sure our v-model is updated to reflect the value from Node-RED
if (msg.payload !== undefined) {
this.textValue = msg.payload
}
},
onLoad (msg) {
// update vuex store to reflect server-state
this.$store.commit('data/bind', {
widgetId: this.id,
msg
})
// make sure we've got the relevant option selected on load of the page
if (msg?.payload !== undefined) {
this.textValue = msg.payload
this.previousValue = msg.payload
}
},
onSync (msg) {
if (typeof (msg?.payload) !== 'undefined') {
this.textValue = msg.payload
this.previousValue = msg.payload
}
},
send () {
this.$socket.emit('widget-change', this.id, this.value)
},
onChange () {
// Since the Vuetify Input Number component doesn't currently support an onClick event,
// compare the previous value with the current value and check whether the value has been increased or decreased by one.
if (
this.previousValue === null ||
this.previousValue + (this.step || 1) === this.value ||
this.previousValue - (this.step || 1) === this.value
) {
this.send()
}
this.previousValue = this.value
this.send()
},
onBlur: function () {
if (this.props.sendOnBlur) {
Expand Down
7 changes: 1 addition & 6 deletions ui/src/widgets/ui-slider/UISlider.vue
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export default {
}
},
created () {
this.$dataTracker(this.id, null, this.onLoad, this.onDynamicProperties, this.onSync)
this.$dataTracker(this.id, null, null, this.onDynamicProperties)
},
mounted () {
const val = this.messages[this.id]?.payload
Expand Down Expand Up @@ -196,11 +196,6 @@ export default {
this.updateDynamicProperty('colorThumb', updates.colorThumb)
this.updateDynamicProperty('showTextField', updates.showTextField)
},
onSync (msg) {
if (typeof msg?.payload !== 'undefined') {
this.sliderValue = Number(msg.payload)
}
},
// Validate the text field input
validateInput () {
this.textFieldValue = this.roundToStep(this.textFieldValue)
Expand Down
59 changes: 13 additions & 46 deletions ui/src/widgets/ui-text-input/UITextInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,22 @@ export default {
},
data () {
return {
delayTimer: null,
textValue: null
delayTimer: null
}
},
computed: {
...mapState('data', ['messages']),
value: {
get () {
return this.messages[this.id]?.payload
},
set (val) {
if (!this.messages[this.id]) {
this.messages[this.id] = {}
}
this.messages[this.id].payload = val
}
},
label: function () {
// Sanetize the html to avoid XSS attacks
return DOMPurify.sanitize(this.getProperty('label'))
Expand Down Expand Up @@ -103,20 +113,6 @@ export default {
iconInnerPosition () {
return this.getProperty('iconInnerPosition')
},
value: {
get () {
return this.textValue
},
set (val) {
if (this.value === val) {
return // no change
}
const msg = this.messages[this.id] || {}
this.textValue = val
msg.payload = val
this.messages[this.id] = msg
}
},
validation: function () {
if (this.type === 'email') {
return [v => !v || /^[^\s@]+@[^\s@]+$/.test(v) || 'E-mail must be valid']
Expand All @@ -127,38 +123,9 @@ export default {
},
created () {
// can't do this in setup as we are using custom onInput function that needs access to 'this'
this.$dataTracker(this.id, this.onInput, this.onLoad, this.onDynamicProperties, this.onSync)
this.$dataTracker(this.id, null, null, this.onDynamicProperties)
},
methods: {
onInput (msg) {
// update our vuex store with the value retrieved from Node-RED
this.$store.commit('data/bind', {
widgetId: this.id,
msg
})
// make sure our v-model is updated to reflect the value from Node-RED
if (msg.payload !== undefined) {
this.textValue = msg.payload
}
},
onLoad (msg) {
if (msg) {
// update vuex store to reflect server-state
this.$store.commit('data/bind', {
widgetId: this.id,
msg
})
// make sure we've got the relevant option selected on load of the page
if (msg.payload !== undefined) {
this.textValue = msg.payload
}
}
},
onSync (msg) {
if (typeof (msg.payload) !== 'undefined') {
this.textValue = msg.payload
}
},
send: function () {
this.$socket.emit('widget-change', this.id, this.value)
},
Expand Down
Loading

0 comments on commit 357ebba

Please sign in to comment.