Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Debug View & Dropdown Improvements #345

Merged
merged 7 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/.vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ export default ({ mode }) => {
collapsed: false,
items: [
{ text: 'Adding Core Widgets', link: '/contributing/widgets/core-widgets' },
{ text: 'Third Party Widgets', link: '/contributing/widgets/third-party' }
{ text: 'Third Party Widgets', link: '/contributing/widgets/third-party' },
{ text: 'Debugging', link: '/contributing/widgets/debugging' }
]
},
{
Expand Down
2 changes: 2 additions & 0 deletions docs/.vitepress/theme/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import './overlay.css'
import './widget-card.css'

import PropsTable from '../../components/PropsTable.vue'
import DynamicPropsTable from '../../components/DynamicPropsTable.vue'
import ControlsTable from '../../components/ControlsTable.vue'

export default {
Expand Down Expand Up @@ -48,6 +49,7 @@ export default {
enhanceApp(ctx) {
// register your custom global components
ctx.app.component('PropsTable', PropsTable)
ctx.app.component('DynamicPropsTable', DynamicPropsTable)
ctx.app.component('ControlsTable', ControlsTable)
}
}
32 changes: 32 additions & 0 deletions docs/components/DynamicPropsTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<template>
<p>Dynamic properties are those that can be overriden at runtime by sending a particular <code>msg</code> to the node.</p>
<p>Where appropriate, the underlying values set within Node-RED will be overriden by the values set in the received messages.</p>
<table style="width: 100%;">
<thead>
<tr>
<th>Prop</th>
<th>Payload</th>
<th>Structures</th>
</tr>
</thead>
<tbody>
<tr v-for="(value, property) in page.frontmatter?.dynamic" :key="property">
<td>{{ property }}</td>
<td><code>{{ value.payload }}</code></td>
<td><ul><li v-for="s in value.structure" :key="s"><code>{{ s }}</code></li></ul></td>
</tr>
</tbody>
</table>
</template>

<script setup>
import { useData } from 'vitepress' // eslint-disable-line import/named

const { page } = useData()
</script>

<script>
export default {
name: 'DynamicPropsTable'
}
</script>
2 changes: 1 addition & 1 deletion docs/components/MigrationWidgetProfile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<PropertyMigrationTable v-if="profile.msgs"
:properties="profile.msgs">
<template #header>
<h4>Message Options</h4>
<h4>Dynamic Properties</h4>
</template>
<template #description>
<p>
Expand Down
6 changes: 4 additions & 2 deletions docs/components/PropsTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
<thead>
<tr>
<th>Prop</th>
<th>Type</th>
<th>Dynamic</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr v-for="(value, property) in page.frontmatter?.props" :key="property">
<td>{{ property }}</td>
<td v-html="value"></td>
<td style="text-align: center;">{{ value.dynamic ? '&#x2713;' : '' }}</td>
<td v-html="value.description || value"></td>
</tr>
</tbody>
</table>
Expand Down
63 changes: 55 additions & 8 deletions docs/contributing/guides/state-management.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
# State Management

Dashboard 2.0 conducts state management through the use of a shared server-side data store. It provides four core functions for interaction with the store.
Dashboard 2.0 conducts state management through the use of a shared server-side data store.

The store can be injected into a widget using:
Stores are imported into a node's `.js` file with:

```js
const datastore = require('<path>/<to>/store/index.js')
const store = require('<path>/<to>/store.js')
```

In our architecture, we use two standalone stores:

## `datastore.save`
- `datastore`: A store for the latest `msg` received by a widget in the Editor.
- `statestore`: A store for all dynamic properties set against widgets in the Editor.

## Data Store

The server-side `datastore` is a centralised store for all messages received by widgets in the Editor. It is a simple key-value store, where the key is the widget's id, and the value is the message received by the widget. In some cases, e.g. `ui-chart` instead of recording _just_ the latest `msg` received, we actually store a history.

### `datastore.save`

When a widget receives a message, the default `node.on('input')` handler will store the received message, mapped to the widget's id into the datastore using:

Expand All @@ -19,7 +27,7 @@ datastore.save(node.id, msg)

This will store the latest message received by the widget, which can be retrieved by that same widget on load using:

## `datastore.get`
### `datastore.get`

When a widget is initialised, it will attempt to retrieve the latest message from the datastore using:

Expand All @@ -29,7 +37,7 @@ datastore.get(node.id)

This ensures, on refresh of the client, or when new clients connect after data has been geenrated, that the state is presented consistently.

## `datastore.append`
### `datastore.append`

With `.append`, we can store multiple messages against the same widget, representing a history of state, rather than a single point reference to the _last_ value only.

Expand All @@ -39,12 +47,51 @@ datastore.append(node.id, msg)

This is used in `ui-chart` to store the history of data points, where each data point could have been an individual message received by the widget.

## `datastore.clear`
### `datastore.clear`

When a widget is removed from the Editor, we can clear the datastore of any messages stored against that widget using:

```js
datastore.clear(node.id)
```

This ensures that we don't have any stale data in the datastore, and that we don't have any data stored against widgets that no longer exist in the Editor.
This ensures that we don't have any stale data in the datastore, and that we don't have any data stored against widgets that no longer exist in the Editor.

## State Store

The `statestore` is a centralised store for all dynamic properties set against widgets in the Editor. Dynamic Properties can be set through sending `msg.<proprrty>` payloads to a given node, e.g. for ` ui-dropdown`, we can send `msg.options` to override the "Options" property at runtime.

At the top-level it is key-mapped to the Widget ID's, then each widget has a map, where each key is the property name, mapping to the value.

### `statestore.getAll`

For a given widget ID, return all dynamic properties that have been set.

```js
statestore.getAll(node.id)
```

### `statestore.getProperty`

For a given widget ID, return the value of a particular property.

```js
statestore.getProperty(node.id, property)
```

### `statestore.set`

Given a widget ID and property, store the associated value in the appropriate mapping

```js
statestore.set(node.id, property, value)
```

### `statestore.reset`

Remove all dynamic properties for a given Widget/Node.

```js
statestore.reset(node.id)
```

30 changes: 30 additions & 0 deletions docs/contributing/widgets/debugging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Debugging Dashboard 2.0

Dashboard 2.0 comes with a built-in debugging tool to understand the data being configured for each dashboard, page, theme, group and widget.

To navigate to the tooling, head to `<your-host>:<your-port>/dashboard/_debug`.

![Debugging tool](/images/debug-example.png "Debugging tool"){data-zoomable}
_Screenshot of the Dashboard 2.0 Debugging Tool_

This tooling is particularly useful when you're building your own custom integrations, and developing on core Dashboard widgets too.

We're hoping to grow some of the scope of what this tooling provides, but for now, it will display the current `props` for a given widget, which is defined by Node-RED configurtion, but will also include the overriden values from the `msg` object (e.g. `msg.options` can override the `Options` property for a `ui-dropdown`).

## Message History

![Debugging tool](/images/debug-example-datastore.png "Debugging tool"){data-zoomable}
_Screenshot of the "Message History" tab for a widget_

This tab will show the latest `msg` values that the associated node has received in Node-RED's `datastore` for a given widget.

This is useful to understand what data will load when a new client connects to Node-RED. It will need refreshing to reflect the latest state if you're expecting new messages since the debug tool was last opened.

## Dynamic Properties

![Debugging tool](/images/debug-example-statestore.png "Debugging tool"){data-zoomable}
_Screenshot of the "Dynamic Properties" tab for a widget_

This tab shows any dynamic properties (properties set with an injection of a `msg.<property>` that have been set since the Node-RED server has been running. Within our server-side architecture, these are stored in our `statestore`.

These values are generally overriding the default properties set within the Node-RED Editor, and can be used to sanity check why a particular widget renders the way that it does.
12 changes: 11 additions & 1 deletion docs/nodes/widgets/ui-dropdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@ props:
Group: Defines which group of the UI Dashboard this widget will render in.
Size: Controls the width of the dropdown with respect to the parent group. Maximum value is the width of the group.
Label: The text shown to the left of the dropdown.
Options: A list of the options available in the dropdown. Each row defines a `label` (shown in the dropdown) and `value` (emitted on selection) property.
Options:
description: A list of the options available in the dropdown. Each row defines a `label` (shown in the dropdown) and `value` (emitted on selection) property.
dynamic: true
Allow Multiple: Whether or not a user can select multiple options, if so, checkboxes are shown, and value is emitted in an array.
dynamic:
Options:
payload: msg.options
structure: ["Array<String>", "Array<{value: String}>", "Array<{value: String, label: String}>"]
---

<script setup>
Expand Down Expand Up @@ -34,6 +40,10 @@ To make a single selection, pass in the `value` of the option as `msg.payload`,

<PropsTable/>

## Dynamic Properties

<DynamicPropsTable/>

## Example

![Example of a dropdown](/images/node-examples/ui-dropdown.png "Example of a dropdown"){data-zoomable}
Expand Down
Binary file added docs/public/images/debug-example-datastore.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/public/images/debug-example-statestore.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/public/images/debug-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions docs/user/migration/ui_dropdown.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"notes": "Allows for dynamic selection of an option (<a href='/nodes/widgets/ui-dropdown.html#programmatic-selections'>docs</a>)"
}, {
"property": "msg.options",
"changes": -1,
"notes": "<a target='_blank' href='https://github.com/FlowFuse/node-red-dashboard/issues/285'>issue</a>"
"changes": null,
"notes": "Allows for dynamic definition of an option (<a href='/nodes/widgets/ui-dropdown.html#dynamic-properties'>docs</a>)"
}]
}
4 changes: 2 additions & 2 deletions nodes/config/ui_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
}
.red-ui-editor .form-row-flex {
display: flex;
align-items: center;
align-items: baseline;
gap: 4px;
}
.red-ui-editor .form-row-flex input,
.red-ui-editor .form-row-flex label {
.red-ui-editor .form-row-flex label:not(:first-child) {
margin: 0;
width: auto;
}
Expand Down
25 changes: 24 additions & 1 deletion nodes/config/ui_base.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
const path = require('path')

const v = require('../../package.json').version
const datastore = require('../store/index.js')
const datastore = require('../store/data.js')
const statestore = require('../store/state.js')
const { appendTopic } = require('../utils/index.js')

// from: https://stackoverflow.com/a/28592528/3016654
Expand Down Expand Up @@ -61,6 +62,16 @@ module.exports = function (RED) {

uiShared.app.use(config.path, uiShared.httpMiddleware, express.static(path.join(__dirname, '../../dist')))

// debugging endpoints
uiShared.app.get(config.path + '/_debug/datastore/:widgetid', uiShared.httpMiddleware, (req, res) => {
return res.json(datastore.get(req.params.widgetid))
})

uiShared.app.get(config.path + '/_debug/statestore/:widgetid', uiShared.httpMiddleware, (req, res) => {
return res.json(statestore.getAll(req.params.widgetid))
})

// serve dashboard
uiShared.app.get(config.path, uiShared.httpMiddleware, (req, res) => {
res.sendFile(path.join(__dirname, '../../dist/index.html'))
})
Expand Down Expand Up @@ -212,6 +223,16 @@ module.exports = function (RED) {
* @param {Socket} socket - socket.io socket connecting to the server
*/
function emitConfig (socket) {
const widgets = node.ui.widgets
// loop over widgets - check statestore if we've had any dynamic properties set
for (const [id, widget] of widgets) {
const state = statestore.getAll(id)
if (state) {
// merge the statestore with our props to account for dynamically set properties:
widget.props = { ...widget.props, ...state }
}
}

// pass the connected UI the UI config
socket.emit('ui-config', node.id, {
dashboards: Object.fromEntries(node.ui.dashboards),
Expand Down Expand Up @@ -516,6 +537,8 @@ module.exports = function (RED) {
widget.props.height = null
}

// merge the statestore with our props toa ccount for dynamically set properties:

// loop over props and check if we have any function definitions (e.g. onMounted, onInput)
// and stringify them for transport over SocketIO
for (const [key, value] of Object.entries(widget.props)) {
Expand Down
File renamed without changes.
35 changes: 35 additions & 0 deletions nodes/store/state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Store to manage any dynamic properties set

const state = {}

const getters = {
// given a widget id, return all dynamically set properties
all (id) {
return state[id]
},
// given a widget id, return a specific dynamically set property
property (id, property) {
return state[id][property]
}
}

const setters = {
// given a widget id, property and value
set (id, prop, value) {
if (!state[id]) {
state[id] = {}
}
state[id][prop] = value
},
// remove data associated to a given widget
reset (id) {
delete state[id]
}
}

module.exports = {
getAll: getters.all,
getProperty: getters.property,
set: setters.set,
reset: setters.reset
}
13 changes: 13 additions & 0 deletions nodes/widgets/locales/en-US/ui_dropdown.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,17 @@ <h3>Selecting Options via <code>msg.</code></h3>
<p>
To clear any selection for a dropdown, pass an empty array <code>[]</code> as <code>msg.payload</code>.
</p>
<h3>Dynamically Setting Options</h3>
<p>
You can change the options available in the dropdown at runtime by sending <code>msg.options</code>.
</p>
<p>
Accepted formats of <code>msg.options</code> are:
<ul>
<li><code>Array&lt;string&gt;</code></li>
<li><code>Array&lt;{value: String}&gt;</code></li>
<li><code>Array&lt;{value: String, label: String}&gt;</code></li>

</ul>
</p>
</script>
2 changes: 1 addition & 1 deletion nodes/widgets/ui_chart.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const datastore = require('../store/index.js')
const datastore = require('../store/data.js')

module.exports = function (RED) {
function ChartNode (config) {
Expand Down
Loading