Dashboard 2 Beta development

...Continued from Dashboard 2.0 Pre-Alpha Available

There is also a dashboard-2 tag which you can subscribe to, by going to Topics tagged dashboard-2 and then selecting how you wish to be alerted about new posts.

watching

1 Like

For sure it is whole new world in terms of layouting. Nothing (specially custom made stuff) can be directly brought over from D1. And even trying to use ready made components takes to understand that world and maybe to learn new tricks.
I think it doesn't take too long when people start to bring over the stuff from D1 and no matter the strategy they choose, there will be some challenges. So without any jokes - it will be somehow topic at it's own for some time.

1 Like

Dashboard 2 pre-alpha has been available for 4 months.

Is there a road map for actual alpha test stage, beta, production release?

Will it be the official dashboard by the time Node-red 5 comes along?

(Schedules have slipped, but the documentation suggests NR 4 goes into maintenance next May!)

Edit:
Maybe it has already been promoted? https://flows.nodered.org/node/@flowfuse/node-red-dashboard, though it's otherwise a blank page (might be a problem with my internet) says node-red-dashboard 0.10.1, no mention of pre-alpha.

I'm just hoping for some clarity so I can decide at what point to start working with the new dashboard.

We announced Beta about 4-5 weeks ago, but the thread title never changed :slight_smile:

It was originally announced as part of https://www.youtube.com/watch?v=7GTHYzRpQ9M&t=1801s

Looks like I never actually published the accompanying article to go with it though! My fault entirely.

1 Like

In the dropdown it says that it allows dynamic options, using msg.options. However, I cannot get it to work, what am I doing wrong? (All I get is 'no options available').

msg.options = [ { value: "1", label: "Option 1" }, { value: "2", label: "Option 2" }, { value: "3", label: "Option 3" } ]

Also when settings options in the node, number, string and boolean types can be selected but the msg.options note specifies string, will this be updated later?

It is working for me, using version 0.10.1

[{"id":"0c31ad9acfb32a07","type":"inject","z":"997da33a0beedade","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":200,"y":940,"wires":[["2dcb529f7d48c393"]]},{"id":"4aef8e568f749630","type":"ui-dropdown","z":"997da33a0beedade","group":"7545fdb22e4a388c","name":"","label":"Select Option:","tooltip":"","order":0,"width":0,"height":0,"passthru":false,"multiple":false,"options":[{"label":"","value":"","type":"str"}],"payload":"","topic":"topic","topicType":"msg","className":"","x":560,"y":940,"wires":[["2dba8a33e051d925"]]},{"id":"2dcb529f7d48c393","type":"function","z":"997da33a0beedade","name":"Set msg.options","func":"msg.options = [ { value: \"1\", label: \"Option 1\" }, { value: \"2\", label: \"Option 2\" }, { value: \"3\", label: \"Option 3\" } ]\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":380,"y":940,"wires":[["4aef8e568f749630"]]},{"id":"2dba8a33e051d925","type":"debug","z":"997da33a0beedade","name":"debug 2465","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":750,"y":940,"wires":[]},{"id":"7545fdb22e4a388c","type":"ui-group","name":"D2 tests","page":"d7fcdd33578e5d34","width":"6","height":"1","order":-1,"className":""},{"id":"d7fcdd33578e5d34","type":"ui-page","name":"d2","ui":"96e4ba9f6c4dd158","path":"/d2","layout":"flex","theme":"48d09dabddfb21da","order":-1,"className":""},{"id":"96e4ba9f6c4dd158","type":"ui-base","name":"D2","path":"/dashboard"},{"id":"48d09dabddfb21da","type":"ui-theme","name":"Theme 1","colors":{"surface":"#ffffff","primary":"#0094ce","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"}}]

That would probably account for it then, I am on the latest release in my Palette which is v0.7.0. I will wait for the update to be published

@Buckskin - Are you using @flowfuse/node-red-dashboard (note - @flowfuse, not @flowforge)?

@flowforge/node-red-dashboard is now depreciated (because of the name change from flowforge to flowfuse)

I'm assured that the best way to update to the newer package is;

  1. Stop node-Red
  2. Uninstall @flowforge/node-red-dashboard via cmd prompt - npm uninstall @flowforge/node-red-dashboard in the usual manner.
  3. Install @flowfuse/node-red-dashboard via cmd prompt - npm install @flowfuse/node-red-dashboard.
  4. Restart node-Red

Doing it that way, should replace the nodes, and not break your flows.

4 Likes

The npm commands have to be run from your .node-red folder (or wherever your flows file is if not .node-red).

In D1 one can use the Font Awsome icons in, for example, a Text node. Is some similar functionality already available in D2?

In a template you can use the mdi icons OOTB

e.g.

<div class="d-flex justify-space-around">
    MDI icons
    <v-icon icon="mdi-information"></v-icon>
    <v-icon icon="mdi-home"></v-icon>
    <v-icon icon="mdi-minecraft"></v-icon>
    <v-icon icon="mdi-crane"></v-icon>
    <v-icon icon="mdi-headphones"></v-icon>
    <v-icon icon="mdi-battery-40-bluetooth"></v-icon>
    <v-icon icon="mdi-gavel"></v-icon>
</div>

image

The v-icon component provides a large set of glyphs to provide context to various aspects of your application. For a list of all available icons, visit the official Material Design Icons page. To use any of these icons simply use the mdi- prefix followed by the icon name.

REF: Icon component — Vuetify

Yes, but what about

I think that comes under this issue Paul

I can be done (right now) with (hacky) script magic though.

<script>

export default {
    mounted() {
        this.applyIcons()
    },
    methods: {
        applyIcons: function() {

            // Get all elements with the class '.mdi'
            const mdiElements = document.querySelectorAll('div[class*="mdi-icon-"] > div > label')
            
            // Iterate through each element
            mdiElements.forEach((element) => {
                // Create a new div element
                const wrapperDiv = document.createElement('div')
                
                // Extract the 'mdi-xxx' class from the original element's class list
                const foundMdiClass = Array.from(element.parentNode.parentNode.classList).find((cls) => cls.startsWith('mdi-icon-'))
                const mdiClass = foundMdiClass.replace('mdi-icon-', 'mdi-')
                
                // Create the new icon element with the extracted class
                const newIcon = document.createElement('i')
                newIcon.className = `mdi ${mdiClass} v-icon mr-2 notranslate v-theme--nrdb v-icon--size-default`
                newIcon.setAttribute('aria-hidden', 'true')
                
                // Remove the 'mdi-xxx' class from the parent element
                element.parentNode.parentNode.classList.remove(foundMdiClass)
                
                // Clone the original element
                const clonedElement = element.cloneNode(true);
                
                // Append the new icon and the CLONED original element to the wrapper div
                wrapperDiv.appendChild(newIcon);
                wrapperDiv.appendChild(clonedElement);
                
                // Insert the new wrapper div before the original element
                element.parentNode.insertBefore(wrapperDiv, element);
                
                // Remove the original element
                element.parentNode.removeChild(element);
            })
        }
    }
}

</script>

NOTE This ^ (accessing the DOM directly) ^ is NOT Vue best practice but until there is a means of formatting a label, this might be suitable for some in desperate need.

Demo:

[{"id":"13232e47a6495e4b","type":"ui-text","z":"1ca1817447127d22","group":"34a8586ca5a5880e","order":0,"width":0,"height":0,"name":"","label":"My Label with crane icon","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"Gill Sans,Geneva,sans-serif","fontSize":16,"color":"#000000","className":"mdi-icon-crane","x":2550,"y":100,"wires":[]},{"id":"4e4e5ebcf0b853c5","type":"ui-template","z":"1ca1817447127d22","group":"34a8586ca5a5880e","dashboard":"ID-BASE-1","page":"ID-PAGE-1","name":"Apply icons to Labels with class .mdi-icon-xxxx","order":0,"width":0,"height":0,"head":"","format":"<script>\n\nexport default {\n    mounted() {\n        this.applyIcons()\n    },\n    methods: {\n        applyIcons: function() {\n\n            // Get all elements with the class '.mdi'\n            const mdiElements = document.querySelectorAll('div[class*=\"mdi-icon-\"] > div > label')\n            \n            // Iterate through each element\n            mdiElements.forEach((element) => {\n                // Create a new div element\n                const wrapperDiv = document.createElement('div')\n                \n                // Extract the 'mdi-xxx' class from the original element's class list\n                const foundMdiClass = Array.from(element.parentNode.parentNode.classList).find((cls) => cls.startsWith('mdi-icon-'))\n                const mdiClass = foundMdiClass.replace('mdi-icon-', 'mdi-')\n                \n                // Create the new icon element with the extracted class\n                const newIcon = document.createElement('i')\n                newIcon.className = `mdi ${mdiClass} v-icon mr-2 notranslate v-theme--nrdb v-icon--size-default`\n                newIcon.setAttribute('aria-hidden', 'true')\n                \n                // Remove the 'mdi-xxx' class from the parent element\n                element.parentNode.parentNode.classList.remove(foundMdiClass)\n                \n                // Clone the original element\n                const clonedElement = element.cloneNode(true);\n                \n                // Append the new icon and the CLONED original element to the wrapper div\n                wrapperDiv.appendChild(newIcon);\n                wrapperDiv.appendChild(clonedElement);\n                \n                // Insert the new wrapper div before the original element\n                element.parentNode.insertBefore(wrapperDiv, element);\n                \n                // Remove the original element\n                element.parentNode.removeChild(element);\n            })\n        }\n    }\n}\n\n</script>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"d-none","x":2620,"y":60,"wires":[[]]},{"id":"05306283049c2598","type":"ui-text","z":"1ca1817447127d22","group":"34a8586ca5a5880e","order":0,"width":0,"height":0,"name":"","label":"My Label with minecraft icon","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"Gill Sans,Geneva,sans-serif","fontSize":16,"color":"#000000","className":"mdi-icon-minecraft","x":2560,"y":140,"wires":[]},{"id":"c97b993296bcdf4c","type":"ui-text","z":"1ca1817447127d22","group":"34a8586ca5a5880e","order":0,"width":0,"height":0,"name":"no text - icon only","label":"","format":"{{msg.payload}}","layout":"row-spread","style":false,"font":"Gill Sans,Geneva,sans-serif","fontSize":16,"color":"#000000","className":"mdi-icon-battery-40-bluetooth","x":2530,"y":180,"wires":[]},{"id":"4a0fdf45a8ef9eaa","type":"inject","z":"1ca1817447127d22","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":2110,"y":140,"wires":[["a7035cd1a3125e8c","1d99b5dba1de131f","e001508b5d471e4e"]]},{"id":"a7035cd1a3125e8c","type":"change","z":"1ca1817447127d22","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"\t\t\t\t(\t    $minimum := 50;\t    $maximum := 90;\t    $round(($random() * ($maximum-$minimum)) + $minimum, 0)\t)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":2320,"y":100,"wires":[["13232e47a6495e4b"]]},{"id":"1d99b5dba1de131f","type":"change","z":"1ca1817447127d22","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"\t\t\t\t(\t    $minimum := 50;\t    $maximum := 90;\t    $round(($random() * ($maximum-$minimum)) + $minimum, 0)\t)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":2320,"y":140,"wires":[["05306283049c2598"]]},{"id":"e001508b5d471e4e","type":"change","z":"1ca1817447127d22","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"\t\t\t\t(\t    $minimum := 50;\t    $maximum := 90;\t    $round(($random() * ($maximum-$minimum)) + $minimum, 0)\t)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":2320,"y":180,"wires":[["c97b993296bcdf4c"]]},{"id":"34a8586ca5a5880e","type":"ui-group","name":"Group 4-1","page":"03b64402237b9653","width":"6","height":"1","order":-1,"showTitle":false,"className":""},{"id":"ID-BASE-1","type":"ui-base","name":"Dashboard","path":"/dashboard"},{"id":"ID-PAGE-1","type":"ui-page","name":"Page 1","ui":"ID-BASE-1","path":"/page1","layout":"notebook","theme":"f9b6670b127dc219"},{"id":"03b64402237b9653","type":"ui-page","name":"Page 4","ui":"ID-BASE-1","path":"/p4","layout":"grid","theme":"4bff2b59c4c518e1","order":-1,"className":""},{"id":"f9b6670b127dc219","type":"ui-theme","name":"FlowForge Theme","colors":{"surface":"#152a47","primary":"#005aff","bgPage":"#ffffff","groupBg":"#ffffff","groupOutline":"#cc3e3e"}},{"id":"4bff2b59c4c518e1","type":"ui-theme","name":"Another Theme","colors":{"surface":"#ffffff","primary":"#0094ce","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"}}]

chrome_cKBvsZvwrv

That appears to be in relation to labels rather than an icon instead of the payload text, but in practice perhaps either would do, as one could manually build the label to be both label and icon.

I think maybe a better temporary solution would be to use a template.

I see I can specify a limited range of colours in v-icon, can I pass that in to the template in a message? I effectively want to have an LED like output showing on/off.

First, here are the colour names available to use as classes: Material color palette — Vuetify

You can achieve this:
chrome_24JRqS54Fp

with this template:

<template>
    <v-icon :color="ledColor" :icon="iconName"></v-icon>
</template>

<script>
    export default {
        data() {
            return {
                ledColor: 'grey',
                iconName: 'mdi-led-off'
            }
        },
        mounted() {
            const component = this
            this.$socket.on('msg-input:' + this.id, function(msg) {
                console.log(msg)
                if (msg.topic === 'on') {
                    component.iconName = 'mdi-led-on'
                    component.ledColor = msg.payload
                } else if (msg.topic === 'off') {
                    component.iconName = 'mdi-led-off'
                    component.ledColor = 'grey'
                }
            })
        }
    }
</script>

Flow:

[{"id":"fb5c574dc9088451","type":"ui-template","z":"1ca1817447127d22","group":"34a8586ca5a5880e","dashboard":"ID-BASE-1","page":"ID-PAGE-1","name":"","order":0,"width":0,"height":0,"head":"","format":"<template>\n    <v-icon :color=\"ledColor\" :icon=\"iconName\"></v-icon>\n</template>\n\n<script>\n    export default {\n        data() {\n            return {\n                ledColor: 'grey',\n                iconName: 'mdi-led-off'\n            }\n        },\n        mounted() {\n            const component = this\n            this.$socket.on('msg-input:' + this.id, function(msg) {\n                console.log(msg)\n                if (msg.topic === 'on') {\n                    component.iconName = 'mdi-led-on'\n                    component.ledColor = msg.payload\n                } else if (msg.topic === 'off') {\n                    component.iconName = 'mdi-led-off'\n                    component.ledColor = 'grey'\n                }\n            })\n        }\n    }\n</script>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":2740,"y":320,"wires":[[]]},{"id":"d813d7d4882adf78","type":"function","z":"1ca1817447127d22","name":"generateRandomColorName","func":"const colorNames = [\"red\", \"pink\", \"yellow\", \"green\", \"purple\", \"blue\", \"cyan\", \"lime\", \"orange\"];\n\nfunction generateRandomColorName() {\n  const randomIndex = Math.floor(Math.random() * colorNames.length);\n  return colorNames[randomIndex];\n}\n\nmsg.payload = generateRandomColorName();\nreturn msg;","outputs":1,"timeout":30,"noerr":0,"initialize":"","finalize":"","libs":[],"x":2680,"y":360,"wires":[["fb5c574dc9088451"]]},{"id":"8330c685d9761104","type":"inject","z":"1ca1817447127d22","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"on","payload":"","payloadType":"str","x":2490,"y":360,"wires":[["d813d7d4882adf78"]]},{"id":"1ebc687c8ca026be","type":"inject","z":"1ca1817447127d22","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"off","payload":"","payloadType":"str","x":2490,"y":320,"wires":[["fb5c574dc9088451"]]},{"id":"34a8586ca5a5880e","type":"ui-group","name":"Group 4-1","page":"03b64402237b9653","width":"6","height":"1","order":-1,"showTitle":false,"className":""},{"id":"ID-BASE-1","type":"ui-base","name":"Dashboard","path":"/dashboard"},{"id":"ID-PAGE-1","type":"ui-page","name":"Page 1","ui":"ID-BASE-1","path":"/page1","layout":"notebook","theme":"f9b6670b127dc219"},{"id":"03b64402237b9653","type":"ui-page","name":"Page 4","ui":"ID-BASE-1","path":"/p4","layout":"grid","theme":"4bff2b59c4c518e1","order":-1,"className":""},{"id":"f9b6670b127dc219","type":"ui-theme","name":"FlowForge Theme","colors":{"surface":"#152a47","primary":"#005aff","bgPage":"#ffffff","groupBg":"#ffffff","groupOutline":"#cc3e3e"}},{"id":"4bff2b59c4c518e1","type":"ui-theme","name":"Another Theme","colors":{"surface":"#ffffff","primary":"#0094ce","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"}}]

Magic! Thanks. That should keep me going until D2 catches up.

My flow is littered with workarounds for:
#352 Disabled button re-enables on browser refresh
#342 Form widget should send Number type fields as Numbers not strings
#363 No inputs on form
#210 Not able to select Show state of input in ui-switch
#311 ui-switch malfunctions if on/off payloads are strings
But that is what you get when going with bleeding edge developments. It is inevitable.

Actually, I went too far, too soon Ted!

It is as simple as this:

The template

<v-icon 
    :color="msg.payload?.state === 'on' ? (msg.payload?.color || 'green') : 'grey'"
    :icon="msg.payload?.state === 'on' ? 'mdi-led-on' : 'mdi-led-off'">
</v-icon>

Demo flow

[{"id":"fb5c574dc9088451","type":"ui-template","z":"1ca1817447127d22","group":"34a8586ca5a5880e","dashboard":"ID-BASE-1","page":"ID-PAGE-1","name":"","order":0,"width":0,"height":0,"head":"","format":"\n<v-icon \n    :color=\"msg.payload.state === 'on' ? (msg.payload?.color || 'green') : msg.payload.color\"\n    :icon=\"msg.payload.state === 'on' ? 'mdi-led-on' : 'mdi-led-off'\">\n</v-icon>\n","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":2780,"y":320,"wires":[[]]},{"id":"d813d7d4882adf78","type":"function","z":"1ca1817447127d22","name":"generateRandomColorName","func":"const colorNames = [\"red\", \"pink\", \"yellow\", \"green\", \"purple\", \"blue\", \"cyan\", \"lime\", \"orange\"];\n\nfunction generateRandomColorName() {\n  const randomIndex = Math.floor(Math.random() * colorNames.length);\n  return colorNames[randomIndex];\n}\n\nmsg.payload = {\n  state: 'on',\n  color: generateRandomColorName()\n}\nreturn msg;","outputs":1,"timeout":30,"noerr":0,"initialize":"","finalize":"","libs":[],"x":2720,"y":360,"wires":[["fb5c574dc9088451"]]},{"id":"8330c685d9761104","type":"inject","z":"1ca1817447127d22","name":"on","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"str","x":2490,"y":360,"wires":[["d813d7d4882adf78"]]},{"id":"1ebc687c8ca026be","type":"inject","z":"1ca1817447127d22","name":"off","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{ \"state\": \"off\" }","payloadType":"json","x":2490,"y":320,"wires":[["fb5c574dc9088451"]]},{"id":"34a8586ca5a5880e","type":"ui-group","name":"Group 4-1","page":"03b64402237b9653","width":"6","height":"1","order":-1,"showTitle":false,"className":""},{"id":"ID-BASE-1","type":"ui-base","name":"Dashboard","path":"/dashboard"},{"id":"ID-PAGE-1","type":"ui-page","name":"Page 1","ui":"ID-BASE-1","path":"/page1","layout":"notebook","theme":"f9b6670b127dc219"},{"id":"03b64402237b9653","type":"ui-page","name":"Page 4","ui":"ID-BASE-1","path":"/p4","layout":"grid","theme":"4bff2b59c4c518e1","order":-1,"className":""},{"id":"f9b6670b127dc219","type":"ui-theme","name":"FlowForge Theme","colors":{"surface":"#152a47","primary":"#005aff","bgPage":"#ffffff","groupBg":"#ffffff","groupOutline":"#cc3e3e"}},{"id":"4bff2b59c4c518e1","type":"ui-theme","name":"Another Theme","colors":{"surface":"#ffffff","primary":"#0094ce","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"}}]

Just send:

{"state":"on","color":"green-lighten-2"}

or

{"state":"off"}

Thats it

1 Like

I really appreciate the patience Colin - working as fast as I can.

2 Likes

Even more magic. Thanks.

Yes, I am sure that is true, I think it is remarkable how much has been accomplished already.