FlowFuse / node-red-dashboard

https://dashboard.flowfuse.com
Apache License 2.0
187 stars 47 forks source link

How exactly does a widget use ui_base? #1289

Open AllanOricil opened 1 week ago

AllanOricil commented 1 week ago

I read the dashboard 2 docs and understood that widgets are node-red nodes, and that they "extend" the ui_base config node, but I could not understand how exactly this happens. Somehow the widget's node has some additional methods called register/deregister. Probably has other, but I did not read everything yet. So, when/how exactly does a widget receive those 2 methods?

I'm planning to recreate it as a base class of Node class called, in the nrg project.

export default class WidgetNode extends Node {

    // specific WidgetNode properties

    constructor(config){
        super(config);
        //initialize specific WidgetNode properties, such as state store
    }

    // specific WidgetNode methods, like register/ deregister
}
export default class MyWidget extends WidgetNode {

    constructor(config){
        super(config);
    } 
}
joepavitt commented 1 week ago

The UI Base is the single parent object for a Dashboard.

In the constructor for each Widget, they get reference to the UI Base (normally via their assigned group/page). They call base.register, and pass themselves into that function call.

The Base then maps pages/groups/widgets/themes, and sends the relevant configurations when a Dashboard client connects

AllanOricil commented 1 week ago

@joepavitt what do you think of my idea of making ui_base into a parent class called WidgetNode, and then widgets would extends this class? I think it is possible. Could you help me to see problems I would have trying to do it?

joepavitt commented 1 week ago

The UI Base has settings and configuration of its own. The widgets aren't necessarily extensions in a Class sense. They are different entities. There is only ever one Base. Having a Node an extension of Base would mean you suddenly have 100's of Dashboards/Based

If we consider Class hierarchy, there could be benefit in having a common WidgetNode class that the nodes, in order to share feature sets, etc.

The base.register function is the Widget's way of communicating to the Dashboard to say "Hey, I exist"

joepavitt commented 1 week ago

My use of the word "extends" here is likely misleading.

From a Dashboard Hierarchy we have:

That doesn't mean that there is Class Inheritance here though. Fundamentally, these are the Classes, then each node in a flow (or config node) in an instance of the respective class

AllanOricil commented 1 week ago

@joepavitt So every WidgetNode class would have to have an instance of ui_base. Is this ok? I'm still not sure if "register/deregister" is called "per instance" of a node, or per type of node. If it by type, then I need to make those 2 methods static

export default class WidgetNode extends Node {

    static #uiBase;

    constructor(config){
        super(config);
        //initialize specific WidgetNode properties, such as state store
    }

    // specific WidgetNode methods, like register/ deregister

    register(){
         WidgetNode.#uiBase.register(this);
    }

    deregister(){
         WidgetNode.#uiBase.deregister(this);
    }
}
joepavitt commented 1 week ago

There are three types of reference, which depend on the "home" of the widget.

If it renders in a group (the most common, e.g Buttons, Charts), then the Widget has a group property. But in turn, can then use group.getBase().register() to register itself with the one true UI Base at the top level.

Some widgets render at the page-level, for example a UI Template might be assigned to a page to override CSS on that page. Similarly, there exists a page.getBase() in order for the Widget to register to the Base too.

Finally, the simplest in this case, is a Widget that renders at the "base" level, I.e. independent on whichever page a user is one that Widget will always be there. An example of this is "UI Notification", where the user chooses the "Base" in the Node config editor, and so we can just call base.register directly

joepavitt commented 1 week ago

Also worth noting, and I may be remembering this wrong, the nodes themselves dont need a register/deregister function.

The base.register() is just called as part of the Node's constructor

Maybe they do need a deregister function? I can't quite remember how I handle that, as I'm not at a keyboard

AllanOricil commented 1 week ago

@joepavitt

Maybe they do need a deregister function? I can't quite remember how I handle that, as I'm not at a keyboard

I believe that if a node can call this.deregister(), maybe there are situations where the node would also call this.register() afterward. Maybe to do a "hard reset" somewhere? Do you know if there is any Widget that does it?

The base.register() is just called as part of the Node's constructor

I can ensure this is called inside the parent's constructor, or in the mixin. This way devs won't need to manually write it. The same way I did for RED.nodes.registerType or RED.nodes.createNode

https://github.com/AllanOricil/nrg/blob/main/templates/server/entrypoint.handlebars https://github.com/AllanOricil/node-red-node

AllanOricil commented 1 week ago

@joepavitt What do you think of this model?

import { Node, Base, Theme, Page, Group } from "@allanoricil/node-red-node";

export default class WidgetNode extends Node {

    scope: Base | Theme | Page | Group;  // each instance of a widget can have its own ui scope

    constructor(config){
        //initialize specific WidgetNode properties, such as state store
    }

    // specific WidgetNode methods, like register/ deregister

    register(){
         this.scope.register(this);
    }

    deregister(){
         this.scope.deregister(this);
    }
}
import { WidgetNode } from "@allanoricil/node-red-node";

export default class ChildWidget extends WidgetNode {

    constructor(config){
        super(config);
        this.scope = TO_BE_DEFINED; // dev configures which scope this node belongs to. The mixin will use it to call `this.register()` for his node
    }

}
joepavitt commented 1 week ago

That could work, bare in mind though some widgets are permitted to be multiple, e.g. ui-template can be switched between group, page and UI

AllanOricil commented 1 week ago

That could work, bare in mind though some widgets are permitted to be multiple, e.g. ui-template can be switched between group, page and UI

How does a Widget switch between "ui scopes" (have to find a better name)?

colinl commented 1 week ago

It is defined in template node's configuration

image

AllanOricil commented 1 week ago

It is defined in template node's configuration

image

@colinl every widget has those fields shown in that image? How can I get to that configuration form?

AllanOricil commented 1 week ago

I started to have a better understanding after reading the config nodes

image

I think I can start modeling it into classes. I will try to do the base ones, and a button.

AllanOricil commented 1 week ago

One last question.

@colinl @joepavitt

What is the schema for the "evts" object widgets pass when calling group.register(node, config, evts)? Also, why passing config to register if you are already passing node? I believe you can dereference config from the node, right?

const evts = {
            beforeSend: async function (msg)
}

group.register(node, config, evts)

After modeling into classes, this is what I'm planning for the event handlers. Therefore, I must know its schema.

import { Node } from "@allanoricil/node-red-node";
export default Widget extends Node {

    group;

     constructor(config){
          super(config);

          this.group = config.group;
      }

     /*
     * It can be implemented by a child class
     * @abstract
     */
    onBeforeSend(msg){}

     // all other possible event handlers a widget can have
     ...
}
import { Widget } from "@allanoricil/node-red-node";
export default MyWidget extends Widget {

      constructor(config){
          super(config);

          // set things that are specific for a given "instance", like initializing its own properties
          this.myCoolAttribute = config.myCoolAttribute;
      }

      // since this is a widget, this event handler "could" be implemented
     onBeforeSend(msg) {
            ...
      }

      // all other event handlers
     ...
}

Inside the mixin, I will do some sorcery js to make sure these "on" events are registered when calling group.register(node, config, evts).

AllanOricil commented 1 week ago

@joepavitt @colinl could you help with the last comment?

colinl commented 1 week ago

Not me, I don't know the answer.

joepavitt commented 6 days ago

@AllanOricil https://dashboard.flowfuse.com/contributing/guides/registration.html#evts

joepavitt commented 6 days ago

every widget has those fields shown in that image? How can I get to that configuration form?

Drop a Dashboard "template" node into the Editor, then double click it to configure it. Only the "Template" node as the option to switch between "parent scope" like this.

AllanOricil commented 6 days ago

@AllanOricil https://dashboard.flowfuse.com/contributing/guides/registration.html#evts

@joepavitt Is the onInput from evts the same as the node's on input this.on("input", onInputHandler())?

If not, is there a widget that use both? If there is, than I would need to change the evts event handler name to avoid conflicts

import { Widget } from "@allanoricil/node-red-node";

export default MyWidget extends Widget { 

    constructor(config){ 
        super(config); 
    }

    // same as node-red this.on("input", ())
    onInput(msg){...}

    // dashboard ui input event
    onUIInput(msg){...}

    // dashboard ui before send
    onBeforeSend(msg){...}
}
joepavitt commented 6 days ago

Not quite, the one you've quoted there is the client-side handler (Vue Widget), the onInput I linked to is the server-side handler (the Node-RED node)

AllanOricil commented 6 days ago

Not quite, the one you've quoted there is the client-side handler (Vue Widget), the onInput I linked to is the server-side handler (the Node-RED node)

So all evts handlers are sent to the client? I thought they executed on the server, because they are declared in the server js of a node.

joepavitt commented 6 days ago

No, they're separate. What a node does on input server-side (defined in the .js file) will differ the the client-side behaviour of a widget when receiving a message (defined in the vue file)

AllanOricil commented 6 days ago

@joepavitt

Sorry, but I got confused a bit. Let me try to make a better question.

The doc says the evts handlers are server- side. Is the evts.onInput the same as this.on("input") inside the node's server-side js? Is there a node that make use of both? I need to see how they are used.

joepavitt commented 6 days ago

Sorry, I thought this.on("input", onInputHandler()) had been taken from a vue file as it's very similar to our code there too.

Yes, if you checkout the node.register function in ui-base.js, you'll see that the on('input') handler is setup for the relevant node and the evts.onInput is passed there (if provided)

AllanOricil commented 6 days ago

Sorry, I thought this.on("input", onInputHandler()) had been taken from a vue file as it's very similar to our code there too.

Yes, if you checkout the node.register function in ui-base.js, you'll see that the on('input') handler is setup for the relevant node and the evts.onInput is passed there (if provided)

I would need a new name for that handler to put it on the class alongside the node's this.on("input")

I can't have 2 onInput methods in the class. What about onData or onDashboardInput or onUIInput for the evts.onInput? If the second one is chosen, then all other methods would also be prepended with either onDashboard or onUI

joepavitt commented 6 days ago

not sure why the class needs that definition though? It needs evts, which is then an object mapped to x functions/objets/booleans

AllanOricil commented 6 days ago

not sure why the class needs that definition though? It needs evts, which is then an object mapped to x functions/objets/booleans

When one of those handlers is triggered, can't they access "instance" data? I can have multiple widget nodes, all with the same handler, but each with its own instance scope. Those events are part of the Widget, so I'm trying to encapsulate them normalized as individual class functions, instead of using an evts attribute. It looks cleaner in my opinion.