Skip to content

Commit

Permalink
Merge pull request #647 from FlowFuse/646-events-diagrams
Browse files Browse the repository at this point in the history
Docs: Update Events & State Store Architecture Docs & Diagrams
  • Loading branch information
Steve-Mcl committed Mar 11, 2024
2 parents 53062e5 + 2f75656 commit 9bf7166
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 22 deletions.
Binary file added docs/assets/images/events-arch-client-events.jpg
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/assets/images/events-arch-load.jpg
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/assets/images/events-arch-msg.jpg
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/assets/images/stores-client-side.jpg
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/assets/images/stores-server-side.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 48 additions & 3 deletions docs/contributing/guides/events.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,54 @@
# Event Architecture
# Events Architecture

An important part of the Dashboard is how Node-RED and the Dashboard communicate. This is achieved using [socket.io](https://socket.io/).

![A flow diagram depicting how events flow between Node-RED and the Dashboard](../../assets/images/events-architecture.png){data-zoomable}
*A flow diagram depicting how events flow between Node-RED (red) and the Dashboard (blue)*
Here, you can find details on the primary communications that occur between Node-RED (blocks in red) and the Dashboard (blocks in blue). The blocks make reference to particular functions and files within the source code to help navigate and understand where to find the relevant code.

Each of the cylindrical blocks directly refer to one of our client- or server-side stores, which are detailed in the [State Management](./state-management.md) guide.

## Architecture

We have broken the events architecture/traffic into three key groups:

- **Loading**: The initial loading of the Dashboard, or when a new configuration is sent by Node-RED on a fresh "Deploy".
- **Input**: When a message (`msg`) is received by a Dashboard node within Node-RED.
- **Dashboard Actions**: When a user interacts with a widget, or a widget sends a message back to Node-RED.

### "Loading" Event Flow

![A flow diagram depicting how events traverse between Node-RED (red) and the Dashboard (blue) at deploy and first-load](../../assets/images/events-arch-load.jpg){data-zoomable}
*A flow diagram depicting how events traverse between Node-RED (red) and the Dashboard (blue) at deploy and first-load*

Here we detail the initial "Setup" HTTP request, consequent SocketIO traffic and appropriate handlers that are run when a Dashboard is deployed (via the Node-RED "Deploy" option), as well as when a Dashboard-client is first loaded.

Note the differentiation between a "Dashboard" loading, i.e. the full app and browser connection, and an individual "Widget" loading. The latter is fired for _each_ widget when it is mounted/rendered into the DOM.

### "Input" Event Flow

![A flow diagram depicting how events traverse between Node-RED (red) and the Dashboard (blue) when messages are received by a Dashboard node](../../assets/images/events-arch-msg.jpg){data-zoomable}
*A flow diagram depicting how events traverse between Node-RED (red) and the Dashboard (blue) when messages are received by a Dashboard node*

This flow details the functions, and SocketIO traffic that occurs when a message is received by a Dashboard node within Node-RED. Note that most core Dashboard 2.0 widgets use the default `onInput` handler, but in some cases, a custom `onInput` handler is used where we want different behaviour.

Our default server-side `onInput` handler handles the common use cases of:

- Updating the widget's value into our server-side data store
- Checking if the widget is configured to define a `msg.topic` and if so, updating the widget's `msg.topic` property
- Check if the widget is configured with a `passthrough` option, and if so, check it's value before emitting the `msg` object to any connected nodes.
- Emit the `msg` object to any connected nodes, if appropriate.

### "Dashboard Actions" Event Flow

Different widgets trigger different events depending on the specific use-cases. The following diagram shows the three types of events that the client can emit to the server, and how these are handled separately.

![A flow diagram depicting how events traverse from Dashboard (blue) to Node-RED (red) when a user interacts with Dashboard](../../assets/images/events-arch-client-events.jpg){data-zoomable}
*A flow diagram depicting how events traverse from Dashboard (blue) to Node-RED (red) when a user interacts with Dashboard*

Some examples of events that are emitted from the Dashboard to Node-RED include:

- `widget-change` - When a user changes the value of a widget, e.g. a slider or text input
- `widget-action` - When a user interacts with a widget, and state of the widget is not important, e.g. a button click
- `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.

## Events List

Expand Down
97 changes: 79 additions & 18 deletions docs/contributing/guides/state-management.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,86 @@
# State Management

Dashboard 2.0 conducts state management through the use of a shared server-side data store.
Dashboard 2.0 provides a data store within Node-RED such that it's possible to refresh your Dashboard clients and data is retained. This is particularly useful for widgets like `ui-chart` where you may want to retain a history of data points, or for widgets like `ui-text` where you want to retain the last value displayed.

Stores are imported into a node's `.js` file with:
This page details the different "stores" we have in place and what they're used for.

```js
const store = require('<path>/<to>/store.js')
You can also check out the [Events Architecture](./events.md) for a more detailed look at when these stores are used and how they interact with the rest of the Dashboard.

## Client-Side (Dashboard)

![An image depicting the three client-side vuex stores we have in Dashboard 2.0](../../assets/images/stores-client-side.jpg){data-zoomable}
*An image depicting the three client-side vuex stores we have in Dashboard 2.0*

Our client-side stores are built using [VueX](https://vuex.vuejs.org/). These stores lose their data on a client refresh (but are re-populated by the server-side stores), and are just used to maintain a centralised, consistent view of the data across the entire Vue application as the user navigates around the Dashboard.

### `setup` store

This just stores the response from our initial `/_setup` request. This object, in core, contains the SocketIO configuration to help the client connect to the server.

It is also possible for plugins to append to this object (see [Adding Plugins](../plugins/#index-js)) additional data that can be useful across the application.

### `ui` store

This store is where we store the full [ui-config](./events#ui-config) that details all of the pages, themes, groups and widgets to render on a Dashboard.

### `data` store

The client-side datastore is a map of widget id's to either:

- The last `msg` received by the widget
- An array of `msg` objects, representing all known `msg` objects received by the widget

In most cases, a widget only needs reference to the _last_ message. In some cases, e.g. `ui-chart`, the full history is required in order to render a history of data.

When a widget is first loaded, we emit a `widget-load` event, which in the default `onLoad` handler, will attempt to retrieve the last message received by the widget from the server-side datastore, and store it in the client-side `data` store. You can read more about this in [Events Architecture](./events.md).

It is possible for a widget to access the mapped `msg` object using:

```vue
<template>
<pre>this.messages[this.id]</pre>
</template>
<script>
export default {
computed: {
...mapState('data', ['messages'])
}
}
</script>
```
_An example Widget.vue file that uses the `data` store to access the last message received by the widget_

This value is also updated automatically when a new message is received, as long as that widget is using the default handlers, again detailed in [Events Architecture](./events.md).


## Server-Side (Node-RED)

![An image depicting the two server-side vuex stores we have in Dashboard 2.0](../../assets/images/stores-server-side.jpg){data-zoomable}
*An image depicting the two server-side stores we have in Dashboard 2.0*

Our server-side stores maintain the "single source of truth". When any Dashboard client connects, the centralised data is sent to each client, and the client-side stores are populated with the relevant parts of this centralised store.

In our server-side architecture, we use two standalone stores:

In our architecture, we use two standalone stores:
- `datastore`: A map of each widget to the latest `msg` received by a respective node in the Editor.
- `statestore`: A store for all dynamic properties set on widgets (e.g. visibility or setting a property at runtime). Often, these values are overrides of the base configuration found in the `datastore`.

- `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 time a function server-side wants to write into these stores, a check is done 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`). In this case, we do not want to store that data in our centralised store since it's not relevant to _all_ users of the Dashboard.

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
### Importing Stores

Stores are imported into a node's `.js` file with:

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

### 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`
#### `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 @@ -33,7 +94,7 @@ datastore.save(base, node, 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 @@ -43,7 +104,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 @@ -57,7 +118,7 @@ datastore.append(base, node, 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:

Expand All @@ -67,29 +128,29 @@ 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.

## State Store
### 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`
#### `statestore.getAll`

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

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

### `statestore.getProperty`
#### `statestore.getProperty`

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

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

### `statestore.set`
#### `statestore.set`

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

Expand All @@ -103,7 +164,7 @@ statestore.set(base, node, msg, property, value)
- `property`: The property name to store
- `value`: The value to store against the property

### `statestore.reset`
#### `statestore.reset`

Remove all dynamic properties for a given Widget/Node.

Expand Down
2 changes: 1 addition & 1 deletion docs/contributing/widgets/third-party.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ const base = group.getBase()
Then, whenever you want to store data in the datastore, you can do so with:

```js
base.stores.data.save(node.id, msg)
base.stores.data.save(base, node.id, msg)
```

#### Node-RED State Store
Expand Down

0 comments on commit 9bf7166

Please sign in to comment.