Uibuilder: to subscribe or not to subscribe?

Where I see the benefit and point of decoupling the node may be not as offering different nodes but at the services level. Choose the service(s) and the project starting point (folder structure, required files, ....) will be built by that, providing then most common but still flexible structure. That would help a lot to get started cos the user have no need to dig into stuff he/she will never use but exist as side product.

Based on my own experiences and I am not smartest on starting big projects and creating the building tools for it from scratch.

1 Like

Side note: Thank you @tve about the uPlot hint. I was on path to not use any charts for my dashboard (going to be handheld friendly) but after a little research I put some on and it is amazingly powerful thing. Stress testing it for now to figure out best points for down-samplings but for first glance it seems that uPlot is not the bottleneck.
Slightly over 200 000 data-points about 1.2 sec in path : request -> query influxdb -> parse to add nulls for 10 series -> send to frontend -> create uPlot -> done (for my mid range samsung mobile a bit under 2 sec)

Reasonable charts (2-3 series) six hours of data with 15sec update rate are still near immediate.

2 Likes

Very nice work! Yes, when I read the design goals for uPlot it was love at first sight :laughing:

What do you use to code your UI?

TypeScript and zero front-end libraries. Pure handcraft.

Nice, but 200k points?! Can't you use InfluxDB to reduce that a bit? The chart looks like you have far more points than pixels.

Stress test is running. Of course that will be reduced but I need to figure out balance between info I'd like to have and performance. If that is settled, then is time to optimize. And also it is good to know where to optimize and how much to optimize. Complex stuff ...

Chart size is flexible and fits into screens so that is also a point to keep in mind in all those equations

1 Like

I spent a bit of time stepping back to contemplate how to couple messages and state between node-red and the UI.

Node-red is about messages and their flow is represented visually and the graph is static. State, especially persistent state, is secondary and a bit more cumbersome to manage, and not really a first-class object in the admin UI.

Modern web UIs are reactive, which means they couple the state of variables with the state of visual elements. The state is directly represented in terms of variables in the code whereas the flow of data is a side-effect of picking the same name in two or more places.

When building a UI for node-red we're trying to couple these two worlds and that's not simple in part due to this mismatch. Another way to express the mismatch is that a UI represents both state and changes (i.e. messages). For example, if the UI shows a switch for the light in your room, when you pull up the UI you want the switch to show the state of the light. And when you toggle the switch you want the UI to send a message to cause a change and then to reflect the new state.

Let me try to expand the light switch example assuming the light is controlled by sending an MQTT message or HTTP request. We have the state of the light in three places:

  • the actual HW light controller to which we send the message
  • in node-red, probably a persistent context variable
  • in the UI in a reactive variable from where it propagates to the DOM

In terms of functionality what we need is:

  • when a UI client connects, transfer the light's state from the persistent context to the UI's reactive variable
  • when the user actuates the switch in the UI, send a message to NR to update the persistent context value and to output a command to the light
  • optionally, at NR start-up, instead of relying on persistent state it may be possible to query the light bulb controller for the current state
  • in addition, we'll want to provide an input so other nodes can change the light state, for example, something that turns all lights off at midnight

What struck me is that when I started thinking about the distributed state I could stop thinking about the umpteen different UI widgets in node-red. Node-red doesn't have to have switches, buttons, text fields, drop-downs and all that. It can, that's one approach embodied in the current dashboard, but it's not the only approach.

A different approach is for node-red to have a node that couples state in node-red with state in the UI's reactive variables. From the node-red perspective the input to the ui-state node changes the state and its output sends a message whenever the state changes. Underneath, the ui-state node communicates with any browser to reflect the state into a "reactive variable".

The UI can then be virtually ignorant of node-red. It needs a module that connects to node-red and maintains the reactive variables but aside from that, the UI components really don't care. The variables could just as well be updated via database queries to a rest service.

Something all this does is decouple the state in node-red from the visual representation in the UI. Node-red doesn't care whether the light's state is represented by an icon, by a switch, by a text string, or by anything else. It also doesn't care whether the state is changed using a button, a slider, a switch, a drop-down, etc. All it needs to do is play its part in coupling its state with the state variables in the UI.

In terms of building the UI, it doesn't have to be a custom piece of code. There could be a visual designer that drops web components onto a canvas, couple them with state variables, and lets the user visually arrange the whole thing. The designer could be completely separate from node-red or it could be a part of the node-red admin UI. It doesn't matter. Maybe there's a UI designer project out there that can be used as-is...

I don't think this different approach replaces the current dashboard. Some people will prefer to have everything in the node-red admin UI and to connect all the parts with wires. (The trade-off being that the connection between a node and its visual element position is rather cumbersome.)

What is really attractive to me is the decoupling though. It means every web component doesn't also have to have a node with logic to massage messages plus a configuration panel for the admin UI. We "just" need one or a couple of nodes in NR to reflect state and web components that are appropriately crafted. Plus probably a designer UI to assemble the web components for those that prefer that approach.

(BTW, I'm not trying to say that I just invented all this, not at all. It's been said before and uibuilder does a lot of it. I'm just trying to formulate the big picture.)

Also, uPlot supports zooming, so it's nice to be able to instantaneously zoom in a bit to see more fine-grained data. There are practical limits to this, of course...

1 Like

@tve you seem quite taken with uPlot. I personally use echarts and absolutely love the capabilities. Have you evaluated them? Do you have any pros/cons on either? (There is also an Echarts Vue component/wrapper)

@Steve-Mcl I did not look much past the home page of echarts. It looks like every possible chart on the planet is supported, which is great in some ways and awful in others. uPlot focuses on time-series (although other charts are possible too) and that's 99% of my use-cases. The full uPlot download is 36.8KB the echarts is over 500KB, unless you repackage it to include only what you need. uPlot is faster and uses less memory, although echarts seems to be plenty fast. (Looking at the examples I find it amazing how much flexibility uPlot packs into 36KB)

After >20 years of futzing with time series charts I'm picky about stuff like handling nulls properly and not using splines to make it look like there's data where there really isn't and uPlot addressed exactly that right in the readme on github. So yeah, I'm fringe :slight_smile:

Bottom line is that I'm very particular about time-series charts... But for other types of charts echarts is going to be a better solution and maybe it does time-series well too and I just didn't dig deep enough. Thanks for pointing it out! In the end, I think we will need multiple chart components so it's OK for some to use uPlot, others echarts, and others yet something else.

After this interlude about uPlot... I did a little mock-up of the "new dashboard concept" and made a minimal implementation of the light switch... In node-red the way it looks (again, a mock-up, not a pretty implementation):

image

There are two groups: the top one is for the light switch, the bottom one is for the uibuilder connection to the web browser.

In the top group there are two inject nodes to manually trigger light-on and light-off from within the admin UI. Then there's a function node that uses persistent context to store the state of the light. Finally there's a debug node that represents the output the the hardware light controller. If you hit one of the inject nodes you'll see the appropriate state change message in the debug pane.

The bottom group manages the connection to web browsers. There's a uibuilder node called d-mini ("dashboard mini demo") and it is cross-connected with a function node UI-init whose role it is to send the initial UI state (just the light's state in this demo) to new web browsers.

Then there are a bunch of link nodes that add a lot of visual clutter and that would be eliminated if I used custom nodes instead of function nodes. The link nodes propagate changes from UI-state to the browser and from the browser to UI-state.

On the web UI side there are a slew of files, but what matter is all at the very top of ./src/Dash.vue:

<template>
  <v-app>
    <v-main>
      <v-container class="d-flex">
        <v-card class="mr-auto d-flex flex-column">
          <v-card-title>Light Switch</v-card-title>

          <v-switch class="mx-auto"
                    v-bind:value="nr.light" true-value="ON" false-value="OFF"
                    @change="send({light:$event})"

          ></v-switch>
        </v-card>
      </v-container>
    </v-main>
  </v-app>
</template>

This is an HTML template that plops down a bunch of nested components from vuetify with an innermost v-switch component that shows a simple light switch:

image

The magic in all this is the binding:

  • in the UI-state function node I set name="light" to tell it to persist the state as "light"
  • in the v-switch component I bind (using a v-bind= attribute) the visual state of the switch to nr.light ("nr" stands for node-red), and when the switch is toggled it emits an @change event which I bound to send({light:$event}) which causes a message with topic "light" and the new state of the switch as value to be sent to node-red.

So basically we bound "light" in node-red to "nr.light" in the web UI and in turn to the value of that web component.

Now what happens is that everything is in sync. When you pull-up the web UI it shows the current state of the light from node-red, when you toggle the switch in the web UI or using the inject nodes the appropriate messages are output and the web component is changed to reflect the new state. If you restart the NR flow or restart node-red altogether the previous state of the light is also preserved.

Obviously this functionality isn't new and also perfectly achievable using the current dashboard. The point of the demo is that the two can be coupled just by giving a common name to a piece of state. In this demo:

  • node-red doesn't know or need to know what type of UI element is used to represent the state
  • the UI is completely oblivious of node-red, there's just a small piece of code to send/receive messages over a socket.io connection
  • the UI can be composed by assembling some web components and binding them to pieces of state (this can be made simpler than the v-bind and @click stuff by wrapping the v-switch)

In case anyone wants to look, everything for this demo is in GitHub - tve/uibuilder-mini: Mini demo of node-red uibuilder and vuetify

This is something that could easily be added to uibuilderfe. A mapping from an incoming topic to a browser variable (a Vue variable in this case but that is irrelevant). That alone would eliminate the need for much of the Node-RED logic since everything could simply be sent straight to the front-end and anything not relevant would simply be ignored, anything relevant would automatically update the variables and hence the UI.

That could also be used to trigger the initial layout as well. On initial startup, a new client could be sent both the layout configuration and the initial data settings.

That is really how uibuilder works anyway. But at the moment, you are responsible for mapping the incoming msg's to UI components. Auto-mapping would be one less thing that people needed to do and would nicely simplify the UI javascript particularly when combined with Vue's generic v-bind.

The @click of course already has a helper that lets you assign a click method directly to a uibuilder method so that it becomes a 1-liner.

Can you elaborate? If I have 10 gauges in the UI I still need to specify which piece of data each gauge is supposed to show, right?

Ah, I had forgotten about eventSend, yup, something like that is what's needed.

Having formulated the loose coupling between node-red and the UI, the next step kind'a falls right out: a self-configuring dashboard UI!

In the current dashboard the configuration happens on the node-red side: there are fields in each UI node and a dashboard designer side bar to configure everything. Now that node-red no longer needs to know about the UI it frees the UI to configure itself.

The idea is that the content area of the UI consists of a grid (duh) and that grid has a little +add button tucked into a corner. Press that button and a modal comes up with a drop-down selector of components and you can choose which type you want to add. Once you choose the component type the modal expands with the UI options offered by that component, one field being the name of the state variable to bind to the component. Hit submit and the component is added to the grid. Under the hood the grid is bound to a state variable just like a gauge or switch would be and that state variable contains the configuration for all the components in the grid. This way the configuration state of the grid is persisted via node-red.

Of course a bit more is needed beyond just an +add button 'cause one has to be able to reorder components, delete them, and edit their configuration. Also, the whole page shouldn't be just a grid: it would be nice to be able to pick components for top-nav, left sidebar, bottom-bar, and in the main area one could have multiple grids one after the other that each lay components out differently, including full-custom grids.

The bottom line of all this is that a UI designer as a separate application or as a sidebar in node-red is not needed. The UI frame can be written to include its own designer functionality, which incidentally can be totally live, i.e., while the UI and all the components are running and showing live data. This is turning out a lot more interesting than I had anticipated!

NB: the existing NR UI components could be ported to this model. It would take a grid that lays components out the way the current UI does and instead of live editing the config would be prepared by the NR UI nodes.

Sorry, I'm more thinking about the little guy rather than your use-case. Just something to make it even easier to link data from Node-RED to the UI components without needing code:

uibuilder.automap(this, [
    {var: 'var1', topic: 'sending_var1'},
    {var: 'var2', topic: 'sending_var2'},
    {var: 'var3', topic: 'sending_var3'},
])

Where this would be your Vue app reference - or could be window for example if you wanted to use global vars (e.g. not using a framework). The array maps a msg to a variable via the msg.topic. In this case, you would define var1 etc in your Vue data section. If a msg is sent from Node-RED with a topic of 'sending_var1', the uibuilderfe library would automatically apply the msg.payload to the appropriate variable.

Not quite decided whether this is really enough of a benefit to be worth while. But it kind of beats having to do an onChange function with a select/case in it.

1 Like

The saga continues, now with a proof-of-concept dashboard designer UI. Here's a UI with a tab that has a grid with zero components and an add button in the upper-right:

Clicking on the add button brings up a modal with a drop-down list of available components (the drop-down looks awkward in this screen shot without cursor or highlighting):

Selecting a component expands the modal to show the properties that can be defined. Each property can be set to a literal string or it can be bound to a reactive state variable using a drop-down of existing state variables:


Hitting the add button adds the component to the grid, and it's live (I added two components):

Reloading the page and keeping the added components works too: the component config is persisted in node-red.

Baby steps, but this looks promising to me! :horse_racing:

2 Likes

Hi Thorsten,
That looks very nice!
I have to admit that this discussion goes a bit too much into frontend framework detail for me, so would be nice if you could answer my noob question below.
I have develped a series of custom UI nodes in the past for the current AngularJs dashboard. Will it also be possible to develop such nodes in your dashboard concept? I mean UI nodes that can be installed separately from the palette, and dragged into the flow. Like the current dashboard allows us to do at the moment.
Or is that not the purpose of your setup?
Thanks for the clarification!
Bart

Yes, "anything is possible" :slight_smile: That being said, unless others get excited and chip in, what I'm doing is unlikely to reach "production quality".

In a bit more detail, the FlexDash expects that each widget is a web component that has a certain interface (details still being worked out). So you can author additional web components. That is similar in effort to authoring new node-red nodes but different.

It may be possible to create a widget similar in spirit to the uitemplate node where the user can compose wimple web components without having to install tooling. That would be useful, for example, to create custom arrangements of buttons, indicator lights switches, drop-downs, text fields, etc.

Note that with FlexDash you really don't drag UI components into the node-red flow. Instead you send messages with a topic and payload to a UI state node. Then, in the dashboard itself, you drag a widget onto your grid and you attach it to the same topic.

1 Like

One thing this discussion has prompted me to do is to start refactoring some of the uibuilder core js.

In particular, I was reminded about the Singleton mode for node.js modules and classes. And so I've started moving all of the Socket.IO logic into a separate module.

The interesting side-effect of this will be that we can reference this singleton module from other modules - including other nodes. Which immediately makes it possible to have more nodes in the uibuilder "universe" using the same Socket.IO channels.

It is a fair chunk of work so won't happen overnight - v3.3.0 will be released shortly with some fixes and a new feature that lets you send a msg that will reload the browser page. Phase 1 of a proper development server capability for uibuilder right inside Node-RED.

The refactoring of the Socket.IO functions will let me send a message to the front-end from more places in the code, including the API's which are currently defined outside the instance and therefore do not currently have access to Socket.IO and therefore cannot send a message to the front-end.

That will mean that I can enable a flag in the uibuilder file editor panel that will trigger all connected clients of a uibuilder instance to reload when a file is saved.

A later improvement will be to add the option of using chokidar to watch the instances folders for changes and automatically trigger a reload.

Doing this refactoring should also give me the knowledge to do similar work on other areas of uibuilder so that we can make it even more flexible.

For me it would be great if there was a core module that only handled the basic socket.io connection without all the helpers to inspect or auto-handle stuff...