Skip to content

Commit

Permalink
Merge pull request #1099 from FlowFuse/1076_expose-data-tracker-compo…
Browse files Browse the repository at this point in the history
…sable

Expose the dataTracker composable to enable its use in third-party nodes
  • Loading branch information
joepavitt authored Jul 28, 2024
2 parents af488b6 + fbf42ca commit 0cac0f2
Show file tree
Hide file tree
Showing 24 changed files with 100 additions and 85 deletions.
4 changes: 3 additions & 1 deletion cypress/tests/widgets/form.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ describe('Node-RED Dashboard 2.0 - Forms', () => {
cy.get('[data-form="form-row-name"]').find('input[type="text"]').type('John Smith', { force: true })
cy.focused().blur()

cy.get('[data-action="form-submit"]').should('not.be.disabled')
cy.get('#nrdb-ui-group-dashboard-ui-group').within(() => {
cy.get('[data-action="form-submit"]').should('not.be.disabled')
})
})
})

Expand Down
40 changes: 29 additions & 11 deletions docs/contributing/widgets/core-widgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ When adding a new widget to Dashboard 2.0, you'll need to ensure that the follow
export default {
name: 'DBUIWidget',
// we need to inject $socket so that we can send events to Node-RED
inject: ['$socket'],
inject: ['$socket', '$dataTracker'],
props: {
id: String, // the id of the widget, as defined by Node-RED
props: Object, // the properties for this widget defined in the Node-RED editor
Expand All @@ -63,10 +63,9 @@ When adding a new widget to Dashboard 2.0, you'll need to ensure that the follow
// received on input from Node-RED
...mapState('data', ['messages']), // provides access to `this.messages` where `this.messages[this.id]` is the stored msg for this widget
},
setup (props) {
// Use our data-tracker, which setups up the basic event handling for us
// including `on('msg-input')` and `on('widget-load')`
useDataTracker(props.id)
created () {
// setup the widget with default onInput, onLoad and onDynamicProperties handlers
this.$dataTracker(this.id)
},
methods: {
onAction () {
Expand All @@ -87,19 +86,38 @@ When adding a new widget to Dashboard 2.0, you'll need to ensure that the follow

## Data Tracker

The data tracker is a set of utility functions that help setup the standard event handlers for a core widget. It will setup the following events:
The data tracker is a globally available utility service that helps setup the standard event handlers for widgets.

- `on('widget-load')` - to handle any initial data that is sent to the widget when it is loaded
- `on('msg-input')` - to handle any incoming data from Node-RED
### Usage

It also provides flexibility to define custom event handlers for the widget if there is bespoke functionality required for a given node, for example in a `ui-chart` node, we have a collection of custom logic that handles the merging of data points and the rendering of the chart when a message is received.
The data tracker is globally available across existing widgets and can be accessed using `this.$dataTracker(...)`.

The inputs for the `useDataTracker (widgetId, onInput, onLoad, onDynamicProperties)` function are used as follows:
The most simple usage of the tracker would be:

```js
...
created () {
this.$dataTracker(this.id)
},
...
```

This will setup the following events:

- `on('widget-load')` - Ensures we save any received `msg` objects when a widget is first loaded into the Dashboard.
- `on('msg-input')` - Default behavior checks for any dynamic properties (e.g. visibility, disabled state) and also stores the incoming `msg` in the Vuex store

### Custom Behaviours

It also provides flexibility to define custom event handlers for a given widget, for example in a `ui-chart` node, we have a logic that handles the merging of data points and the rendering of the chart when a message is received.

The inputs for the `this.$dataTracker(widgetId, onInput, onLoad, onDynamicProperties)` function are used as follows:

- `widgetId` - the unique ID of the widget
- `onInput` - a function that will be called when a message is received from Node-RED through the `on(msg-input)` socket handler
- `onLoad` - a function that will be called when the widget is loaded, and triggered by the `widget-load` event
- `onDynamicProperties` - a function called as part of the `on(msg-input)` event, and is triggered _before_ the default `onInput` function. This is a good entry point to check against any properties that have been included in the `msg` in order to set a dynamic property.
- `onDynamicProperties` - a function called as part of the `on(msg-input)` event, and is triggered _before_ the default `onInput` function. This is a good entry point to check against any properties that have been included in the `msg` in order to set a dynamic property (i.e. content sent into `msg.ui_update...`).


## Dynamic Properties

Expand Down
18 changes: 18 additions & 0 deletions docs/contributing/widgets/third-party.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,23 @@ export default {
}
```
It is recommended to use our in built [Data Tracker](../widgets/core-widgets.md#data-tracker) to setup the standard input/load events for your widget. This can be done by calling the following in your widget's `.vue` file:
```js
export default {
inject: ['$dataTracker'],
// rest of your vue component here
created () {
this.$dataTracker(this.id)
// we can override the default events if we want to with
// this.$dataTracker(this.id, myOnInputFunction, myOnLoadFunction, myOnDynamicPropertiesFunction)
}
}
```
More details on customisation of the Data Tracker can be found [here](../widgets/core-widgets.md#custom-behaviours).
#### Sending Node-RED Messages
You can send a `msg` on to any connected nodes in Node-RED by calling one of the following events via SocketIO:
Expand Down Expand Up @@ -315,6 +332,7 @@ We use the concept of data stores on both the client and server side of Dashboar
Data stores are a mapping of the widget/node's ID to the latest data received into that widget. This is most commonly used to restore state when the Dashboard is refreshed.
#### Node-RED Data Store
Node-RED's data store is made accessible for third-party widgets via the associated `ui-base`.
Expand Down
2 changes: 1 addition & 1 deletion ui/src/debug/Debug.vue
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export default {
components: {
'debug-data': DebugData
},
inject: ['$socket'],
inject: ['$socket', '$dataTracker'],
data () {
return {
view: {
Expand Down
2 changes: 2 additions & 0 deletions ui/src/main.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { VTreeview } from 'vuetify/labs/VTreeview'
import './stylesheets/common.css'

import store from './store/index.mjs'
import { useDataTracker } from './widgets/data-tracker.mjs' // eslint-disable-line import/order

// set a base theme on which we will add our custom NR-defined theme
const theme = {
Expand Down Expand Up @@ -200,6 +201,7 @@ fetch('_setup')

// make the socket service available app-wide via this.$socket
app.provide('$socket', socket)
app.provide('$dataTracker', useDataTracker)

// mount the VueJS app into <div id="app"></div> in /ui/public/index.html
app.mount('#app')
Expand Down
6 changes: 2 additions & 4 deletions ui/src/widgets/ui-button-group/UIButtonGroup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,9 @@
<script>
import { mapState } from 'vuex'
import { useDataTracker } from '../data-tracker.mjs'
export default {
name: 'DBUIButtonGroup',
inject: ['$socket'],
inject: ['$socket', '$dataTracker'],
props: {
id: { type: String, required: true },
props: { type: Object, default: () => ({}) },
Expand Down Expand Up @@ -68,7 +66,7 @@ export default {
},
created () {
// can't do this in setup as we are using custom onInput function that needs access to 'this'
useDataTracker(this.id, this.onInput, this.onLoad, this.onDynamicProperty)
this.$dataTracker(this.id, this.onInput, this.onLoad, this.onDynamicProperty)
// let Node-RED know that this widget has loaded
this.$socket.emit('widget-load', this.id)
Expand Down
5 changes: 2 additions & 3 deletions ui/src/widgets/ui-button/UIButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,11 @@
</template>
<script>
import { useDataTracker } from '../data-tracker.mjs' // eslint-disable-line import/order
import { mapState } from 'vuex' // eslint-disable-line import/order
export default {
name: 'DBUIButton',
inject: ['$socket'],
inject: ['$socket', '$dataTracker'],
props: {
id: { type: String, required: true },
props: { type: Object, default: () => ({}) },
Expand Down Expand Up @@ -70,7 +69,7 @@ export default {
}
},
created () {
useDataTracker(this.id, null, null, this.onDynamicProperties)
this.$dataTracker(this.id, null, null, this.onDynamicProperties)
},
methods: {
action ($evt) {
Expand Down
6 changes: 2 additions & 4 deletions ui/src/widgets/ui-chart/UIChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,12 @@
import { Chart } from 'chart.js/auto' // eslint-disable-line n/file-extension-in-import
import 'chartjs-adapter-luxon'
import { useDataTracker } from '../data-tracker.mjs' // eslint-disable-line import/order
import { shallowRef } from 'vue'
import { mapState } from 'vuex'
export default {
name: 'DBUIChart',
inject: ['$socket'],
inject: ['$socket', '$dataTracker'],
props: {
id: { type: String, required: true },
props: { type: Object, default: () => ({}) }
Expand Down Expand Up @@ -54,7 +52,7 @@ export default {
},
created () {
// can't do this in setup as we have custom onInput function
useDataTracker(this.id, this.onMsgInput, this.onLoad)
this.$dataTracker(this.id, this.onMsgInput, this.onLoad)
},
mounted () {
// get a reference to the canvas element
Expand Down
2 changes: 1 addition & 1 deletion ui/src/widgets/ui-control/UIControl.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { mapGetters, mapState } from 'vuex'
export default {
name: 'DBUIControl',
inject: ['$socket'],
inject: ['$socket', '$dataTracker'],
props: {
id: { type: String, required: true },
props: { type: Object, default: () => ({}) }
Expand Down
6 changes: 2 additions & 4 deletions ui/src/widgets/ui-dropdown/UIDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,9 @@
<script>
import { mapState } from 'vuex'
import { useDataTracker } from '../data-tracker.mjs'
export default {
name: 'DBUIDropdown',
inject: ['$socket'],
inject: ['$socket', '$dataTracker'],
props: {
id: { type: String, required: true },
props: { type: Object, default: () => ({}) },
Expand Down Expand Up @@ -81,7 +79,7 @@ export default {
},
created () {
// can't do this in setup as we are using custom onInput function that needs access to 'this'
useDataTracker(this.id, null, this.onLoad, this.onDynamicProperties)
this.$dataTracker(this.id, null, this.onLoad, this.onDynamicProperties)
// let Node-RED know that this widget has loaded
this.$socket.emit('widget-load', this.id)
Expand Down
5 changes: 2 additions & 3 deletions ui/src/widgets/ui-event/UIEvent.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
<script>
import { mapState } from 'vuex'
import { useDataTracker } from '../data-tracker.mjs' // eslint-disable-line import/order
export default {
name: 'DBUIEvent',
inject: ['$socket'],
inject: ['$socket', '$dataTracker'],
props: {
id: { type: String, required: true },
props: { type: Object, default: () => ({}) }
Expand Down Expand Up @@ -38,7 +37,7 @@ export default {
},
created () {
// can't do this in setup as we have custom onInput function
useDataTracker(this.id, null, this.onLoad)
this.$dataTracker(this.id, null, this.onLoad)
this.page = this.pages[this.$route.meta.id]
this.pageview()
},
Expand Down
10 changes: 4 additions & 6 deletions ui/src/widgets/ui-file-input/UIFileInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,16 @@
</template>

<script>
import { useDataTracker } from '../data-tracker.mjs' // eslint-disable-line import/order
import { mapState } from 'vuex' // eslint-disable-line import/order
export default {
name: 'DBUIFileInput',
inject: ['$socket'],
inject: ['$socket', '$dataTracker'],
props: {
id: { type: String, required: true },
props: { type: Object, default: () => ({}) },
state: { type: Object, default: () => ({}) }
},
setup (props) {
useDataTracker(props.id)
},
data () {
return {
files: null,
Expand Down Expand Up @@ -90,6 +85,9 @@ export default {
return !this.uploading && !this.uploaded
}
},
created () {
this.$dataTracker(this.id)
},
methods: {
formatFileSize (bytes) {
if (bytes === 0) return '0 Bytes'
Expand Down
6 changes: 2 additions & 4 deletions ui/src/widgets/ui-form/UIForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,11 @@
</template>

<script>
import { useDataTracker } from '../data-tracker.mjs' // eslint-disable-line import/order
import { mapState } from 'vuex' // eslint-disable-line import/order
export default {
name: 'DBUIForm',
inject: ['$socket'],
inject: ['$socket', '$dataTracker'],
props: {
id: { type: String, required: true },
props: { type: Object, default: () => ({}) },
Expand Down Expand Up @@ -63,7 +61,7 @@ export default {
},
created () {
// can't do this in setup as we are using custom onInput function that needs access to 'this'
useDataTracker(this.id, this.onInput, null, this.onDynamicProperties)
this.$dataTracker(this.id, this.onInput, null, this.onDynamicProperties)
},
mounted () {
this.reset()
Expand Down
9 changes: 4 additions & 5 deletions ui/src/widgets/ui-gauge/UIGauge.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
</template>

<script>
import { useDataTracker } from '../data-tracker.mjs' // eslint-disable-line import/order
import { mapState } from 'vuex' // eslint-disable-line import/order
import UIGaugeDial from './types/UIGaugeDial.vue'
Expand All @@ -16,15 +15,12 @@ export default {
'ui-gauge-dial': UIGaugeDial,
'ui-gauge-tile': UIGaugeTile
},
inject: ['$socket'],
inject: ['$socket', '$dataTracker'],
props: {
id: { type: String, required: true },
props: { type: Object, default: () => ({}) },
state: { type: Object, default: () => ({}) }
},
setup (props) {
useDataTracker(props.id)
},
computed: {
...mapState('data', ['messages']),
value: function () {
Expand All @@ -33,6 +29,9 @@ export default {
icon () {
return this.props.icon?.replace(/^mdi-/, '')
}
},
created () {
this.$dataTracker(this.id)
}
}
</script>
Expand Down
2 changes: 1 addition & 1 deletion ui/src/widgets/ui-gauge/types/UIGaugeDial.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import * as d3 from 'd3' // eslint-disable-line import/order

export default {
name: 'DBUIGaugeDial',
inject: ['$socket'],
inject: ['$socket', '$dataTracker'],
props: {
id: { type: String, required: true },
props: { type: Object, default: () => ({}) },
Expand Down
6 changes: 2 additions & 4 deletions ui/src/widgets/ui-markdown/UIMarkdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import { marked } from 'marked'
import mermaid from 'mermaid'
import { mapState } from 'vuex' // eslint-disable-line import/order
import { useDataTracker } from '../data-tracker.mjs'
import { escapeHTML } from './../../util.mjs'
// set up mermaid
Expand Down Expand Up @@ -51,7 +49,7 @@ marked.use({ extensions: [customRenderer] })
export default {
name: 'DBUIMarkdown',
inject: ['$socket'],
inject: ['$socket', '$dataTracker'],
props: {
id: { type: String, required: true },
props: { type: Object, default: () => ({}) }
Expand All @@ -75,7 +73,7 @@ export default {
},
created () {
// can't do this in setup as we have custom onInput function
useDataTracker(this.id, this.onMsgInput, this.onMsgLoad)
this.$dataTracker(this.id, this.onMsgInput, this.onMsgLoad)
// make sure we render something on first creation
this.update()
},
Expand Down
Loading

0 comments on commit 0cac0f2

Please sign in to comment.