Skip to content

Commit

Permalink
Merge pull request #438 from FlowFuse/plugins-api
Browse files Browse the repository at this point in the history
(Draft) Plugins API
  • Loading branch information
joepavitt authored Dec 22, 2023
2 parents ad6115c + 4ce3b33 commit a6ea66f
Show file tree
Hide file tree
Showing 24 changed files with 797 additions and 265 deletions.
9 changes: 8 additions & 1 deletion docs/.vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export default ({ mode }) => {
]
},
{
text: 'Guides',
text: 'Useful Guides',
collapsed: false,
items: [
{ text: 'Repo Structure', link: '/contributing/guides/repo' },
Expand All @@ -132,6 +132,13 @@ export default ({ mode }) => {
{ text: 'Layout Managers', link: '/contributing/guides/layouts' },
{ text: 'Registering Widgets', link: '/contributing/guides/registration' }
]
},
{
text: 'Plugins',
collapsed: false,
items: [
{ text: 'Adding Plugins', link: '/contributing/plugins/' }
]
}
]
}
Expand Down
22 changes: 19 additions & 3 deletions docs/contributing/guides/state-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ In our architecture, we use two standalone stores:
- `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.

Each store will run checks to ensure that any provided messages are permitted to be stored. An example of where this would get blocked is if `msg._client.socketid` is specified and the relevant node type is setup to listen to socket constraints (by default, this is `ui-control` and `ui-notification`).

## 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.
Expand All @@ -22,9 +24,13 @@ The server-side `datastore` is a centralised store for all messages received by
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:

```js
datastore.save(node.id, msg)
datastore.save(base, node, msg)
```

- `base`: The `ui_base` node that the store is attached to
- `node`: The Node-RED node object we're storing state for
- `msg`: The message that was received by the node

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

### `datastore.get`
Expand All @@ -42,9 +48,13 @@ This ensures, on refresh of the client, or when new clients connect after data h
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.

```js
datastore.append(node.id, msg)
datastore.append(base, node, msg)
```

- `base`: The `ui_base` node that the store is attached to
- `node`: The Node-RED node object we're storing state for
- `msg`: The message that was received by the node

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`
Expand Down Expand Up @@ -84,9 +94,15 @@ statestore.getProperty(node.id, property)
Given a widget ID and property, store the associated value in the appropriate mapping

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

- `base`: The `ui_base` node that the store is attached to
- `node`: The Node-RED node object we're storing state for
- `msg`: The message that was received by the node
- `property`: The property name to store
- `value`: The value to store against the property

### `statestore.reset`

Remove all dynamic properties for a given Widget/Node.
Expand Down
189 changes: 189 additions & 0 deletions docs/contributing/plugins/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<script setup>
import AddedIn from '../../components/AddedIn.vue';
</script>

# Building Dashboard Plugins <AddedIn version="0.11.0"/>

Node-RED supports the development of custom plugins that add behaviour and functionality to the Node-RED runtime. A really common use case of plugins is [custom Node-RED Themes](https://nodered.org/docs/api/ui/themes/), which modify the overall CSS/appearance of the underlying Node-RED Editor.

Node-RED Dashboard 2.0 also supports plugins. This allows you to define custom behaviour for the Dashboard runtime. For now, we provide a collection of API hooks that allow the injection of code at various points in the Dashboard instantiation and runtime.

To integrate, make sure your plugins are registered with `"type": "node-red-dashboard-2"` in the `package.json` file. This will tell Node-RED that this is a Dashboard 2.0 plugin.

## Plugin Structure

Let's take a quick example to give an overview of the structure of a Dashboard plugin:

### package.json

```json
{
"name": "node-red-dashboard-2-<plugin-name>",
"version": "<x.y.z>",
"description": "<describe your plugin>",
"main": "index.js",
"scripts": {
"test": "<run your tests here>"
},
"author": {
"name": "<your name>",
"url": "<your website/gh profile>"
},
"node-red": {
"plugins": {
"node-red-dashboard-2-<plugin-name>": "index.js"
}
},
"license": "Apache-2.0"
}
```


### index.js

A plugin's `js` file will define runtime behaviours for the Dashboard 2.0. This is where you will define your hooks, and any other code that you want to run when the Dashboard 2.0 is instantiated, or messages are sent back and forth between the Dashboard and Node-RED.

```js
module.exports = function(RED) {
RED.plugins.registerPlugin("node-red-dashboard-2-<plugin-name>", {

// Tells Node-RED this is a Node-RED Dashboard 2.0 plugin
type: "node-red-dashboard-2",

// hooks - a collection of functions that will inject into Dashboard 2.0
hooks: {
/**
* onSetup - called when the Dashboard 2.0 is instantiated
* @param {object} RED - Node-RED runtime
* @param {object} config - UI Base Node Configuration
* @param {object} req - ExpressJS request object
* @param {object} res - ExpressJS response object
* @returns {object} - Setup object passed to the Client
*/
onSetup: (RED, config, req, res) => {
return {
// must ALWAYS return socketio.path if using this hook
socketio: {
path: `${config.path}/socketio`,
}
}
},
/**
* onInput - called when a node receives a message
* @param {object} msg - Node-RED msg object
* @returns {object} - Returns Node-RED msg object
*/
onInput: (msg) => {
// modify msg in anyway you like
return msg
},
/**
* onAction - called when a D2.0 widget emits the `widget-action` event via SocketIO
* @param {object} conn - SocketIO connection object
* @param {object} id - Unique Node/Widget ID
* @param {object} msg - Node-RED msg object
* @returns {object} - Returns Node-RED msg object
*/
onAction: (conn, id, msg) => {
// modify msg in anyway you like
msg.myField = "Hello World"
return msg
},
/**
* onChange - called when a D2.0 widget emits the `widget-change` event via SocketIO
* @param {object} conn - SocketIO connection object
* @param {object} id - Unique Node/Widget ID
* @param {object} msg - Node-RED msg object
* @returns {object} - Returns Node-RED msg object
*/
onChange: (conn, id, msg) => {
// modify msg in anyway you like
msg.myField = "Hello World"
return msg
},
/**
* onLoad - called when a D2.0 widget emits the `widget-load` event via SocketIO
* @param {object} conn - SocketIO connection object
* @param {object} id - Unique Node/Widget ID
* @param {object} msg - Node-RED msg object
* @returns {object} - Returns Node-RED msg object
*/
onLoad: (conn, id, msg) => {
// modify msg in anyway you like
msg.myField = "Hello World"
return msg
},
/**
* onAddConnectionCredentials - called when a D2.0 is about to send a message in Node-RED
* @param {object} conn - SocketIO connection object
* @param {object} msg - Node-RED msg object
* @returns {object} - Returns Node-RED msg object
*/
onAddConnectionCredentials: (conn, msg) => {
// modify msg in anyway you like
msg._client.socketIp = conn.request.socket.remoteAddress
return msg
},
/**
* onIsValidConnection - Checks whether, given a msg structure and Socket connection,
* any _client data specified allows for this message to be sent, e.g.
* if the msg._client.socketid is the same as the connection's ID
* @param {object} conn - SocketIO connection object
* @param {object} msg - Node-RED msg object
* @returns {boolean} - Is a valid connection or not
*/
onIsValidConnection: (conn, msg) => {
if (msg._client?.socketId) {
// if socketId is specified, check that it matches the connection's ID
return msg._client.socketId === conn.id
}
// if no specifics provided, then allow the message to be sent
return true
},
/**
* onCanSaveInStore - Checks whether, given a msg structure, the msg can be saved in the store
* Saving into a store is generally a bad idea if we're dealing with messages only intended for
* particular clients (e.g. a msg._client.socketId is specified)
* @param {object} msg - Node-RED msg object
* @returns {boolean} - Is okay to store this, or not
*/
onCanSaveInStore: (msg) => {
if (msg._client?.socketId) {
// if socketId is specified, then don't save in store
return false
}
return true
},

}
})
}
```
If any of `onInput`, `onAction`, `onChange` or `onLoad` return `null`, then the `msg` will abruptly stop there, and not be sent on any further in the flow.
### index.html
This defines any client/editor plugins. This allows for definiton of Node-RED Editor features such as injecting content into the Dashboard 2.0 sidebar.
```html
<script type="text/javascript">
RED.plugins.registerPlugin('node-red-dashboard-2-<plugin-name>', {
type: 'node-red-dashboard-2',
tabs: [
{
id: 'my-tab-id',
label: 'My Tab',
/**
* Runs when tabs are first created
* @param {object} base - ui-base node for which this sidebar represents
* @param {object} parent - DOM element to append content to
*/
init (base, parent) {
// add some content to the tab
}
}
]
})
</script>
```
4 changes: 3 additions & 1 deletion nodes/config/locales/en-US/ui_base.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
"ui-base" : {
"auto": "auto",
"label": {
"category": "dashboard 2"
"category": "dashboard 2",
"layout": "Layout",
"settings": "Settings"
},
"layout": {
"pages": "Pages",
Expand Down
Loading

0 comments on commit a6ea66f

Please sign in to comment.