Skip to content

Commit

Permalink
Merge pull request #977 from FlowFuse/947-client-constraints
Browse files Browse the repository at this point in the history
Multi Tenancy: Ensure all conditions are met defined in _client constraints
  • Loading branch information
joepavitt authored Jun 14, 2024
2 parents 60c52ef + a9172fd commit 2cb8ca9
Show file tree
Hide file tree
Showing 4 changed files with 309 additions and 2 deletions.
1 change: 1 addition & 0 deletions docs/.vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export default ({ mode }) => {
{ text: 'Dynamic Properties', link: '/user/dynamic-properties' },
{ text: 'Migration Guide', link: '/user/migration' },
{ text: 'UI Template Examples', link: '/user/template-examples' },
{ text: 'Multi-Tenancy', link: '/user/multi-tenancy' },
{ text: 'Subflows', link: '/user/subflows' },
{ text: 'Installing on Mobile', link: '/user/pwa' }
]
Expand Down
229 changes: 229 additions & 0 deletions docs/examples/multi-tenancy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
[
{
"id": "160e82270b278a06",
"type": "ui-slider",
"z": "a725245cfaf96f83",
"group": "32fcb041e36de17e",
"name": "",
"label": "Single Client",
"tooltip": "",
"order": 4,
"width": 0,
"height": 0,
"passthru": false,
"outs": "all",
"topic": "topic",
"topicType": "msg",
"thumbLabel": true,
"min": 0,
"max": 10,
"step": 1,
"className": "",
"x": 370,
"y": 60,
"wires": [
[
"fa633410a8a67c23"
]
]
},
{
"id": "fa633410a8a67c23",
"type": "ui-chart",
"z": "a725245cfaf96f83",
"group": "32fcb041e36de17e",
"name": "",
"label": "chart",
"order": 6,
"chartType": "line",
"category": "topic",
"categoryType": "msg",
"xAxisProperty": "",
"xAxisPropertyType": "msg",
"xAxisType": "time",
"yAxisProperty": "",
"ymin": "",
"ymax": "",
"showLegend": true,
"removeOlder": 1,
"removeOlderUnit": "3600",
"removeOlderPoints": "",
"colors": [
"#1f77b4",
"#aec7e8",
"#ff7f0e",
"#2ca02c",
"#98df8a",
"#d62728",
"#ff9896",
"#9467bd",
"#c5b0d5"
],
"width": 6,
"height": 8,
"className": "",
"x": 550,
"y": 120,
"wires": [
[]
]
},
{
"id": "8a9f5a7b055210a9",
"type": "change",
"z": "a725245cfaf96f83",
"name": "Remove _client",
"rules": [
{
"t": "delete",
"p": "_client",
"pt": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 360,
"y": 120,
"wires": [
[
"fa633410a8a67c23"
]
]
},
{
"id": "8934d174f3845df7",
"type": "ui-slider",
"z": "a725245cfaf96f83",
"group": "32fcb041e36de17e",
"name": "",
"label": "Send to All Users",
"tooltip": "",
"order": 2,
"width": 0,
"height": 0,
"passthru": false,
"outs": "all",
"topic": "topic",
"topicType": "msg",
"thumbLabel": true,
"min": 0,
"max": 10,
"step": 1,
"className": "",
"x": 130,
"y": 120,
"wires": [
[
"8a9f5a7b055210a9"
]
]
},
{
"id": "31b0d54757860d1b",
"type": "ui-slider",
"z": "a725245cfaf96f83",
"group": "32fcb041e36de17e",
"name": "Send to Same User",
"label": "Send to Same User",
"tooltip": "",
"order": 3,
"width": 0,
"height": 0,
"passthru": false,
"outs": "all",
"topic": "topic",
"topicType": "msg",
"thumbLabel": true,
"min": 0,
"max": 10,
"step": 1,
"className": "",
"x": 130,
"y": 180,
"wires": [
[
"de0801fb73bdceb3"
]
]
},
{
"id": "de0801fb73bdceb3",
"type": "change",
"z": "a725245cfaf96f83",
"name": "Remove socketId",
"rules": [
{
"t": "delete",
"p": "_client.socketId",
"pt": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 350,
"y": 180,
"wires": [
[
"fa633410a8a67c23"
]
]
},
{
"id": "32fcb041e36de17e",
"type": "ui-group",
"name": "Group Name",
"page": "e857a4a5a6b5320f",
"width": "6",
"height": "1",
"order": 1,
"showTitle": true,
"className": "",
"visible": true,
"disabled": false
},
{
"id": "e857a4a5a6b5320f",
"type": "ui-page",
"name": "Page Name",
"ui": "c851adb9b2a29c9e",
"path": "/",
"icon": "",
"layout": "grid",
"theme": "35ee7753b5b3599b",
"order": 3,
"className": "",
"visible": false,
"disabled": false
},
{
"id": "c851adb9b2a29c9e",
"type": "ui-base",
"name": "Multi User Dashboard",
"path": "/dashboard",
"includeClientData": true,
"acceptsClientConfig": [
"ui-notification",
"ui-control",
"ui-template",
"ui-chart"
]
},
{
"id": "35ee7753b5b3599b",
"type": "ui-theme",
"name": "Theme Name",
"colors": {
"surface": "#16234b",
"primary": "#1d44b9",
"bgPage": "#ecf2f8",
"groupBg": "#ffffff",
"groupOutline": "#cccccc"
}
}
]
77 changes: 77 additions & 0 deletions docs/user/multi-tenancy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<script setup>
import { ref } from 'vue'
import FlowViewer from '../components/FlowViewer.vue'
import ExampleSlider from '../examples/multi-tenancy.json'

const examples = ref({
'multi-tenant-slider': ExampleSlider
})
</script>

# Building Multi-Tenant Dashboards

We have two fundamental design patterns when building a Dashboard:

- **Single Source of Truth:** All users of your Dashboard will see the same data. This is useful for industrial IoT or Home Automation applications.
- **Multi Tenancy:** Data shown in a particular widget is unique to a given client/session/user. This represents a more traditional web application, where each user has their own session and associated data.

By default, any data passed to a Dashboard node is broadcast to all connected clients, and therefore visible to all users ("Single Source of Truth"). In Industrial IoT type use-cases, this is very useful where you may have a chart showing data about a given piece of equipment.

In other cases though, you may wish to send data to just a single client/user, this is where multi-tenancy comes in, and allows you to define constraints on which clients receive particular data.

You can read more about the design patterns [here](../getting-started.md#design-patterns).

## Understanding Client Data

With "Include Client Data" enabled, every `msg` a node emits will have a `_client` object appended to it. This object will detail any available information about the client/user interacting with a given Dashboard.

### Core Client Data

Out of the box, Dashboard will append two piece of information to the `_client` object:

- `socketId`: The unique ID of the socket connection that the client is using to interact with the Dashboard.
- `socketIp`: The IP address of the client interacting with the Dashboard.

### Plugin Data Providers

Plugins, such as the [FlowFuse User Addon](https://flowfuse.com/blog/2024/04/displaying-logged-in-users-on-dashboard/), are available to append additional information to the `_client` object.

These Authentication plugins can be installed via the Node-RED Palette Manager, and often require Node-RED to be setup with a given Authentication provider, separately from Dashboard. For example, for the FlowFuse User Addon, it is a requirement for Node-RED to be running on FlowFuse with the ["FlowFuse User Authentication"](https://flowfuse.com/docs/user/instance-settings/#flowfuse-user-authentication) option enabled.

The plugins will append additional information to the `_client` object, such as the `user` object, which details the user's name, email address, and any other information that the Authentication provider has available.

## Configuring Client Data

In the [Dashboard sidebar](./sidebar.md#client-data) within the Node-RED Editor, you will find the "Client Data" tab:

<img data-zoomable style="max-width: 400px; margin: auto;" src="/images/dashboard-sidebar-clientdata.png" alt="Screenshot of an example 'Client Data' tab"/>
<em>Screenshot of an example "Client Data" tab</em>

Client data defines information on the user/client interacting with the Dashboard. This data can be appended to every `msg` a node emits, underneath teh `msg._client` object.

When "Include Client Data" is enabled, every `msg._client` will detail the `socketId` and `socketIp` of any connected users.

## Simple Example

Here we demonstrate three different clients opening the same Dashboard.

The Dashboard consists of three sliers and a chart, and is running on FlowFuse, with the "FlowFuse User Addon" plugin enabled.

Two of the clients are logged in as the same user, and the third, "Another User".

<video controls>
<source src="https://github.com/FlowFuse/node-red-dashboard/assets/99246719/76601b4c-8d25-451c-b04f-e5ee4cf7efb0" type="video/mp4">
Your browser does not support the video tag.
</video>

We can see how it's possible to control the interaction of a widget, and how the data emitted from that widget is shared to other components and clients.

The first, "Send to All Users" slider pass through a change node which removes the `_client` object from the message, meaning that the data is sent to all clients, as no constraints are defined.

The second, "Send to Same User" slider passes through a change node, and has the `socketId` field removed from `msg._client`, leaving just the `user` object. This means the data is sent to any connected clients where the same authenticated user is found.

Finally, the "Single Client" slider just passes it's default output to the "Chart" node, including the full `msg._client` object. This means that the data is only sent to the client (`socketId`) & user (`user`) that interacted with the slider.

Below you will find the flow that runs the above example:

<FlowViewer :flow="examples['multi-tenant-slider']" height="300px"/>
4 changes: 2 additions & 2 deletions nodes/config/ui_base.js
Original file line number Diff line number Diff line change
Expand Up @@ -349,8 +349,8 @@ module.exports = function (RED) {
// we only send comms on the connection that matches that id
checks.push(msg._client?.socketId === conn.id)
}
// if ANY check says this is valid - we send the msg
return !checks.length || checks.includes(true)
// ensure all checks validate sending this
return !checks.length || !checks.includes(false)
}

/**
Expand Down

0 comments on commit 2cb8ca9

Please sign in to comment.