Alternative of current ui nodes in the FlexDash dashboard

Hi folks,

I would like to discuss here how we can assist @tve to build an alternative for the current AngularJs UI nodes in his FlexDash dashboard. For people that don't like the concept of UI nodes, please just ignore this discussion because you can already use FlexDash without UI nodes :wink:

So this discussion is NOT about why we would like to have UI nodes, but about how we can implement this...

The trigger for this discussion was the following quote from Thorsten in another discussion:

To avoid that Thorsten had to reverse engineer some of the current UI nodes, I have tried to help him by summarizing how I think the UI nodes in the current AngularJs nodes work. Of course I am not a dashboard specialist, so if anybody has extra input please share it here. We have NOT discussed yet what would be the best way to implement this into Flexdash. I will leave that discussion to Thorsten and others in the community that are familiar with modern web technologies...

Here is a summary of the stuff that I have shared yesterday with Thorsten in a PM.

Contrib UI nodes vs standard UI nodes

I have mainly explained the contrib ui nodes (instead of the standard ui nodes like e.g. a button) because most probably in Flexdash ALL ui nodes will be separate contrib ui nodes. Which means that all ui nodes will be developed in the same way, unlike the current standard ui nodes that are very much integrated into the dashboard code. Although that should not mean that a FlexDash cannot be shipped by default with some standard ui nodes included...

Internal flow a contrib ui node

This is how a custom ui node works internally:

Note that there is also a convert and convertBack function, but I have not used those yet so not sure where the are positioned in this diagram...

  1. At startup the Node-RED registry creates the server side node (just as it does for non-ui nodes). At this point a node developer can call some server side startup code. In case of a UI node we call ui.addWidget on the dashboard, and we pass a bunch of parameters to that function. One of those parameters is a callback function 'HTML' that returns the html string. So the html string is currenly created once dynamically as soon as the flow is started.

    But of course if FlexDash wants to create that html in another way (like the existing FlexDash widgets currently do), be my guest!! Although it might be useful to take this into account somehow: currently the dashboard ui-template node allows users to enter their html/javascript/css in the config screen, and then it is send to the client. So it would be useful if you would be able to find a FD alternative for that...

    Most other input parameters (of the ui.addWidget function) are related to message replaying and that kind of stuff (see further). Not sure if you need it...

  2. When an input message arrives on the ui node, the dashboard framework has implemented the on-'input' event handler (so that is not maintained by the ui-node developer like we use to do with non-ui nodes). That handler makes sure that e.g. the beforeEmit function is being called...

  3. In the beforeEmit function we can e.g. do server-side validation of the input message. Or we can adjust the message, before it is being send to the frontend.

    One big disadvantage currently is that we cannot stop an invalid message being send to the frontend.

  4. When a dashboard page is opened, the dashboard framework will call our initController function on the frontend size. In that function we can do all stuff of initialization, e.g. specify callback functions for $scope.init (called when the AngularJs scope is initialized) and $scope.watch (called when input msg object arrives).

  5. When a new input message arrives, it is being stored in the AngularJs scope.msg.

  6. In our AngularJs frontend code, we have to watch for changes in the $scope.msg object. So as soon as the input object changes, our watch function will be called. There we can do all kind of frontend stuff, e.g. update the DOM tree of our ui widget.

  7. At certain conditions (e.g. when an element in the DOM tree is being clicked), we can construct an output message object and send it to the server with $scope.send.

  8. When the output message arrives on the server, our beforeSend function is being called. There we can modify the output message, or stop the output message from being send.

  9. At the end our output message is send to the next nodes in the flow.

Message resending

In the current dashboard there is also a message resending mechanism:

  1. When we inject an input message, all open dashboards clients will be updated based on the information in that message. Because the watch function in all the clients will be called, which will update their DOM.

  2. When a new dashboard client is opened, the dashboard will resend the last message to that new client. That way the new client is synced with the existing clients. E.g. when you have a ui-led node and you inject a message to turn the LED to red, then - when a new dashboard is opened - it will also receive that last message, which means the LED will also be red on that new dashboard client.

  3. When the browser page is refreshed, the last message is being resend also (but now within the frontend). So the replayed message comes from somewhere in the frontend, not sure how it works.

Although this message resending is very useful, it has given me already lots of headaches:

  • Only the last message is being replayed. But the state of a ui node is most of the time constructed incrementally, by a sequence of multiple input messages. For example my ui-svg allows you to draw a floorplan. Suppose you inject a message to start flickering the icon of camera 1. Afterwards you inject a message to start flickering the icon of camera 2. When a new dashboard is opened, only the last message will be resend. Which means that on the new dashboard client, only icon of camera2 will be flickering (instead of both camera icons like in the other clients)...

  • Suppose I want to see my video recordings from yesterday till today. So I enter two dates in my dashboard (in two date fields) and I press the search button. Then the search results will be displayed on all my dashboards that are currently open. But my partner might be looking at the same moment for other recordings, which will be overwritten by mine on her screen...

  • This message replaying might also cause high load on the current charts, when too much data is being send...

So I am not sure what Flexdash should do with message replaying. It is very hard to get it right in all circumstances, and there are a series of parameters that you can adjust. It would be very nice if there would be a better mechanism possible, so that a ui node developer doesn't have to invent the same logic over and over again. But not sure how that could be done...

An example node

If you want to have some more technical information, you might have a look at the node-red-node-ui-mylittleuinode. When - at the time being - Hitachi had implemented a pull request to allow us to create custom ui nodes (using the above flow), we have implemented that mylittleuinode as an example for people that want to implement their own ui node. So it contains a lot of comments...

Files of a contrib ui node

  • The .html file contains the config screen code, i.e. everything that is needed to construct the config screen when you double click the node in the flow editor).
  • The .js file contains both the server side stuff of the node (i.e. beforeEmit, beforeSend...) but also the frontend stuff (i.e. initController, scope.watch...). This is VERY confusing, when you start developing ui nodes. But I assume the contrib ui nodes in FlexDash will separate server side and client side stuff in separate files.

Standard UI nodes

I don't know much about the standard UI nodes, but here are some pointers if you need to analyze e.g. the code of the current ui-control node:

  • The client side code of all those nodes is inside the main.js file.
  • The server side code is separated per node, e.g. in the ui_ui_control.js
  • The config screen code is also separated per node, e.g. in the ui_ui_control.html file.

How is the config passed to the dashboard

In the html file of a ui node, a developer can specify what Node-RED has to show on the config screen and in the node info panel. For example we want to allow the user to configure the button color.

This works the same way for non-ui and ui nodes:

  1. The user double clicks on the ui node in the flow, and Node-RED will display the node's config screen (based on the html file).
  2. The user can read the documentation in the info panel on the right, and change e.g. the color on the config screen.
  3. When the user hits the "Deploy" button, Node-RED will send the updated config of the node to the server.
  4. Node-RED will restart the flows (to use the updated config), and call the node's server-side code (in the js file). That is the part of the code - seen my diagram above - where a UI node needs to call the ui.addWidget(....) function offered by the AngularJs dashboard:
    // Node-RED calls this function at startup and after every deploy
    function FDButtonNode(nodeConfig) {
             var node = this;
    
             // Get a reference to the dashboard framework
             var ui = RED.require("node-red-dashboard")(RED);
    
             RED.nodes.createNode(this, config);
    
             // Generate the html string, by calling our HTML function (which simply returns a string)
             var html = HTML(config);
    
             var done = ui.addWidget({
                     node: node,
                     order: config.order,
                     group: config.group,
                     width: config.width,
                     height: config.height,
                     format: html,
                     ...
             });
      }
    
     // This is called when Node-RED starts.  We register our fd node in the Node-RED node registry.
     // That way Node-RED can call our FDButtonNode function at startup and at every redeploy
     RED.nodes.registerType("`fd-button", FDButtonNode);
    
    So Node-RED calls our function at startup and at every deploy, and then we pass our html string (and other parameters) to the dashboard...

Special UI nodes

If all ui nodes would be contrib nodes, then all ui nodes would be implemented using the same internal skeleton. However there are two ui nodes that are a bit different from the other ui nodes:

  • A ui_template node: on the config screen of this node, users can enter html/css/javascript. Which means instead of generating this via the HTML function, this would arrive in your server side code via config.html.
  • A ui_control node: this node has to cooperate with the dashboard more closely compared to the other nodes. It has a two-way communication with the dashboard frontend:
    • If something changes on the frontend (tab switch, resize, ...) an output message will be produced.
    • If an input message is injected (e.g. tab switch) then the frontend will be updated.

Like Thorsten already mentioned, it will be hard (or impossible?) to create a wrapper so the old AngularJs code from the old UI template nodes can be used within the new Vue.js ui template nodes...

The config screen

The config screen of a UI node is almost similar to that of a non-UI node. The main difference is that two widgets have been provided, which UI node developers can use on their config screen (i.e. in the .html file). Those widgets allow the user to adjust:

  • The size of the ui element.
  • The tab and group where the ui element should be visualized

image

Note: at deploy time, those parameters are passed to the server side widget via config.width, config.height, config.order and config.group. The node developer then needs to pass those parameters to the addWidget function, so the dashboard can use them to adjust the layout.

Not sure how an alternative would fit into the FlexDash concept...

Layout

I have not discussed this yet with Thorsten. But the current AngularJs dashboard has a tree view in the right sidebar, which also offers a layout editor.

Again not sure how this would fit into the FlexDash concept...

Perhaps a third-party layout manager can somehow be integrated into Node-RED. I am not following these kind of technologies, so cannot give any advise...

7 Likes

[copy-paste of my reply from private chat]

Thanks for the excellent summary! Thoughts:

  • All the stuff that happens on the front-end is completely different, I don't think it makes sense to port anything or try to be compatible. At least not at this stage.
  • Users clearly want to have nodes in the flow editor representing stuff showing up in the dashboard, so that's what I'm trying to implement.
  • I believe there are 3 parts to this:
    1. the flow of messages incl ability to validate/modify in beforeEmit/beforeSend
    2. the ability to define configuration settings for each node that can then be manipulated by the user in the flow editor
    3. the layout panel in the flow editor that allows widgets to be placed in tabs and groups.
  • I don't think the beforeEmit/beforeSend parts are difficult, the code that needs to be written will differ, but it seems straight-forward
  • How are the configuration settings implemented? E.g. if a user changes the color of a button, what ends up implementing that change? Is it the 'HTML' callback function that gets called? What I need for FlexDash is a callback function that translates the configuration settings to widget settings (which is simply a map {} ).
  • The layout may be more difficult, however, it basically has tabs, groups, and widgets. FlexDash has tabs, panels, and widgets. The layout is a linear ordering of groups in tabs and widgets in groups. FlexDash is the same, except that widgets can appear outside of panels. The layout has width x height for widgets and width for groups. FlexDash also needs a height for Panels, minor difference. Dunno whether it's useful to try and 'port' the layout editor...
  • A first step could ignore the layout and just have the send/receive messages and config settings. At the end of the day, is there anything different about the config of UI nodes vs. regular NR nodes?

This is how far I got...

2 Likes
  • Front end stuff - I Agree - internally at least - would be nice if visually similar (or better) widgets are available.

  • nodes in the flow editor representing stuff showing up in the dashboard, - yes 100% that is the USP of the current dashboard - one widget per... (does of course causes issues like "can I auto create x widgets the same (but different)" automatically etc)

  • validate/modify in beforeEmit/beforeSend - yes - eg for rounding/formatting numbers dates colours etc,

  • define configuration settings for each node - yes that users immediately want to override per message :slight_smile:

  • layout panel in the flow editor - Would be great to do this from the start rather than an afterthought.

  • How are the configuration settings implemented? things in the nodes config panel are defined in the .html file and saved to the backend as node properties by the editor. And reloaded when you open the edit panel.

  • Layout FlexDash is the same, except that widgets can appear outside of panels. Nothing about the layout has to be the same - CSS and so on have moved on a lot since then so there may be better ways to do this / allow more possibilities - Various requests have included more proportional spacing (ie fill screen), groups in groups (eg like sublows but for screens), multiple endpoints (rather than SPA) - to allow more separation for authentication etc...

A first step could ignore the layout and just have the send/receive messages and config settings. At the end of the day, is there anything different about the config of UI nodes vs. regular NR nodes? - Agree - would be a fantastic start. And config wise - for the existing dashboard - no - the config of each node is same as per any other node as far as creating them goes and edit panels etc... but they do all talk to a base node (ui_base) that sets up the side bar panel in the editor and that holds all the basic dashboard stuff...

Final thing you didn't mention yet is themes... There is always a fight between trying to theme things at an overall level and then users wanting to tweak every single part of a widget - so yes I'm sure that will remain - but thinking about it up front may or may not help :slight_smile:

5 Likes

Thanks for chiming in!

the way flexdash works, any attribute oif any node can be set statically or dynamically :slight_smile:

Perhaps, except that doing it live in the dashboard itself is much more intuitive imho... I don't have proper drag&drop implemented, but that's just so much better than doing it abstractly and you see the real results as opposed to what some UI pretends it gonna look like. (E.g., the current dashboard has massive failures when it comes to layout with widgets ending up on top obscuring others).

The layout implementation in FD is completely different and pluggable (although that's not polished 'cause I haven't found a point in doing a second layout yet). What I referred to is that the concepts in FD are essentially the same with a 3-level hierarchy and linear ordering within each.

I think the main question I have is whether I can tie into the stuff in the ui_base to leverage some of the configuration elements that I believe are only in there. But I have to look at the code at this point...

Yup. I've held off on doing too much in that direction 'cause it looks like there will be significant changes from Vuetify2 to Vuetify3 in terms of theming support. I've kept the CSS stuff to a minimum, it's quite the rabbit hole. It's easy to change the base grid dimensions, but that only results in crap layout. You need to have a ton of other stuff change "in proportion" and that means a lot of CSS pre-processing magic.

Anyway, I mostly wanted to thank you guys for all the input, at this point I need to roll up my sleeves and dive into some code...

1 Like

Hey Guys,

Thanks for getting this going.

From an end user point of view with NOT ONE IOTA of design ability most of this goes over my head ! But i would say that Flexdash does look nice - i think one of the key things is the ability to iframe the current UI so that we can port everything we have and continue using that and then slowly shrink it over time as Flexdash becomes more functional with more base widgets.

Just playing devils advocate though - is there much of a difference between FlexDash and Unified-red in terms of embracing newer technology and providing a path forward - as they seem to have ported a few more widgets and the like ?

Craig

Hi Craig,
The Unified-red project looks very professional. But to be honest I am not familiar with it. Perhaps because they have their own forum... I only had a quick look at it, and it seems to me that they don't have e.g. a way to plugin custom ui nodes. But I might be mistaken completely!

Anyway I suggest that we discuss such things (like comparing all the possible alternatives) in the original discussion. Because this discussion is dedicated to how we can implement custom ui nodes into Flexdash, to support @tve as much as we can with Node-RED specific knowledge...
Bart

Yep fair enough - sorry for the derail !!

Craig

1 Like

Just as a quick FYI, this train of thought has not run aground.
I spent some time this evening hacking up the mylittleuinode example UI node,
reading the ui-dashboard addWidget code, re-reading what Bart wrote,
and thinking about how to get started.
It all seems to make sense in my head, "just" need to do it...

1 Like

I'm running into a question about the use of {{...}} in the existing UI nodes. Specifically, some UI nodes allow configuration settings to be changed dynamically by using something like {{foo}} instead of a value in a config field and then sending the UI node a message that has msg.foo = "some dynamic value". The mustache notation was probably used because it falls out naturally from the implementation in angular. Question is what to do with this in the FlexDash nodes...

One option is just to parse the mustache notation, that's not really the issue. What I don't like is that it's not complete. For example, I can't put {{foo}} into the range min config of a gauge, that doesn't even pass validation, it only accepts a number. This notation "trick" probably only works for text fields. But in FlexDash it's very easy and natural to do the dynamic linking for any config field.

An alternative could be as follows:

  • the config UI in the flow editor could show the param name, e.g. range_min in the case above
  • any message to the node can have a msg.set field that holds a map of param names to values, e.g. msg.set = { range_min: -20 }
  • this allows any config value to be set dynamically
  • also, this allows a default/initial value to be set in the flow editor UI which can then be overriden dynamically, and also reset dynamically to the default/initial value using msg.set = { range_min: null }

Of course a "legacy mode" could be implemented as well that supports the mustache stuff for the params that currently support it and that has msg.set as the new way.

Thoughts?

watching with interest - but way out of my league in terms of abilities to comment meaningfully !

Craig

3 Likes

I'm running into a difficulty and don't know how to solve this elegantly...

Each Node-RED FlexDash node, like a gauge or a button, has a corresponding widget in the FlexDash config. That widget has an ID and I need to persist the ID of that widget in the config of the node. I thought I'd just add a "hidden" config param, e.g. fd_widget_id in this code snippet:

  RED.nodes.registerType('flexdash testgauge', {
    category: 'flexdash',
    inputs: 1,
    outputs: 0,
    [...]
    defaults: {
      // required fields for FlexDash Widget nodes
      fd: { type:"flexdash config", value:"", required:true }, // FlexDash config node
      fd_widget_id: { value: null }, // ID of widget in FlexDash to which this node corresponds
      name: { value: '' }, // name field of NR node (standard)
      [...]
    },

I have a function that given the fd_widget_id will ensure that widget exists in the FlexDash config or will create a new widget if it's null. I can call that function in the constructor of the node, but then how do I save the config again so the value is persisted if a widget got created?

Morning @tve,
Could you please explain this a bit more, because I am not familiar with the internals of Flexdash. A quickly drawn scetch would help :wink:

Currently we do it like this:

  1. You drag a UI node into your flow.
  2. You adjust the configuration on the config screen of that UI node.
  3. You deploy your flow.
  4. In the UI node we (= the node developers) have the node.id available, which we can use to create (in the HTML function on the backend) identifiers for our widgets when we generate our html. For example:
    <div id="my-ui-node-" + node.id ...>
    
  5. Then our frontend Js code can communicate with these html elements via the id:
    $("#my-ui-node" + node.id)
    

But do I understand correctly that you do it the other way around? Something like this:

  1. You add the widget to your FD config, which generates an id when the widget is not available yet.
  2. You want to store that FD widget id in your node.

Not sure if that is possible. But can't you just pass the node id to your FD config. So when FD needs to create the widget, that it first checks whether a default id is already passed. If so then it uses that one, and otherwise it will generate a new id (in case you don't work with UI nodes)?

Personally I like the id that all UI nodes use the same message field, which makes them uniform to use. But not sure what you mean with "legacy mode"? Because all ui nodes will have to be redesigned anyway. When I should find some time to do that, I am not going to spend any time in backwards compatitibility to be honest. If I need to do that, I don't have to go to sleep anymore... Moreover I have learned from my mistakes in the past, so I will probably implement some of my nodes in another way...

Now off to work...

Well... This flow

Produces the following dashboard:
image

That's two buttons and one gauge :smiley:

And the config panel for one of the buttons:

Yup, that layout could use some clean-up...

The code for a button is small enough I can paste it here:

module.exports = function (RED) {

  function flexdashTestButton(config) {
    const fd = RED.nodes.getNode(config.fd)
    RED.nodes.createNode(this, config)

    // propagate this node's config to the FD widget
    // The third arg is the kind of widget to create, if it doesn't exist
    fd.initWidget(this, config, 'PushButton')

    // handle flow input messages, basically massage them a bit and update the FD widget
    this.on("input", msg => {
      // prepare update of widget params
      const params = typeof msg.params === 'object' ? Object.assign({}, msg.params) : {}
      // msg.payload is interpreted as setting the button value, i.e. params.value
      if ('payload' in msg) params.value = msg.payload
      // send the params to the widget
      fd.updateWidget(this, params)
    })

    // handle widget input messages, we receive the payload sent by the widget
    fd.onInput(this, payload => {
      // the button sends the value of its output_value param, we just propagate it as msg.payload
      // note that output_value could be an object, so we could decide to use that as full msg...
      this.send({payload})
    })

  }

  RED.nodes.registerType("flexdash testbutton", flexdashTestButton)
}

I still have some packaging improvements left so the whole thing can be installed painlessly from within Node-RED... :tada:

2 Likes

@tve,
I am quite surprised to see this flow arriving so soon... Very cool!!!

Thank you very much for all your hard work, to introduce the ui node mechanism into Flexdash!!!

First question :slight_smile:

You now generate a ´PushButton´. How does a ui node developer generates his own widget. For example my ui-svg node. Or is that perhaps not possible yet in your first version?

1 Like

I knew you would ask that ;-). It's not yet possible. The widgets are baked in at the moment. It's one step at a time... (There are also significant improvements related to dynamically loading web components in Vue 3, so I'm trying to avoid battling Vue 2 needlessly.)

1 Like

Well my knowledge of Vue is zero...
Had a quick look and I see that Vue 3 is the default version since two weeks ago. Since Flexdash is developed in Vue 2.16.3 that you need to migrate to Vue 3. Which will be quite a huge job probably? Is that correct?

1 Like

Vue 3 has been out for quite a long time (a year?), that's not the issue. The problem is Vuetify 3, which is just about to get a first alpha out... I'm using Vuetify 2, which keeps me stuck on Vue 2...

I'm also not clear on what the end goal ought to be. On one end there are use-cases where it's just about a couple of lines of html with some variable substitutions and on the other end a UI node / widget may be a complex SVG visualization thing...

I need to go back and re-learn what the various packaging tools really do...

1 Like

Hi @tve I've been trying to keep up with this and the other thread (so my apologies if I have missed something).

firstly, some great progress there :clap:

Suggestions...

Templates (AKA create components in node-red and use them in templates in node-red)

To get more buy in, more quickly I would like to see 2 additional node types

  • FD-component - akin to a creating a vue component in a vue project
  • FD-template - akin to a .vue file in a project using internal and imported vue components to build a page (or part of page)

:point_up: Hope that makes sense and you can see the benefits?

NOTE: needs fleshing out (e.g. FD-component could actually use other FD-components and the FD-template might simply be another FD-component!) but the idea is to be able to build SPAs (and the like) from within node-red, also permitting users to make components and re-usable parts all within node-red (i.e. permit user to extend what is "off the shelf" in your V1)

Node fields

If you are not already, please consider using typedInputs for fields (where sensible) to provide maximum flexibility (and reduce the need for change nodes littered all over the place moving msg properties around)

1 Like

Can we please try to get some common agreement on things like this because it is the kind of thing that the whole community can get behind - it could become a common property to recommend across node development.

In doing so, we could have multiple new Dashboards that all use a common data schema.

This is partly the reason for suggesting the use of typedInput. Sure, it can default to msg.set (or msg.data etc) but forcing this to be the one and only property causes issues with other nodes that may have decided to use that property. This then leads to lots of change nodes before/after to shift things around.