Skip to content

Commit 85a9cf1

Browse files
authored
Merge pull request #1463 from FlowFuse/679-sync-widgets
Widget Sync - Add new widget-sync event
2 parents 9bac4c2 + 841834e commit 85a9cf1

File tree

12 files changed

+82
-12
lines changed

12 files changed

+82
-12
lines changed
84.6 KB
Loading

docs/contributing/guides/events.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@ Some examples of events that are emitted from the Dashboard to Node-RED include:
5454
- `widget-action` - When a user interacts with a widget, and state of the widget is not important, e.g. a button click
5555
- `widget-send` - Used by `ui-template` to send a custom `msg` object, e.g. `send(msg)`, which will be stored in the server-side data store.
5656

57+
#### Syncing Widgets
58+
59+
The `widget-change` event is used to emit input from the server, and represents a change of state for that widget, e.g. a switch can be on/off by a user clicking it. In this case, if you have multiple clients connected to the same Node-RED instance, Dashboard will ensure that clients are in-sync when values change.
60+
61+
For Example if you move a slider on one instance of the Dashboard, all sliders connected will also auto-update.
62+
63+
To disable this "single source of truth" design pattern, you can check the widget type in the ["Client Data"](../../user/multi-tenancy#configuring-client-data) tab of the Dashboard settings.
64+
5765
## Events List
5866

5967
This is a comprehensive list of all events that are sent between Node-RED and the Dashboard via socket.io.
@@ -100,6 +108,11 @@ and the `widget-change` received a new value of `40`, then the newly emitted mes
100108

101109
Any value received here will also be stored against the widget in the datastore.
102110

111+
### `widget-sync`
112+
- Payload: `<msg>`
113+
114+
Triggered from the server-side `onChange` handler. This send a message out to all connected clients and informs relevant widgets of state/value changes. For example, when a slider is moved, the `widget-sync` message will ensure all connected clients, and their respective slider, are updated with the new value.
115+
103116
### `widget-action`
104117
- ID: `<node-id>`
105118
- Payload: `<msg>`

nodes/config/ui_base.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1953,7 +1953,9 @@
19531953
<p>
19541954
This tab allows you to control whether or not client-specific data is included in messages,
19551955
and which nodes accept it in order to constrain communication to specific clients.
1956-
You can read more about it <a href="https://dashboard.flowfuse.com/user/sidebar.html#client-data" target="_blank">here</a>
1956+
You can read more about it <a href="https://dashboard.flowfuse.com/user/sidebar.html#client-data" target="_blank">here</a>.
1957+
This is also used to disable syncing between clients, meaning that widgets will automatically update
1958+
their values across multiple client connections, e.g. toggling a switch in one client, will update all other clients too.
19571959
</p>
19581960
</div>
19591961
<div class="form-row form-row-flex">

nodes/config/ui_base.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -368,9 +368,9 @@ module.exports = function (RED) {
368368
* @param {Object} msg
369369
* @param {Object} wNode - the Node-RED node that is emitting the event
370370
*/
371-
function emit (event, msg, wNode) {
371+
function emit (event, msg, wNode, exclude) {
372372
Object.values(uiShared.connections).forEach(conn => {
373-
if (canSendTo(conn, wNode, msg)) {
373+
if (canSendTo(conn, wNode, msg) && (!exclude || exclude.indexOf(conn.id) === -1)) {
374374
conn.emit(event, msg)
375375
}
376376
})
@@ -651,6 +651,8 @@ module.exports = function (RED) {
651651
msg = await widgetEvents.beforeSend(msg)
652652
}
653653
datastore.save(n, wNode, msg)
654+
const exclude = [conn.id] // sync this change to all clients with the same widget
655+
emit('widget-sync:' + id, msg, wNode, exclude) // let all other connect clients now about the value change
654656
wNode.send(msg) // send the msg onwards
655657
}
656658

ui/src/widgets/data-tracker.mjs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { inject, onMounted, onUnmounted } from 'vue'
22
import { useStore } from 'vuex'
33

44
// by convention, composable function names start with "use"
5-
export function useDataTracker (widgetId, onInput, onLoad, onDynamicProperties) {
5+
export function useDataTracker (widgetId, onInput, onLoad, onDynamicProperties, onSync) {
66
if (!widgetId) {
77
throw new Error('widgetId is required')
88
}
@@ -68,6 +68,21 @@ export function useDataTracker (widgetId, onInput, onLoad, onDynamicProperties)
6868
}
6969
}
7070

71+
function onWidgetSync (msg) {
72+
// only care about msg.payload here as it's a change of value sync
73+
if (onSync) {
74+
onSync(msg)
75+
} else {
76+
// only need the msg.payload
77+
store.commit('data/bind', {
78+
widgetId,
79+
msg: {
80+
payload: msg.payload
81+
}
82+
})
83+
}
84+
}
85+
7186
function onMsgInput (msg) {
7287
// check for common dynamic properties cross all widget types
7388
checkDynamicProperties(msg)
@@ -106,6 +121,7 @@ export function useDataTracker (widgetId, onInput, onLoad, onDynamicProperties)
106121
socket?.off('disconnect', onDisconnect)
107122
socket?.off('msg-input:' + widgetId, onMsgInput)
108123
socket?.off('widget-load:' + widgetId, onWidgetLoad)
124+
socket?.off('widget-sync:' + widgetId, onWidgetSync)
109125
socket?.off('connect', onConnect)
110126
}
111127

@@ -118,6 +134,9 @@ export function useDataTracker (widgetId, onInput, onLoad, onDynamicProperties)
118134
socket.on('disconnect', onDisconnect)
119135
socket.on('msg-input:' + widgetId, onMsgInput)
120136
socket.on('widget-load:' + widgetId, onWidgetLoad)
137+
// when a widget in a different client has a value change
138+
socket.on('widget-sync:' + widgetId, onWidgetSync)
139+
// When SocketIO connects
121140
socket.on('connect', onConnect)
122141

123142
// let Node-RED know that this widget has loaded

ui/src/widgets/ui-button-group/UIButtonGroup.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export default {
6363
},
6464
created () {
6565
// can't do this in setup as we are using custom onInput function that needs access to 'this'
66-
this.$dataTracker(this.id, this.onInput, this.onLoad, this.onDynamicProperty)
66+
this.$dataTracker(this.id, this.onInput, this.onLoad, this.onDynamicProperty, this.onSync)
6767
6868
// let Node-RED know that this widget has loaded
6969
this.$socket.emit('widget-load', this.id)
@@ -113,6 +113,9 @@ export default {
113113
this.updateDynamicProperty('options', updates.options)
114114
}
115115
},
116+
onSync (msg) {
117+
this.selection = msg.payload
118+
},
116119
onChange (value) {
117120
if (value !== null && typeof value !== 'undefined') {
118121
// Tell Node-RED a new value has been selected

ui/src/widgets/ui-dropdown/UIDropdown.vue

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export default {
8686
},
8787
created () {
8888
// can't do this in setup as we are using custom onInput function that needs access to 'this'
89-
this.$dataTracker(this.id, null, this.onLoad, this.onDynamicProperties)
89+
this.$dataTracker(this.id, null, this.onLoad, this.onDynamicProperties, this.onSync)
9090
9191
// let Node-RED know that this widget has loaded
9292
this.$socket.emit('widget-load', this.id)
@@ -135,6 +135,12 @@ export default {
135135
this.updateDynamicProperty('msgTrigger', updates.msgTrigger)
136136
}
137137
},
138+
onSync (msg) {
139+
// update the UI with any changes
140+
if (typeof msg?.payload !== 'undefined') {
141+
this.value = msg.payload
142+
}
143+
},
138144
onChange () {
139145
// ensure our data binding with vuex store is updated
140146
const msg = this.messages[this.id] || {}

ui/src/widgets/ui-number-input/UINumberInput.vue

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ export default {
170170
},
171171
created () {
172172
// can't do this in setup as we are using custom onInput function that needs access to 'this'
173-
this.$dataTracker(this.id, this.onInput, this.onLoad, this.onDynamicProperties)
173+
this.$dataTracker(this.id, this.onInput, this.onLoad, this.onDynamicProperties, this.onSync)
174174
},
175175
methods: {
176176
onInput (msg) {
@@ -196,6 +196,12 @@ export default {
196196
this.previousValue = msg.payload
197197
}
198198
},
199+
onSync (msg) {
200+
if (typeof (msg?.payload) !== 'undefined') {
201+
this.textValue = msg.payload
202+
this.previousValue = msg.payload
203+
}
204+
},
199205
send () {
200206
this.$socket.emit('widget-change', this.id, this.value)
201207
},

ui/src/widgets/ui-radio-group/UIRadioGroup.vue

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export default {
6969
},
7070
created () {
7171
// can't do this in setup as we are using custom onInput function that needs access to 'this'
72-
this.$dataTracker(this.id, this.onInput, this.onLoad, this.onDynamicProperties)
72+
this.$dataTracker(this.id, this.onInput, this.onLoad, this.onDynamicProperties, this.onSync)
7373
7474
// let Node-RED know that this widget has loaded
7575
this.$socket.emit('widget-load', this.id)
@@ -123,7 +123,7 @@ export default {
123123
},
124124
select (value) {
125125
// An empty string value can be used to clear the current selection
126-
if (value !== '') {
126+
if (value !== '' && typeof (value) !== 'undefined') {
127127
const option = this.options.find((o) => {
128128
return o.value === value
129129
})
@@ -145,6 +145,9 @@ export default {
145145
this.updateDynamicProperty('label', updates.label)
146146
this.updateDynamicProperty('columns', updates.columns)
147147
this.updateDynamicProperty('options', updates.options)
148+
},
149+
onSync (msg) {
150+
this.select(msg.payload)
148151
}
149152
}
150153
}

ui/src/widgets/ui-slider/UISlider.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export default {
132132
}
133133
},
134134
created () {
135-
this.$dataTracker(this.id, null, this.onLoad, this.onDynamicProperties)
135+
this.$dataTracker(this.id, null, this.onLoad, this.onDynamicProperties, this.onSync)
136136
},
137137
mounted () {
138138
const val = this.messages[this.id]?.payload
@@ -196,6 +196,11 @@ export default {
196196
this.updateDynamicProperty('colorThumb', updates.colorThumb)
197197
this.updateDynamicProperty('showTextField', updates.showTextField)
198198
},
199+
onSync (msg) {
200+
if (typeof msg?.payload !== 'undefined') {
201+
this.sliderValue = Number(msg.payload)
202+
}
203+
},
199204
// Validate the text field input
200205
validateInput () {
201206
this.textFieldValue = this.roundToStep(this.textFieldValue)

0 commit comments

Comments
 (0)