Trying to load npm package justgage into UIBUILDER 7.4.3 NR 4.0.9

Running Node-RED on Ubuntu 24.04

I am trying to load Justgage into UIBUILDER and I am having problems. It appears through the Browser Console (Vivaldi and Firefox) that Raphael and Justgage are not being run because of some security problem.

Space is left for the gauge, but nothing appears in it after injecting the payload and topic. I have followed @Paul-Reed instructions from his detailed post and also confirmed that the instructions are still valid.

Any ideas gladly accepted!!

Using CDN

VM1099:18 callback globalThis.ns_setupCallback is not a function (undefined).
(anonymous) @ VM1099:18
(anonymous) @ VM1099:26
panel/:1 [Report Only] Refused to load the script 'https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.4/raphael-min.js' because it violates the following Content Security Policy directive: "script-src-elem 'none'".

panel/:1 [Report Only] Refused to load the script 'https://cdnjs.cloudflare.com/ajax/libs/justgage/1.7.0/justgage.min.js' because it violates the following Content Security Policy directive: "script-src-elem 'none'".

panel/:1 [Report Only] Refused to load the script 'http://localhost:1880/uibuilder/uibuilder.iife.min.js' because it violates the following Content Security Policy directive: "script-src-elem 'none'".


Using local

VM889:18 callback globalThis.ns_setupCallback is not a function (undefined).
(anonymous) @ VM889:18
(anonymous) @ VM889:26
panel/:1 [Report Only] Refused to load the script 'http://localhost:1880/uibuilder/vendor/raphael/raphael.min.js' because it violates the following Content Security Policy directive: "script-src-elem 'none'".

panel/:1 [Report Only] Refused to load the script 'http://localhost:1880/uibuilder/vendor/justgage/dist/justgage.min.js' because it violates the following Content Security Policy directive: "script-src-elem 'none'".

panel/:1 [Report Only] Refused to load the script 'http://localhost:1880/uibuilder/uibuilder.iife.min.js' because it violates the following Content Security Policy directive: "script-src-elem 'none'".

panel/:25  GET http://localhost:1880/uibuilder/vendor/raphael/raphael.min.js net::ERR_ABORTED 404 (Not Found)
panel/:35 Refused to execute script from 'http://localhost:1880/uibuilder/vendor/raphael/raphael.min.js' because its MIME type ('text/html') is not executable, and strict MIME type checking is enabled.
justgage.js:20 Uncaught ReferenceError: Raphael is not defined
    at justgage.js:20:29
    at justgage.js:8:1
(anonymous) @ justgage.js:20
(anonymous) @ justgage.js:8

[{"id":"b48f1cec0c424cac","type":"catch","z":"584f914f499733a6","name":"","scope":null,"uncaught":false,"x":140,"y":60,"wires":[["0396434b195a9d23"]]},{"id":"0396434b195a9d23","type":"debug","z":"584f914f499733a6","name":"Catch Err","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":320,"y":60,"wires":[]},{"id":"1990d038d58b543b","type":"group","z":"584f914f499733a6","name":"Static Dashboard","style":{"fill":"#bfdbef","fill-opacity":"0.31","label":true,"color":"#000000"},"nodes":["9a66ac6446246585","7d9436c9535bb04e","bc90cd16ec35bc86","227db5eabf708658","9c3eb8d9afa912ea","c0acb041b0ace082","ab9ab2ee1e1e982b","53d8e4c892caae0f","131dc3f34ef3f4be","b4f102b6d584e292","339b98e312dc1cc3","17da9997449ebc65","d000b4c883b4723d","f7d7a879f4c4db1e","7f620b906e81dd64","18e3e837e73c7048","657d42630f1da5b0"],"x":74,"y":139,"w":1012,"h":448},{"id":"9a66ac6446246585","type":"inject","z":"584f914f499733a6","g":"1990d038d58b543b","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":215,"y":220,"wires":[["53d8e4c892caae0f"]],"l":false},{"id":"7d9436c9535bb04e","type":"uibuilder","z":"584f914f499733a6","g":"1990d038d58b543b","name":"","topic":"","url":"panel","okToGo":true,"fwdInMessages":false,"allowScripts":false,"allowStyles":false,"copyIndex":true,"templateFolder":"blank","extTemplate":"","showfolder":false,"reload":true,"sourceFolder":"src","deployedVersion":"7.4.1","showMsgUib":false,"title":"","descr":"","editurl":"vscode://file/home/colin/.node-red/projects/2801101/uibuilder/panel/?windowId=_blank","x":580,"y":280,"wires":[["bc90cd16ec35bc86"],["227db5eabf708658","657d42630f1da5b0"]]},{"id":"bc90cd16ec35bc86","type":"debug","z":"584f914f499733a6","g":"1990d038d58b543b","name":"debug 452","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"","statusType":"counter","x":775,"y":260,"wires":[],"l":false},{"id":"227db5eabf708658","type":"debug","z":"584f914f499733a6","g":"1990d038d58b543b","name":"debug 453","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"","statusType":"counter","x":935,"y":280,"wires":[],"l":false},{"id":"9c3eb8d9afa912ea","type":"comment","z":"584f914f499733a6","g":"1990d038d58b543b","name":"Design","info":"This layout is designed to roughly replicate\nthe Node-RED Dashboard grid layout.\n\nIt keeps the outer layout from the content/blog\nstyle (header, footer, left-sidebar, main).\n\nBut the sidebar now has the navigation menu\ninstead of the header.\n\nThe main content area is now a sub-grid. It has\nthe `id` of `more` which means that you can\neasily add new content using uibuilder's \nno-code nodes.\n\nEach `<article>` or `<div>` tag in the main \ncontent sub-grid can be considered the \nequivalent of a \"widget\" in Node-RED Dashboard \nterms. It can contain whatever you want it to.\nIt has a slightly different background colour\nand a rounded border.\n\nIn the example all of the layout can be\ncontrolled from the CSS variables in `:root`.\n\nThe sub-grid has 12 columns by default, you\ncan change that to whatever is useful to you.\n\nThe articles have a default width of 3. You can\neasily override a single article by adding:\n`--article-width: 6` or however many columns\nyou want it to take up.\n\nBy default, specific grid rows are undefined\n(set to `auto`) so articles take up whatever\nheight they need. If you want to define the\nrows, you can easily do so by changing\n`--main-rows` to `repeat(12, 1fr)` or some\nother number. You have lots of control over the\ncolumn/row layout but it can get a bit complex\nso you are advised to keep it simple, at least\nto start with.\n\n-----------------\n\n## NOTES\n\nIt is very likely that much of the\ndefined CSS in this example will be incorporated\ninto UIBUILDER's `uib-brand.css` in the \nfuture.\n\nThe Markdown-IT library has been included in\n`index.html`, loaded from a public Internet\nCDN. Install & use the library locally using \nUIBUILDER's library manager if you prefer.\nOr remove completely if you don't need to use\nMarkdown dynamic content.","x":940,"y":180,"wires":[]},{"id":"c0acb041b0ace082","type":"group","z":"584f914f499733a6","g":"1990d038d58b543b","name":"Setup - run this first to set up the page and style - only needs to run once","style":{"fill":"#ffffff","fill-opacity":"0.31","label":true,"color":"#000000"},"nodes":["428ce314901f3767","de7eb158dfc01708","5d994d5ed445588e","d1613f2217e9ecb1","5dcdb42f869e188f","65ed85522e1f51ac","3234eca4ec17a21a","a3d2eb3e24fefdbf"],"x":134,"y":399,"w":652,"h":162},{"id":"428ce314901f3767","type":"template","z":"584f914f499733a6","g":"c0acb041b0ace082","name":"index.html","field":"payload","fieldType":"msg","format":"html","syntax":"mustache","template":"<!doctype html>\n<html lang=\"en\">\n\n<head>\n\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <link rel=\"icon\" href=\"../uibuilder/images/node-blue.ico\">\n\n    <title>Theodore</title>\n    <meta name=\"description\" content=\"Theodore - Dashboard Layout\">\n\n    <!-- Your own CSS (defaults to loading uibuilders css)-->\n    <link type=\"text/css\" rel=\"stylesheet\" href=\"./index.css\" media=\"all\">\n\n    <!-- #region Supporting Scripts. These MUST be in the right order. Note no leading / -->\n    <!-- <script defer src=\"https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js\"></script> -->\n\n    <!-- Raphael must be included before justgage -->\n\n    <script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.4/raphael-min.js\"></script>\n    <script type=\"text/javascript\" src=\"https://cdnjs.cloudflare.com/ajax/libs/justgage/1.7.0/justgage.min.js\"></script>\n\n<!--\n    <script defer src=\"../uibuilder/vendor/raphael/raphael.min.js\"></script>\n    <script defer src=\"../uibuilder/vendor/justgage/dist/justgage.min.js\"></script>\n-->\n    <script defer src=\"../uibuilder/uibuilder.iife.min.js\"></script>\n    <!-- <script defer src=\"./index.js\">/* <= OPTIONAL: Put your custom code in that */</script> -->\n    <!-- #endregion -->\n\n</head>\n\n<body>\n    <div class=\"container\">\n        <!--\n            <header class=\"header\">\n                 <h1 class=\"with-subtitle\">Theodore Dashboard</h1>\n            </header>\n            -->\n\n<div id=\"gauge-temp\" class=\"gauge\"></div>\n\n        <main id=\"more\">\n            <!-- '#more' is used as a parent for dynamic HTML content in examples -->\n            <article>\n                <h2>Temperatures &deg;C</h2>\n            </article>\n\n            <article>\n                <h2>Fridge &deg;C</h2>\n            </article>\n        </main>\n    </div>\n</body>\n\n</html>","output":"str","x":530,"y":440,"wires":[["d1613f2217e9ecb1"]]},{"id":"de7eb158dfc01708","type":"inject","z":"584f914f499733a6","g":"c0acb041b0ace082","name":"","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":"2","topic":"setup all FE files","x":195,"y":440,"wires":[["5d994d5ed445588e","5dcdb42f869e188f","3234eca4ec17a21a"]],"l":false},{"id":"5d994d5ed445588e","type":"change","z":"584f914f499733a6","g":"c0acb041b0ace082","name":"index.html","rules":[{"t":"set","p":"fname","pt":"msg","to":"index.html","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":350,"y":440,"wires":[["428ce314901f3767"]]},{"id":"d1613f2217e9ecb1","type":"uib-save","z":"584f914f499733a6","g":"c0acb041b0ace082","url":"panel","uibId":"7d9436c9535bb04e","folder":"src","fname":"","createFolder":false,"reload":true,"usePageName":false,"encoding":"utf8","mode":438,"name":"","topic":"","x":710,"y":440,"wires":[]},{"id":"5dcdb42f869e188f","type":"change","z":"584f914f499733a6","g":"c0acb041b0ace082","name":"index.css","rules":[{"t":"set","p":"fname","pt":"msg","to":"index.css","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":340,"y":480,"wires":[["65ed85522e1f51ac"]]},{"id":"65ed85522e1f51ac","type":"template","z":"584f914f499733a6","g":"c0acb041b0ace082","name":"index.css","field":"payload","fieldType":"msg","format":"css","syntax":"plain","template":"/* Load defaults from `<userDir>/node_modules/node-red-contrib-uibuilder/front-end/uib-brand.min.css`\n * This version auto-adjusts for light/dark browser settings.\n */\n@import url(\"../uibuilder/uib-brand.min.css\");\n\n.gauge {\n    width: 325px;\n    height: 200px;\n    display: inline-block;\n}","output":"str","x":520,"y":480,"wires":[["d1613f2217e9ecb1"]]},{"id":"3234eca4ec17a21a","type":"change","z":"584f914f499733a6","g":"c0acb041b0ace082","name":"index.js","rules":[{"t":"set","p":"fname","pt":"msg","to":"index.js","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":340,"y":520,"wires":[["a3d2eb3e24fefdbf"]]},{"id":"a3d2eb3e24fefdbf","type":"template","z":"584f914f499733a6","g":"c0acb041b0ace082","name":"index.js","field":"payload","fieldType":"msg","format":"css","syntax":"plain","template":"\ndocument.addEventListener('DOMContentLoaded', ()=> {\n\n        // Create just one gauge up front\n        const tempGauge=new JustGage( {\n\n                id: 'gauge-temp',\n                value: 0,\n                min: 0,\n                max: 100,\n                symbol: '°C',\n                pointer: true,\n                pointerOptions: {\n                    toplength: -15,\n                    bottomlength: 10,\n                    bottomwidth: 12,\n                    color: '#8e8e93',\n                    stroke: '#ffffff',\n                    stroke_width: 3,\n                    stroke_linecap: 'round'\n                }\n\n                ,\n                title: 'Temperature (°C)',\n                label: 'Temperature',\n                labelMinFontSize: 14,\n                valueFontColor: '#a2a2a2'\n            }\n\n        );\n\n        // Handle incoming data via uibuilder\n        uibuilder.onChange('msg', (msg)=> {\n                if (msg.topic==='sensorTemp') {\n                    tempGauge.refresh(Number(msg.payload));\n                }\n\n                else {\n                    console.warn('Unhandled topic', msg.topic, msg.payload);\n                }\n            }\n\n        );\n    }\n\n);","output":"str","x":520,"y":520,"wires":[["d1613f2217e9ecb1"]]},{"id":"ab9ab2ee1e1e982b","type":"link in","z":"584f914f499733a6","g":"1990d038d58b543b","name":"uib input","links":["101a4ac6efce50b6","0a7182f9c1d85b85","18e3e837e73c7048"],"x":255,"y":320,"wires":[["53d8e4c892caae0f"]]},{"id":"53d8e4c892caae0f","type":"junction","z":"584f914f499733a6","g":"1990d038d58b543b","x":320,"y":280,"wires":[["131dc3f34ef3f4be"]]},{"id":"131dc3f34ef3f4be","type":"uib-cache","z":"584f914f499733a6","g":"1990d038d58b543b","cacheall":false,"cacheKey":"topic","newcache":false,"num":"15","storeName":"memory","name":"Cache","storeContext":"context","varName":"uib_cache","x":390,"y":280,"wires":[["7d9436c9535bb04e"]]},{"id":"b4f102b6d584e292","type":"comment","z":"584f914f499733a6","g":"1990d038d58b543b","name":"msg.topic and msg.payload pass through to UIBUILDER","info":"","x":320,"y":360,"wires":[]},{"id":"339b98e312dc1cc3","type":"debug","z":"584f914f499733a6","d":true,"g":"1990d038d58b543b","name":"uib client connect/disconnect Output","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"","statusType":"counter","x":1025,"y":340,"wires":[],"l":false},{"id":"17da9997449ebc65","type":"inject","z":"584f914f499733a6","g":"1990d038d58b543b","name":"Clear Cache","props":[{"p":"uibuilderCtrl","v":"clear cache","vt":"str"},{"p":"cacheControl","v":"CLEAR","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":190,"y":280,"wires":[["53d8e4c892caae0f"]]},{"id":"d000b4c883b4723d","type":"debug","z":"584f914f499733a6","g":"1990d038d58b543b","name":"debug 17","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":630,"y":180,"wires":[]},{"id":"f7d7a879f4c4db1e","type":"inject","z":"584f914f499733a6","g":"1990d038d58b543b","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"sensorTemp","payload":"37","payloadType":"num","x":400,"y":180,"wires":[["7d9436c9535bb04e","d000b4c883b4723d"]]},{"id":"7f620b906e81dd64","type":"debug","z":"584f914f499733a6","g":"1990d038d58b543b","name":"route changes","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"","statusType":"counter","x":910,"y":380,"wires":[]},{"id":"18e3e837e73c7048","type":"link out","z":"584f914f499733a6","g":"1990d038d58b543b","name":"To input","mode":"link","links":["ab9ab2ee1e1e982b"],"x":780,"y":340,"wires":[]},{"id":"657d42630f1da5b0","type":"switch","z":"584f914f499733a6","g":"1990d038d58b543b","name":"Route different control msgs","property":"uibuilderCtrl","propertyType":"msg","rules":[{"t":"regex","v":"client (dis)?connect","vt":"str","case":false},{"t":"eq","v":"route change","vt":"str"},{"t":"eq","v":"visibility","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":4,"x":720,"y":360,"wires":[["18e3e837e73c7048","339b98e312dc1cc3"],["7f620b906e81dd64"],[],[]],"outputLabels":["Network (socket) client connect/disconnect","uibrouter route changes","page visibility","Anything else"],"l":false}]

This, I think is the source. You have somehow set your web server to block ALL access to JavaScript by blocking the loading from <script> tags. Oops! :smiley:

Doh! I’ll check it out.

I do have NoScript running, but NR et al. are trusted sites. Over to Brave which is my ‘clean’ browser in that it has no extensions of any type. I’ll let you know!

Yes, much more likely to be a proxy server or configuration of ExpressJS in Node-RED.

I am currently looking at the messages when you view the page in a browser to try to understand the ‘chain of events’. CDN raphael and justgage load without error, and so I am working with them at the moment. Still can’t get a gage to display although the space is there for it!!

BTW, lost my patience with NoScript, disabled for now!! Removal probably imminent!!!

1 Like

I keep a completely separate browser profile for doing Node-RED and development work. Means that I can target the addins accordingly.

I use Vivaldi for most personal stuff except for watching media, I use Brave for that because of its ability to fight against YouTube adverts. Vivaldi is rather more flexible over all and has some nice features for tab and page management that are missing currently from Brave. I use Brave's search engine though for everything.

1 Like

Yup, Profiles would be the way around the pool of mire that I have been known to get myself in to! :rofl:

It could be a timing issue, are you using the javascript which I DM'd you about earlier to load & run the gauge? - GitHub - Paul-Reed/UIbuilder-SPA-dashboard: A 3 page uibuilder dashboard

Oh, I've just loaded your test flow. You aren't loading your index.js file which I assume you want to.

It must be loading the index.html because it contains the links to the plotly, justgage & raphael libraries, and also the navigation to the SPA partial html files.
They all are working fine.
It's also showing in the browser console.

If you want to test the gauges, just use this flow to inject some dummy data for the gauges (2 formatted inject nodes) -

[{"id":"6404e70b7bb409fd","type":"inject","z":"1326aadbacf36704","name":"Dummy data","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"2","crontab":"","once":false,"onceDelay":0.1,"topic":"cpuUsage","payload":"$floor($random() * 100) + 1","payloadType":"jsonata","x":480,"y":1060,"wires":[["648d15378d7a2bec"]]},{"id":"e16b9cc1cb80ba1f","type":"inject","z":"1326aadbacf36704","name":"Dummy data","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"2","crontab":"","once":false,"onceDelay":0.1,"topic":"memoryUsage","payload":"$floor($random() * 100) + 1","payloadType":"jsonata","x":480,"y":1100,"wires":[["648d15378d7a2bec"]]},{"id":"648d15378d7a2bec","type":"junction","z":"1326aadbacf36704","x":600,"y":1080,"wires":[[]]}]

The index.html is not loading the index.js in the provided template html file. It is commented out. I assume that the live version is not the same as the template one.

The live version is identical.
In the git repo template, index.html shows the link to index.js on line 17, which isn't commented out??

Morning Paul, Julian,

Didn’t have a chance to check your git flow out properly. Never having loaded a flow like this before, I think I have got something wrong as I didn’t end up with what I expected - but it is probably me as I slotted it in between doing other stuff. My next opportunity to test this out will be later today.

Currently I have loaded Julian’s ‘Example Dashboard-style layout’ to take everything back to basics and I am using your example. I still apparently have a problem (space available, no gauge), but that again, will have to wait for later.

Thank you both for your help!

If you wanted to try my example, it’s quick & easy.

  • Drop a new 'UIbuilder' node into your editor, and name it's url to whatever name you wish, and deploy.

  • From the uibuilder node config, in 'Template Settings' select `Load an external template using Degit' from the dropdown, and enter Paul-Reed/UIbuilder-SPA-dashboard and deploy.

  • UIbuilder will then over-write the contents of the 'new' UIbuilder node with the git template.

  • Edit the index.html file to load the 3 libraries via the CDN’s instead of local installs that I use

  • The small flow below is 2 inject nodes, which can be used to inject ‘dummy data’ direct into your new uibuilder node, which will populate the gauges.

[{"id":"6404e70b7bb409fd","type":"inject","z":"1326aadbacf36704","name":"Dummy data","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"2","crontab":"","once":false,"onceDelay":0.1,"topic":"cpuUsage","payload":"$floor($random() * 100) + 1","payloadType":"jsonata","x":480,"y":1060,"wires":[["648d15378d7a2bec"]]},{"id":"e16b9cc1cb80ba1f","type":"inject","z":"1326aadbacf36704","name":"Dummy data","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"2","crontab":"","once":false,"onceDelay":0.1,"topic":"memoryUsage","payload":"$floor($random() * 100) + 1","payloadType":"jsonata","x":480,"y":1100,"wires":[["648d15378d7a2bec"]]},{"id":"648d15378d7a2bec","type":"junction","z":"1326aadbacf36704","x":600,"y":1080,"wires":[[]]}]
2 Likes

Thank you Paul, it works as intended.

The first time I tried, the UIBUILDER node stayed as default, but today when I tried it, Firefox gave me the warning regarding overwriting of the node. When I checked the node, it had changed to your version.

The important bit is - Justgage is working, so I now have something to work through!!

This is a great help and I can check against it so that I know where I have gone wrong.

2 Likes

That's good to hear!

You'll notice that I've also added PWA (progressive web app) to the the instance, which makes loading very fast, and can be installed as an app on devices.

If you make changes to the code, it's necessary to refresh the cache to see the changes.

2 Likes

I have a SPA site running, but I will delve into your code and, hopefully learn more! I just need to get the Basics in first.

It has been about 15 years since I built Interactive websites that used PHP and AJAX to integrate with our control system, but things have moved on a bit since then!

1 Like

I will be looking to do some automation for UIBUILDER PWA's in future releases. I've done some experiments but haven't quite decided on the details yet. There are too many variables possible to be able to do a generic pre-build.

1 Like

First glance of Paul’s git ‘Degit’ shows that my installed library calls, or at least the one for Raphael, was incorrect. I changed the path to match the Paul’s PWA versions and the meters now work. :partying_face:

Looking through the Justgage in UIBUILDER thread, from which my original paths were taken, they seem incorrect in that the Raphael call seems to go to the Raphael repository, whereas both paths on the working version go through the justgage path…

```
../uibuilder/vendor/justgage/raphael.min.js"></script> ../uibuilder/vendor/justgage/dist/justgage.min.js
```

Thank you both for your help!

Now that I have worked out how SPA works (without the finer details yet!), I now have a PWA flow that I can look through to understand. Although maybe I should leave that until I have a full working Dashboard for the Motorhome before changing direction!

Thanks, I'll see if I can update it.

Urm, yes, I would leave that for now. While it is a nice to have, it isn't necessary and can be quite complex to do well. I don't pretend to know all the complexities as yet. Nothing wrong with having a play of course, if you have time.

A UIBUILDER solution is already quite a bit faster and lighter than a Dashboard one.

1 Like