Orchestrating Dashboard page flow via 'ui_control'

There have been several questions lately about how to switch dashboard pages, or trigger actions when a certain page becomes active. Although the Dashboard has its own built-in Models, Views, and Controllers (MVC), it's nice to be able to have more "control" over the UI page interactions. What follows here is a pattern that has worked well for me in the past...

In the first Editor tab (flow), I like to put a single ui_control node with input and output links connected to all my other tabs (the application pages):

The input links allow events on one page to switch to another dashboard page -- for instance, clicking on a row in the "Supplier Table" page switches to the "Purchase Order" list for the selected supplier. This way, the user does not have to lookup a Supplier name, open the Nav Menu, select the Purchase Orders page, and then execute a search based on that name.

The output links go to a switch node that directs the control msg based on the targeted tab's name. Each output port corresponds to one application page, using a link out/in pair to carry the msg object. The goal is to detect when a new page is activated, and to kick off a flow that queries a database for the results to be shown on that page (e.g. a list of Materials, or a graph of Sensor readings). The switch node is configured to lookup the activated tab by name (using the msg.name property), since the order of the tabs can change over time:

Of course, this means that you will have to give each tab a unique name -- and if you want to rename the tab, you'll need to keep the switch rules in sync. Note also that you should only have 1 ui_control node across all of your flows. "Bad Stuff" can happen if you cross the streams, Ray... bad stuff. (well, at least odd stuff)

So now we have pages that switch to each other, passing along information as the user selects it, and only querying for data as its needed to render the target page. Nice -- and also useful for some "simple" user/role based access.

Since each page gets notified when it becomes active, your flow can check for things like global context variables, or the outputs of other flows, and when not found it routes the user back to the expected input page. Although the Dashboard itself is designed for a single-user, you can redirect all pages back to a "login" or "access" page before allowing the user to see certain data -- certainly not high-security, but still a valid technique when you want to present different slices of data to some users, or require a valid Pin Pad entry before changing the thermostat settings, for instance.

8 Likes

Steve, do yo have a small demo flow so people (me) could take a look at and see this in action?

1 Like

I was going to include one, but it was too intrinsically tied into my application – I’ll have to work on a stripped down version that I can post later. But the heart of the idea is represented above…

Hi Steve, thank you very much for the awesome explanation !! There was very little info about this node (even in the old forum). My only reference was the flow posted by Dave (link below), which clearly explains how to use the node but you expanded a lot the available knowledge.

Simple example of hiding groups

Hey Steve,
i just stumbled upon your post. I am still very interested to see an example in action. It's not fully clear to me what happens after the switch node is passed...

Thanks!

second this...

I'm looking for a way to protect some of the tabs (settings pages) with a simple PIN input.
I have found a PIN input panel, it's not clear to me how to create a button on a page which requires a PIN input before switching to the page. Ideally the PIN can be defined with the page to be accessed...

Here's the PIN Input:

[{"id":"4ec43274.4d990c","type":"ui_template","z":"6880e39e.295cac","group":"e50f9fb2.c6557","name":"Key 1","order":1,"width":"2","height":"2","format":"<md-button class=\"vibrate filled touched bigfont rounded\" style=\"background-color:#333333\" ng-click=\"send({payload: '1' })\"> \n<svg width=\"105px\" height=\"105px\" version=\"1.1\" viewBox=\"0 0 200 200\">\n <g id=\"Key-1\">\n  <rect fill=\"#4D4D4D\" width=\"200\" height=\"200\" rx=\"12\" ry=\"12\"/>\n  <path fill=\"none\" stroke=\"#B3B3B3\" stroke-width=\"7.99957\" d=\"M6 194c-1,-1 -2,-3 -2,-6l0 -176c0,-4 4,-8 8,-8l176 0c2,0 4,1 6,2\"/>\n  <path fill=\"none\" stroke=\"#1A1A1A\" stroke-width=\"7.99957\" d=\"M194 6c1,1 2,3 2,6l0 176c0,4 -4,8 -8,8l-176 0c-2,0 -4,-1 -6,-2\"/>\n  <text x=\"59\" y=\"153\" style=\"fill: #E6E6E6; font-weight: normal; font-size: 150px; font-family: Arial;\">1</text>\n </g>\n</svg>\n</md-button>\n","storeOutMessages":false,"fwdInMessages":false,"templateScope":"local","x":710,"y":420,"wires":[["12c6ceca.ad5f99"]]},{"id":"622a1965.9c012","type":"ui_template","z":"6880e39e.295cac","group":"e50f9fb2.c6557","name":"Key 6","order":6,"width":"2","height":"2","format":"<md-button class=\"vibrate filled touched bigfont rounded\" style=\"background-color:#333333\" ng-click=\"send({payload: '6'})\"> \n<svg width=\"105px\" height=\"105px\" version=\"1.1\" viewBox=\"0 0 200 200\">\n <g id=\"Key-6\">\n  <rect fill=\"#4D4D4D\" width=\"200\" height=\"200\" rx=\"12\" ry=\"12\"/>\n  <path fill=\"none\" stroke=\"#B3B3B3\" stroke-width=\"7.99957\" d=\"M6 194c-1,-1 -2,-3 -2,-6l0 -176c0,-4 4,-8 8,-8l176 0c2,0 4,1 6,2\"/>\n  <path fill=\"none\" stroke=\"#1A1A1A\" stroke-width=\"7.99957\" d=\"M194 6c1,1 2,3 2,6l0 176c0,4 -4,8 -8,8l-176 0c-2,0 -4,-1 -6,-2\"/>\n  <text x=\"59\" y=\"153\" style=\"fill: #E6E6E6; font-weight: normal; font-size: 150px; font-family: Arial;\">6</text>\n </g>\n</svg>\n</md-button>","storeOutMessages":false,"fwdInMessages":false,"templateScope":"local","x":990,"y":460,"wires":[["12c6ceca.ad5f99"]]},{"id":"c877b09a.f22a28","type":"ui_template","z":"6880e39e.295cac","group":"e50f9fb2.c6557","name":"Key 3","order":3,"width":"2","height":"2","format":"<md-button class=\"vibrate filled touched bigfont rounded\" style=\"background-color:#333333\" ng-click=\"send({payload: '3' })\"> \n<svg width=\"105px\" height=\"105px\" version=\"1.1\" viewBox=\"0 0 200 200\">\n <g id=\"Key-3\">\n  <rect fill=\"#4D4D4D\" width=\"200\" height=\"200\" rx=\"12\" ry=\"12\"/>\n  <path fill=\"none\" stroke=\"#B3B3B3\" stroke-width=\"7.99957\" d=\"M6 194c-1,-1 -2,-3 -2,-6l0 -176c0,-4 4,-8 8,-8l176 0c2,0 4,1 6,2\"/>\n  <path fill=\"none\" stroke=\"#1A1A1A\" stroke-width=\"7.99957\" d=\"M194 6c1,1 2,3 2,6l0 176c0,4 -4,8 -8,8l-176 0c-2,0 -4,-1 -6,-2\"/>\n  <text x=\"59\" y=\"153\" style=\"fill: #E6E6E6; font-weight: normal; font-size: 150px; font-family: Arial;\">3</text>\n </g>\n</svg>\n</md-button>","storeOutMessages":false,"fwdInMessages":false,"templateScope":"local","x":990,"y":420,"wires":[["12c6ceca.ad5f99"]]},{"id":"7979b9.87747e48","type":"ui_template","z":"6880e39e.295cac","group":"e50f9fb2.c6557","name":"Key 4","order":4,"width":"2","height":"2","format":"<md-button class=\"vibrate filled touched bigfont rounded\" style=\"background-color:#333333\" ng-click=\"send({payload: '4'})\"> \n<svg width=\"105px\" height=\"105px\" version=\"1.1\" viewBox=\"0 0 200 200\">\n <g id=\"Key-4\">\n  <rect fill=\"#4D4D4D\" width=\"200\" height=\"200\" rx=\"12\" ry=\"12\"/>\n  <path fill=\"none\" stroke=\"#B3B3B3\" stroke-width=\"7.99957\" d=\"M6 194c-1,-1 -2,-3 -2,-6l0 -176c0,-4 4,-8 8,-8l176 0c2,0 4,1 6,2\"/>\n  <path fill=\"none\" stroke=\"#1A1A1A\" stroke-width=\"7.99957\" d=\"M194 6c1,1 2,3 2,6l0 176c0,4 -4,8 -8,8l-176 0c-2,0 -4,-1 -6,-2\"/>\n  <text x=\"59\" y=\"153\" style=\"fill: #E6E6E6; font-weight: normal; font-size: 150px; font-family: Arial;\">4</text>\n </g>\n</svg>\n</md-button>","storeOutMessages":false,"fwdInMessages":false,"templateScope":"local","x":710,"y":460,"wires":[["12c6ceca.ad5f99"]]},{"id":"ea362834.bc9cf8","type":"ui_template","z":"6880e39e.295cac","group":"e50f9fb2.c6557","name":"Key 5","order":5,"width":"2","height":"2","format":"<md-button class=\"vibrate filled touched bigfont rounded\" style=\"background-color:#333333\" ng-click=\"send({payload: '5'})\"> \n<svg width=\"105px\" height=\"105px\" version=\"1.1\" viewBox=\"0 0 200 200\">\n <g id=\"Key-5\">\n  <rect fill=\"#4D4D4D\" width=\"200\" height=\"200\" rx=\"12\" ry=\"12\"/>\n  <path fill=\"none\" stroke=\"#B3B3B3\" stroke-width=\"7.99957\" d=\"M6 194c-1,-1 -2,-3 -2,-6l0 -176c0,-4 4,-8 8,-8l176 0c2,0 4,1 6,2\"/>\n  <path fill=\"none\" stroke=\"#1A1A1A\" stroke-width=\"7.99957\" d=\"M194 6c1,1 2,3 2,6l0 176c0,4 -4,8 -8,8l-176 0c-2,0 -4,-1 -6,-2\"/>\n  <text x=\"59\" y=\"153\" style=\"fill: #E6E6E6; font-weight: normal; font-size: 150px; font-family: Arial;\">5</text>\n </g>\n</svg>\n</md-button>","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":850,"y":460,"wires":[["12c6ceca.ad5f99"]]},{"id":"ccfa88ba.1d96d","type":"ui_template","z":"6880e39e.295cac","group":"e50f9fb2.c6557","name":"Key 7","order":7,"width":"2","height":"2","format":"<md-button class=\"vibrate filled touched bigfont rounded\" style=\"background-color:#333333\" ng-click=\"send({payload: '7'})\"> \n<svg width=\"105px\" height=\"105px\" version=\"1.1\" viewBox=\"0 0 200 200\">\n <g id=\"Key-7\">\n  <rect fill=\"#4D4D4D\" width=\"200\" height=\"200\" rx=\"12\" ry=\"12\"/>\n  <path fill=\"none\" stroke=\"#B3B3B3\" stroke-width=\"7.99957\" d=\"M6 194c-1,-1 -2,-3 -2,-6l0 -176c0,-4 4,-8 8,-8l176 0c2,0 4,1 6,2\"/>\n  <path fill=\"none\" stroke=\"#1A1A1A\" stroke-width=\"7.99957\" d=\"M194 6c1,1 2,3 2,6l0 176c0,4 -4,8 -8,8l-176 0c-2,0 -4,-1 -6,-2\"/>\n  <text x=\"59\" y=\"153\" style=\"fill: #E6E6E6; font-weight: normal; font-size: 150px; font-family: Arial;\">7</text>\n </g>\n</svg>\n</md-button>","storeOutMessages":false,"fwdInMessages":false,"templateScope":"local","x":710,"y":500,"wires":[["12c6ceca.ad5f99"]]},{"id":"3d0356e8.3564f2","type":"ui_template","z":"6880e39e.295cac","group":"e50f9fb2.c6557","name":"Key 2","order":2,"width":"2","height":"2","format":"<md-button class=\"vibrate filled touched bigfont rounded\" style=\"background-color:#333333\" ng-click=\"send({payload: '2' })\"> \n<svg width=\"105px\" height=\"105px\" version=\"1.1\" viewBox=\"0 0 200 200\">\n <g id=\"Key-2\">\n  <rect fill=\"#4D4D4D\" width=\"200\" height=\"200\" rx=\"12\" ry=\"12\"/>\n  <path fill=\"none\" stroke=\"#B3B3B3\" stroke-width=\"7.99957\" d=\"M6 194c-1,-1 -2,-3 -2,-6l0 -176c0,-4 4,-8 8,-8l176 0c2,0 4,1 6,2\"/>\n  <path fill=\"none\" stroke=\"#1A1A1A\" stroke-width=\"7.99957\" d=\"M194 6c1,1 2,3 2,6l0 176c0,4 -4,8 -8,8l-176 0c-2,0 -4,-1 -6,-2\"/>\n  <text x=\"59\" y=\"153\" style=\"fill: #E6E6E6; font-weight: normal; font-size: 150px; font-family: Arial;\">2</text>\n </g>\n</svg>\n</md-button>","storeOutMessages":false,"fwdInMessages":false,"templateScope":"local","x":850,"y":420,"wires":[["12c6ceca.ad5f99"]]},{"id":"c7330a2.ac1d8f8","type":"ui_template","z":"6880e39e.295cac","group":"e50f9fb2.c6557","name":"Key 8","order":8,"width":"2","height":"2","format":"<md-button class=\"vibrate filled touched bigfont rounded\" style=\"background-color:#333333\" ng-click=\"send({payload: '8'})\"> \n<svg width=\"105px\" height=\"105px\" version=\"1.1\" viewBox=\"0 0 200 200\">\n <g id=\"Key-8\">\n  <rect fill=\"#4D4D4D\" width=\"200\" height=\"200\" rx=\"12\" ry=\"12\"/>\n  <path fill=\"none\" stroke=\"#B3B3B3\" stroke-width=\"7.99957\" d=\"M6 194c-1,-1 -2,-3 -2,-6l0 -176c0,-4 4,-8 8,-8l176 0c2,0 4,1 6,2\"/>\n  <path fill=\"none\" stroke=\"#1A1A1A\" stroke-width=\"7.99957\" d=\"M194 6c1,1 2,3 2,6l0 176c0,4 -4,8 -8,8l-176 0c-2,0 -4,-1 -6,-2\"/>\n  <text x=\"59\" y=\"153\" style=\"fill: #E6E6E6; font-weight: normal; font-size: 150px; font-family: Arial;\">8</text>\n </g>\n</svg>\n</md-button>","storeOutMessages":false,"fwdInMessages":false,"templateScope":"local","x":850,"y":500,"wires":[["12c6ceca.ad5f99"]]},{"id":"8610d7d3.f19168","type":"ui_template","z":"6880e39e.295cac","group":"e50f9fb2.c6557","name":"Key 9","order":9,"width":"2","height":"2","format":"<md-button class=\"vibrate filled touched bigfont rounded\" style=\"background-color:#333333\" ng-click=\"send({payload: '9'})\"> \n<svg width=\"105px\" height=\"105px\" version=\"1.1\" viewBox=\"0 0 200 200\">\n <g id=\"Key-9\">\n  <rect fill=\"#4D4D4D\" width=\"200\" height=\"200\" rx=\"12\" ry=\"12\"/>\n  <path fill=\"none\" stroke=\"#B3B3B3\" stroke-width=\"7.99957\" d=\"M6 194c-1,-1 -2,-3 -2,-6l0 -176c0,-4 4,-8 8,-8l176 0c2,0 4,1 6,2\"/>\n  <path fill=\"none\" stroke=\"#1A1A1A\" stroke-width=\"7.99957\" d=\"M194 6c1,1 2,3 2,6l0 176c0,4 -4,8 -8,8l-176 0c-2,0 -4,-1 -6,-2\"/>\n  <text x=\"59\" y=\"153\" style=\"fill: #E6E6E6; font-weight: normal; font-size: 150px; font-family: Arial;\">9</text>\n </g>\n</svg>\n</md-button>","storeOutMessages":false,"fwdInMessages":false,"templateScope":"local","x":990,"y":500,"wires":[["12c6ceca.ad5f99"]]},{"id":"978a7747.08ba6","type":"ui_template","z":"6880e39e.295cac","group":"e50f9fb2.c6557","name":"Key *","order":10,"width":"2","height":"2","format":"<md-button class=\"vibrate filled touched bigfont rounded\" style=\"background-color:#333333\" ng-click=\"send({payload: '*'})\"> \n<svg width=\"105px\" height=\"105px\" version=\"1.1\" viewBox=\"0 0 200 200\">\n <g id=\"Key-Reset\">\n  <rect fill=\"#4D4D4D\" width=\"200\" height=\"200\" rx=\"12\" ry=\"12\">\n    <title>Reset</title>\n  </rect>\n  <path fill=\"none\" stroke=\"#B3B3B3\" stroke-width=\"7.99957\" d=\"M6 194c-1,-1 -2,-3 -2,-6l0 -176c0,-4 4,-8 8,-8l176 0c2,0 4,1 6,2\"/>\n  <path fill=\"none\" stroke=\"#1A1A1A\" stroke-width=\"7.99957\" d=\"M194 6c1,1 2,3 2,6l0 176c0,4 -4,8 -8,8l-176 0c-2,0 -4,-1 -6,-2\"/>\n  <text x=\"50\" y=\"250\" style=\"fill: #E6E6E6; font-weight: normal; font-size: 250px; font-family: Arial;\">*</text>\n </g>\n</svg>\n</md-button>","storeOutMessages":false,"fwdInMessages":false,"templateScope":"local","x":710,"y":540,"wires":[["12c6ceca.ad5f99"]]},{"id":"95d0781b.02f34","type":"ui_template","z":"6880e39e.295cac","group":"e50f9fb2.c6557","name":"Key 0","order":11,"width":"2","height":"2","format":"<md-button class=\"vibrate filled touched bigfont rounded\" style=\"background-color:#333333\" ng-click=\"send({payload: '0'})\"> \n<svg width=\"105px\" height=\"105px\" version=\"1.1\" viewBox=\"0 0 200 200\">\n <g id=\"Key-0\">\n  <rect fill=\"#4D4D4D\" width=\"200\" height=\"200\" rx=\"12\" ry=\"12\"/>\n  <path fill=\"none\" stroke=\"#B3B3B3\" stroke-width=\"7.99957\" d=\"M6 194c-1,-1 -2,-3 -2,-6l0 -176c0,-4 4,-8 8,-8l176 0c2,0 4,1 6,2\"/>\n  <path fill=\"none\" stroke=\"#1A1A1A\" stroke-width=\"7.99957\" d=\"M194 6c1,1 2,3 2,6l0 176c0,4 -4,8 -8,8l-176 0c-2,0 -4,-1 -6,-2\"/>\n  <text x=\"59\" y=\"153\" style=\"fill: #E6E6E6; font-weight: normal; font-size: 150px; font-family: Arial;\">0</text>\n </g>\n</svg>\n</md-button>","storeOutMessages":false,"fwdInMessages":false,"templateScope":"local","x":850,"y":540,"wires":[["12c6ceca.ad5f99"]]},{"id":"a452985a.1afa7","type":"ui_template","z":"6880e39e.295cac","group":"e50f9fb2.c6557","name":"Key #","order":12,"width":"2","height":"2","format":"<md-button class=\"vibrate filled touched bigfont rounded\" style=\"background-color:#333333\" ng-click=\"send({payload: '#'})\"> \n<svg width=\"105px\" height=\"105px\" version=\"1.1\" viewBox=\"0 0 200 200\">\n <g id=\"Key-Enter\">\n  <rect fill=\"#4D4D4D\" width=\"200\" height=\"200\" rx=\"12\" ry=\"12\">\n    <title>Enter</title>\n  </rect>\n  <path fill=\"none\" stroke=\"#B3B3B3\" stroke-width=\"7.99957\" d=\"M6 194c-1,-1 -2,-3 -2,-6l0 -176c0,-4 4,-8 8,-8l176 0c2,0 4,1 6,2\"/>\n  <path fill=\"none\" stroke=\"#1A1A1A\" stroke-width=\"7.99957\" d=\"M194 6c1,1 2,3 2,6l0 176c0,4 -4,8 -8,8l-176 0c-2,0 -4,-1 -6,-2\"/>\n  <text x=\"59\" y=\"153\" style=\"fill: #E6E6E6; font-weight: normal; font-size: 150px; font-family: Arial;\">#</text>\n </g>\n</svg>\n</md-button>","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":990,"y":540,"wires":[["12c6ceca.ad5f99"]]},{"id":"cadeafc6.552e4","type":"ui_text","z":"6880e39e.295cac","group":"e50f9fb2.c6557","order":13,"width":0,"height":0,"name":"keys pressed","label":"Number:","format":"{{msg.payload}}","layout":"row-spread","x":1210,"y":420,"wires":[]},{"id":"12c6ceca.ad5f99","type":"function","z":"6880e39e.295cac","name":"code entry","func":"var key = msg.payload;\nvar out = context.get(\"code\") || \"\";\n\nif (key === \"#\") {\n    key = \"\"; // send code\n}\nelse if (key === \"*\") {\n    key = \"\"; // reset code\n    out = \"\";\n}\nelse if (!isNaN(+key)) {\n    out += key; // append key\n}\ncontext.set(\"code\", key ? out : undefined);\n\n// output #1: keys entered\n// output #2: completed code\nmsg.payload = out;\nreturn [\n    key ? msg : {payload: \"\"},\n    out ? msg : null\n];","outputs":2,"noerr":0,"x":1170,"y":480,"wires":[["cadeafc6.552e4"],["6f371256.0e1004"]],"outputLabels":["last key number","completed code"]},{"id":"6f371256.0e1004","type":"debug","z":"6880e39e.295cac","name":"code entered","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload","x":1210,"y":540,"wires":[]},{"id":"e50f9fb2.c6557","type":"ui_group","z":"","name":"Keypad","tab":"84110940.4950e8","order":5,"disp":true,"width":"6"},{"id":"84110940.4950e8","type":"ui_tab","z":"","name":"Button Panel","icon":"dashboard"}]

I am also highly interested in martin's request. Has anyone been able to accomplish this?

I have been able to get a button placed on a page that redirects me to another page, but Martin's request about "it's not clear to me how to create a button on a page which requires a PIN input before switching to the page" is were I am struggling.

I did this well over a year ago, and I keep meaning to see if i can dig up that old project...
unfortunately, we didn't have discourse at the time, and I cannot find any documentation on the old Google groups.

In the meantime, there was this post, although it's not complete.

Hi @shrickus, I like the panel for authentication in your parallel post.

The panel works great. And it's easy enough to hide all tabs with ui_control before authentication. As well, it's easy to unhide tabs by correct PIN entered.

What's usable way to hide tabs? I mean, when user completed his session with UI, how the Node-Red might recognize this?

The easiest way is timeout: if no activity for "defined" time, then lock/hide tabs.
However, this might be annoying if timeout is short and very vulnerable if timeout is too long. (Not saying the spoken method of user authentication is very safe, I mean it's better than none.)

So maybe it's possible to detect the session initiator (user) closed the UI in his browser, or went out of the network, or something else?
I wonder how to make such authentication user friendly, still not getting rid of the authentication?

I'm not convinced this is secure at all.
Surely if someone else is on the UI at the same time and you put in your code you're just outright authenticating nodered and they can do whatever they please? (On that specifice page)

1 Like

True -- but then again, there is no concept of "someone else on the ui", since the dashboard only supports a single user...

That being said, there is a unique client id associated with each browser client's websocket -- so a clever storing of the "access flag" per client connection could be used to give the illusion of multiple logins... maybe.

2 Likes

I try it but i don't find how to create multiple pages.
does anyone try it si he can post a demo flow please !