diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index 4a576b3c2..801cf41c2 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -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' } ] diff --git a/docs/examples/multi-tenancy.json b/docs/examples/multi-tenancy.json new file mode 100644 index 000000000..64e384667 --- /dev/null +++ b/docs/examples/multi-tenancy.json @@ -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" + } + } +] \ No newline at end of file diff --git a/docs/user/multi-tenancy.md b/docs/user/multi-tenancy.md new file mode 100644 index 000000000..3d9a9f318 --- /dev/null +++ b/docs/user/multi-tenancy.md @@ -0,0 +1,77 @@ + + +# 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: + +Screenshot of an example 'Client Data' tab +Screenshot of an example "Client Data" tab + +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". + + + +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: + + diff --git a/nodes/config/ui_base.js b/nodes/config/ui_base.js index 82859f529..c97f6b38e 100644 --- a/nodes/config/ui_base.js +++ b/nodes/config/ui_base.js @@ -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) } /**