Dashboard 2.0 is now Generally Available

Any news for a Gauge widget?

It's currently under development, see New Widget: UI Gauge (in Draft) by joepavitt · Pull Request #530 · FlowFuse/node-red-dashboard · GitHub

2 Likes

Some building blocks to play with:

Template:

<template>
    <div class="level">
        <div v-for="(color, index) in colors" :key="index" class="led" :ref="'dot-' + index +'-'+this.id"></div>
    </div>
</template>

<script>
export default {
    data(){
        return {
            //define me here                   
            min:0,
            max:100,
            dark:0.4,            
            colors:["#4ed34e",
                    "#4ed34e",
                    "#4ed34e",
                    "#4ed34e",
                    "#4ed34e",
                    "#4ed34e",
                    "#4ed34e",
                    "#4ed34e",
                    "#4ed34e",
                    "#4ed34e",
                    "#ffcf00",
                    "#ffcf00",
                    "#ffcf00",
                    "#ffcf00",
                    "red",
                    "red",
                    "red"],

            //no need to change those
            value:0,
            inited:false
        }
    },

   
    methods: {        
        getElement: function(name){
            return this.$refs[name][0]
        },

        full: function(){
            return Math.floor(this.colors.length*this.percentage/100)
        },
        half: function (){
            let p = this.colors.length*this.percentage/100;
            p -= this.full()
            p *= .5
            p += this.dark;
            return p;
        },

        lit: function(){
            if(this.inited == false){
                return
            }
            this.colors.forEach((e,i) => {
                let dot = this.getElement("dot-"+i+"-"+this.id);
                if(!dot){
                    console.log("no dots found")
                    return
                }                
                if(i<this.full()){
                    dot.style.filter= "brightness(1.1)";
                }
                else if(i==this.full()){                   
                    dot.style.filter = "brightness("+this.half()+")";
                }
                else{
                    dot.style.filter= "brightness("+this.dark+")";
                }
            })
        }
    },
    watch: {
        msg: function(){    
            if(this.msg.payload != undefined){           
                this.value = this.msg.payload                
                this.lit()
            }
        }
    },
    computed: {
        formattedValue: function () {
            return this.value.toFixed(2)
        },
        percentage: function(){
            return Math.floor(((this.value - this.min) / (this.max - this.min)) * 100);
        }
    },
    mounted(){
        this.colors.forEach((c,i) => {
                let dot = this.getElement("dot-"+i+"-"+this.id);
                if(!dot){
                    console.log("no dots found")
                    return
                }
                dot.style.backgroundColor = c
            }
        )       
        this.inited = true;
    },
        
}
</script>

CSS:

.level{
    display: flex;
    gap:2px;
}    
.led {
    background: #ffffff;
    width: 100%;
    height: 100%;
    border-radius: 4px;
    box-shadow: inset 0px 0px 20px 0px #00000099, 0px 0px 3px 0px #00000099;
    filter: brightness(0.4);
}

6 Likes

Nice work my friend.. I already got it here! =]

I've work a few with dashboard 2 and ui-template. I'm new with Vue.js and vuetify.

So, i've tried to make a flow to start with ui-template in 5 (simple) steps.

Next step for me, trying to make some examples with this tutorial : Tutorial | Vue.js
and using ui-template in a Vue.js way...

[
    {
        "id": "359f4155f8c47688",
        "type": "tab",
        "label": "The easy way",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "161e548163a6e3f7",
        "type": "group",
        "z": "359f4155f8c47688",
        "name": "0 - start default template",
        "style": {
            "label": true
        },
        "nodes": [
            "ac6602aedb2bc00e",
            "95a84f14b2851bca",
            "fa75fdf0120a9f8d"
        ],
        "x": 34,
        "y": 19,
        "w": 452,
        "h": 122
    },
    {
        "id": "a124bca636cf7e1d",
        "type": "group",
        "z": "359f4155f8c47688",
        "name": "1 - Most basic Vue template",
        "style": {
            "label": true
        },
        "nodes": [
            "0d26e5311e23b0da"
        ],
        "x": 34,
        "y": 159,
        "w": 181,
        "h": 82
    },
    {
        "id": "847acde69067f419",
        "type": "group",
        "z": "359f4155f8c47688",
        "name": "2 - Adding your first Vuetify component (Text Field)",
        "style": {
            "label": true
        },
        "nodes": [
            "3643280942742de0",
            "73e263314550aaaf"
        ],
        "x": 34,
        "y": 259,
        "w": 322,
        "h": 122
    },
    {
        "id": "ed383e6317517380",
        "type": "group",
        "z": "359f4155f8c47688",
        "name": "3 - Change label value from Node-RED",
        "style": {
            "label": true
        },
        "nodes": [
            "dcdf3507637a958a",
            "aa4058b90cc301f5",
            "8a3d68de5a549211"
        ],
        "x": 34,
        "y": 399,
        "w": 432,
        "h": 122
    },
    {
        "id": "1d47ea8478ef9d1c",
        "type": "group",
        "z": "359f4155f8c47688",
        "name": "4 - Send text to Nodered",
        "style": {
            "label": true
        },
        "nodes": [
            "9db6bd112414a6f7",
            "fa9dc8a93420fa72",
            "fe4ecec7a56b7a3e"
        ],
        "x": 34,
        "y": 539,
        "w": 462,
        "h": 122
    },
    {
        "id": "531f51f1625c11e2",
        "type": "group",
        "z": "359f4155f8c47688",
        "name": "5 - Send value to Nodered",
        "style": {
            "label": true
        },
        "nodes": [
            "418ebd279d8609fe",
            "7a0c7f9c75e885bb",
            "dbfd55ba2afcf6a0"
        ],
        "x": 34,
        "y": 679,
        "w": 462,
        "h": 122
    },
    {
        "id": "ac6602aedb2bc00e",
        "type": "ui-template",
        "z": "359f4155f8c47688",
        "g": "161e548163a6e3f7",
        "group": "ebee2ab416276aee",
        "page": "",
        "ui": "",
        "name": "",
        "order": 0,
        "width": 0,
        "height": 0,
        "head": "",
        "format": "<template>\n    0 - start default template\n    <div>\n        <h2>Counter</h2>\n        <p>Current Count: {{ count }}</p>\n        <p class=\"my-class\">Formatted Count: {{ formattedCount }}</p>\n        <v-btn @click=\"increase()\">Increment</v-btn>\n    </div>\n</template>\n\n<script>\n    export default {\n        data() {\n            // define variables available component-wide\n            // (in <template> and component functions)\n            return {\n                count: 0\n            }\n        },\n        watch: {\n            // watch for any changes of \"count\"\n            count: function () {\n                if (this.count % 5 === 0) {\n                    this.send({payload: 'Multiple of 5'})\n                }\n            }\n        },\n        computed: {\n            // automatically compute this variable\n            // whenever VueJS deems appropriate\n            formattedCount: function () {\n                return this.count + 'Apples'\n            }\n        },\n        methods: {\n            // expose a method to our <template> and Vue Application\n            increase: function () {\n                this.count++\n            }\n        },\n        mounted() {\n            // code here when the component is first loaded\n        },\n        unmounted() {\n            // code here when the component is removed from the Dashboard\n            // i.e. when the user navigates away from the page\n        }\n    }\n</script>\n<style>\n    /* define any styles here - supports raw CSS */\n    .my-class {\n        color: red;\n    }\n</style>",
        "storeOutMessages": true,
        "passthru": true,
        "resendOnRefresh": true,
        "templateScope": "local",
        "className": "",
        "x": 120,
        "y": 100,
        "wires": [
            [
                "95a84f14b2851bca"
            ]
        ]
    },
    {
        "id": "95a84f14b2851bca",
        "type": "debug",
        "z": "359f4155f8c47688",
        "g": "161e548163a6e3f7",
        "name": "0 - start default template",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 330,
        "y": 100,
        "wires": []
    },
    {
        "id": "fa75fdf0120a9f8d",
        "type": "comment",
        "z": "359f4155f8c47688",
        "g": "161e548163a6e3f7",
        "name": "click me for help !",
        "info": "## Setup\n1 - Edit this template node\n2 - Edit Group\n3 - Edit Page\n4 - Edit UI\n5 - Save UI \n6 - Edit Theme\n7 - Add Theme\n8 - Save all and deploy\n=> You can see widget organisation on the dashboard 2.0 tab\n\n## Running\n1 - Click on Open Dashboard button (Or go to /dashboard url, eg : http://localhost:1880/dashboard/ )\nIn Home assistant Addon, go to http://ip_home_assistant:1880/endpoint/dashboard/\n2 - click 5 time on increment to see the debug is working",
        "x": 140,
        "y": 60,
        "wires": []
    },
    {
        "id": "0d26e5311e23b0da",
        "type": "ui-template",
        "z": "359f4155f8c47688",
        "g": "a124bca636cf7e1d",
        "group": "ebee2ab416276aee",
        "page": "",
        "ui": "",
        "name": "",
        "order": 0,
        "width": 0,
        "height": 0,
        "head": "",
        "format": "<template>\n    1 - Most basic Vue template <br>\n    \n    This is the most basic Vue template !\n</template>\n\n<script>\n    export default {\n    }\n</script>",
        "storeOutMessages": true,
        "passthru": true,
        "resendOnRefresh": true,
        "templateScope": "local",
        "className": "",
        "x": 120,
        "y": 200,
        "wires": [
            []
        ]
    },
    {
        "id": "3643280942742de0",
        "type": "ui-template",
        "z": "359f4155f8c47688",
        "g": "847acde69067f419",
        "group": "ebee2ab416276aee",
        "page": "",
        "ui": "",
        "name": "",
        "order": 0,
        "width": 0,
        "height": 0,
        "head": "",
        "format": "<template>\n    2 - Adding your first Vuetify component (Text Field)\n    <v-text-field label=\"Label\"></v-text-field>\n</template>\n\n<script>\n    export default {\n        data() {\n            // define variables available component-wide\n            // (in <template> and component functions)\n            return {\n            }\n        }\n    }\n</script>",
        "storeOutMessages": true,
        "passthru": true,
        "resendOnRefresh": true,
        "templateScope": "local",
        "className": "",
        "x": 120,
        "y": 340,
        "wires": [
            []
        ]
    },
    {
        "id": "73e263314550aaaf",
        "type": "comment",
        "z": "359f4155f8c47688",
        "g": "847acde69067f419",
        "name": "click me for help !",
        "info": "Dashboard 2.0 is using Vue.js (https://vuejs.org/) \nand Vuetify (https://vuetifyjs.com) is working out of the box\n\nIt's easy to add any Vuetify component (https://vuetifyjs.com/en/components/all/#containment)\nLet's start with a Label !",
        "x": 140,
        "y": 300,
        "wires": []
    },
    {
        "id": "dcdf3507637a958a",
        "type": "ui-template",
        "z": "359f4155f8c47688",
        "g": "ed383e6317517380",
        "group": "ebee2ab416276aee",
        "page": "",
        "ui": "",
        "name": "",
        "order": 0,
        "width": 0,
        "height": 0,
        "head": "",
        "format": "<template>\n    3 - Change label value from Node-RED <br>\n    Get msg.payload value from Nodered\n    <v-text-field label=\"\">{{this.msg.payload}}</v-text-field>\n</template>\n\n<script>\n    export default {\n        data() {\n            // define variables available component-wide\n            // (in <template> and component functions)\n            return {\n            }\n        }\n    }\n</script>",
        "storeOutMessages": true,
        "passthru": true,
        "resendOnRefresh": true,
        "templateScope": "local",
        "className": "",
        "x": 380,
        "y": 480,
        "wires": [
            []
        ]
    },
    {
        "id": "aa4058b90cc301f5",
        "type": "comment",
        "z": "359f4155f8c47688",
        "g": "ed383e6317517380",
        "name": "click me for help !",
        "info": "From ui-template, you have access to buil-in variables.\nthis.msg - The last message received by the ui-template node.\n(see : https://dashboard.flowfuse.com/nodes/widgets/ui-template.html#built-in-variables)\n\nLet's get the payload from this inject node !",
        "x": 140,
        "y": 440,
        "wires": []
    },
    {
        "id": "8a3d68de5a549211",
        "type": "inject",
        "z": "359f4155f8c47688",
        "g": "ed383e6317517380",
        "name": "",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "Hello from Node-RED !",
        "payloadType": "str",
        "x": 180,
        "y": 480,
        "wires": [
            [
                "dcdf3507637a958a"
            ]
        ]
    },
    {
        "id": "9db6bd112414a6f7",
        "type": "ui-template",
        "z": "359f4155f8c47688",
        "g": "1d47ea8478ef9d1c",
        "group": "ebee2ab416276aee",
        "page": "",
        "ui": "",
        "name": "",
        "order": 0,
        "width": 0,
        "height": 0,
        "head": "",
        "format": "<template>\n  <div>\n    <v-text-field \n      label=\"4 - Send text to Nodered\"\n      @input=\"send({payload: 'text changed'})\"\n    ></v-text-field>\n  </div>\n</template>    \n\n<script>\n    export default {\n        data() {\n            // define variables available component-wide\n            // (in <template> and component functions)\n            return {\n            }\n        }\n    }\n</script>",
        "storeOutMessages": true,
        "passthru": true,
        "resendOnRefresh": true,
        "templateScope": "local",
        "className": "",
        "x": 120,
        "y": 620,
        "wires": [
            [
                "fe4ecec7a56b7a3e"
            ]
        ]
    },
    {
        "id": "fa9dc8a93420fa72",
        "type": "comment",
        "z": "359f4155f8c47688",
        "g": "1d47ea8478ef9d1c",
        "name": "click me for help !",
        "info": "https://dashboard.flowfuse.com/nodes/widgets/ui-template.html#built-in-functions\n\nfunction this.send - Send a message to the Node-RED flow.\n\nLet's send a message to nodered from ui-template\n",
        "x": 140,
        "y": 580,
        "wires": []
    },
    {
        "id": "fe4ecec7a56b7a3e",
        "type": "debug",
        "z": "359f4155f8c47688",
        "g": "1d47ea8478ef9d1c",
        "name": "4 - Send text to Nodered",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 340,
        "y": 620,
        "wires": []
    },
    {
        "id": "418ebd279d8609fe",
        "type": "ui-template",
        "z": "359f4155f8c47688",
        "g": "531f51f1625c11e2",
        "group": "ebee2ab416276aee",
        "page": "",
        "ui": "",
        "name": "",
        "order": 0,
        "width": 0,
        "height": 0,
        "head": "",
        "format": "<template>\n  <div>\n    <v-text-field \n      label=\"5 - Send value to Nodered\"\n      @input=\"send({payload: $event.target.value})\"\n    ></v-text-field>\n  </div>\n</template>    \n\n<script>\n    export default {\n        data() {\n            // define variables available component-wide\n            // (in <template> and component functions)\n            return {\n            }\n        }\n    }\n</script>",
        "storeOutMessages": true,
        "passthru": true,
        "resendOnRefresh": true,
        "templateScope": "local",
        "className": "",
        "x": 120,
        "y": 760,
        "wires": [
            [
                "dbfd55ba2afcf6a0"
            ]
        ]
    },
    {
        "id": "7a0c7f9c75e885bb",
        "type": "comment",
        "z": "359f4155f8c47688",
        "g": "531f51f1625c11e2",
        "name": "click me for help !",
        "info": "You can get input value with $event.target.value\nBut now, it's time to discover Vue.js !",
        "x": 140,
        "y": 720,
        "wires": []
    },
    {
        "id": "dbfd55ba2afcf6a0",
        "type": "debug",
        "z": "359f4155f8c47688",
        "g": "531f51f1625c11e2",
        "name": "5 - Send value to Nodered",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 340,
        "y": 760,
        "wires": []
    },
    {
        "id": "ebee2ab416276aee",
        "type": "ui-group",
        "name": "My Group ",
        "page": "027e47745a6c6bee",
        "width": "12",
        "height": "1",
        "order": -1,
        "showTitle": true,
        "className": ""
    },
    {
        "id": "027e47745a6c6bee",
        "type": "ui-page",
        "name": "Page test",
        "ui": "5818fe6ad310e494",
        "path": "/",
        "icon": "home",
        "layout": "grid",
        "theme": "3bedd6defc6b1907",
        "order": -1,
        "className": ""
    },
    {
        "id": "5818fe6ad310e494",
        "type": "ui-base",
        "name": "UI Name",
        "path": "/dashboard",
        "includeClientData": true,
        "acceptsClientConfig": [
            "ui-notification",
            "ui-control"
        ]
    },
    {
        "id": "3bedd6defc6b1907",
        "type": "ui-theme",
        "name": "Theme Name",
        "colors": {
            "surface": "#ffffff",
            "primary": "#0094ce",
            "bgPage": "#eeeeee",
            "groupBg": "#ffffff",
            "groupOutline": "#cccccc"
        }
    }
]

How to start measuring from the bottom up? @hotNipi

image

<template>    
    <v-card style="display: flex; flex-direction: column; margin: auto; width: 75px; height: 150px; background-color: #4F4F4F; border: 1px solid #000000; border-radius: 18px;">  
        
        
               
        <div v-for="(color, index) in colors" :key="index" class="led" :ref="'dot-' + index +'-'+this.id"></div>      
        <span style="color: black; margin: auto; font-size: 11px;"> Valor {{this.value}}%</span>
        
    </v-card>
</template>

<script>
    export default {
    data(){
        return {
            //define me here                   
            min:0,
            max:100,
            dark:0.4,            
            colors:["red",
                    "red",
                    "red",
                    "#ffcf00",
                    "#ffcf00",
                    "#ffcf00",
                    "#ffcf00",                    
                    "#4ed34e",
                    "#4ed34e",
                    "#4ed34e",
                    "#4ed34e",
                    "#4ed34e",
                    "#4ed34e",
                    "#4ed34e",
                    "#4ed34e",
                    "#4ed34e",
                    "#4ed34e"
                    ],

            //no need to change those
            value:0,
            inited:false
        }
    },

   
    methods: {        
        getElement: function(name){
            return this.$refs[name][0]
        },

        full: function(){
            return Math.floor(this.colors.length*this.percentage/100)
        },
        half: function (){
            let p = this.colors.length*this.percentage/100;
            p -= this.full()
            p *= .5
            p += this.dark;
            return p;
        },

        lit: function(){
            if(this.inited == false){
                return
            }
            this.colors.forEach((e,i) => {
                let dot = this.getElement("dot-"+i+"-"+this.id);
                if(!dot){
                    console.log("no dots found")
                    return
                }                
                if(i<this.full()){
                    dot.style.filter= "brightness(1.1)";
                }
                else if(i==this.full()){                   
                    dot.style.filter = "brightness("+this.half()+")";
                }
                else{
                    dot.style.filter= "brightness("+this.dark+")";
                }
            })
        }
    },
    watch: {
        msg: function(){    
            if(this.msg.payload != undefined){           
                this.value = this.msg.payload                
                this.lit()
            }
        }
    },
    computed: {
        formattedValue: function () {
            return this.value.toFixed(2)
        },
        percentage: function(){
            return Math.floor(((this.value - this.min) / (this.max - this.min)) * 100);
        }
    },
    mounted(){
        this.colors.forEach((c,i) => {
                let dot = this.getElement("dot-"+i+"-"+this.id);
                if(!dot){
                    console.log("no dots found")
                    return
                }
                dot.style.backgroundColor = c
            }
        )       
        this.inited = true;
    },
        
}
</script>

<style>
    .level{
        display: flex;
        gap:2px;
        flex-direction: row;
        margin: auto;
    }
    .led {
        background: #ffffff;
        width: 100%;
        height: 100%;
        border-radius: 4px;
        box-shadow: inset 0px 0px 20px 0px #00000099, 0px 0px 3px 0px #00000099;
        filter: brightness(0.4);
    }
</style>

For CSS add those:

.vertical{
        flex-direction:column-reverse;
    }
.vertical .led{
    box-shadow: inset 0px 0px 8px 0px #00000099, 0px 0px 3px 0px #00000099;
}

Template:

<template>
    <div class="level" :class="{ vertical: isVertical}">
        <div v-for="(color, index) in colors" :key="index" class="led" :ref="'dot-' + index +'-'+this.id"></div>
    </div>
</template>

And for data definition add isVertical (true for vertical, false for horisontal)

 data(){
        return {
            //define me here                   
            min:0,
            max:100,
            dark:0.4,
            isVertical:true,
// continue with colors and other stuff

1 Like

Love it when you guys share some nice UI. Spurs me on to create a pure HTML/CSS/JS version. :slight_smile:

So I'm trying to work on a web component version - something I meant to find time to do a long time back. Anyway, getting there slowly. Need to make the sizing a bit more robust & add in the colouring.

<simple-gauge 
    min="0" max="5" value="3.5" 
    mode="v" 
    style="width:50%;"
>
    Caption 1
</simple-gauge>

<p></p>

<simple-gauge min="0" max="5" value="3.5" mode="h" style="width:50%">
    Caption 2
</simple-gauge>   

So is this something that would render in Dashboard 2, or are you talking about something for UIbuilder?

I used another approach, but I'm learning a lot from the elements you're posting. I'm modifying it and applying it to the panels of the projects I create. I would like to ask you for permission to provide these components on my YouTube channel to promote dashborad 2.0 and encourage the use of node-red in Brazil. Everything I create I will post here and we will help each other. Below is the element I derived for using water and gas box measurements in the automations we carry out.

image

<template>     
    <v-card class="elemento">         
        <div class="titulo1">{{this.value}}%</div>
        <div class="led" v-for="(color, index) in colors" :key="index" :ref="'dot-' + index +'-'+this.id"></div>
        <div class="titulo2">Valor</div>
    </v-card>
</template>

<script>
    export default {
    data(){
        return {                   
            min:0,
            max:100,
            dark:0.4,            
            colors:[
                    "#4ed34e",
                    "#4ed34e",
                    "#4ed34e",
                    "#4ed34e",
                    "#4ed34e",
                    "#4ed34e",
                    "#4ed34e",
                    "#4ed34e",
                    "#4ed34e",
                    "#4ed34e",
                    "#ffcf00",
                    "#ffcf00",
                    "#ffcf00",
                    "#ffcf00", 
                    "red",
                    "red",
                    "red"                
                ],            
            value:0,            
            inited:false
        }
    },   
    methods: {        
        getElement: function(name){
            return this.$refs[name][0]
        },

        full: function(){
            return Math.floor(this.colors.length*this.percentage/100)
        },
        half: function (){
            let p = this.colors.length*this.percentage/100;
            p -= this.full()
            p *= .5
            p += this.dark;
            return p;
        },

        lit: function(){
            if(this.inited == false){
                return
            }
            this.colors.forEach((e,i) => {
                let dot = this.getElement("dot-"+i+"-"+this.id);
                if(!dot){
                    console.log("no dots found")
                    return
                }                
                if(i<this.full()){
                    dot.style.filter= "brightness(1.1)";
                }
                else if(i==this.full()){                   
                    dot.style.filter = "brightness("+this.half()+")";
                }
                else{
                    dot.style.filter= "brightness("+this.dark+")";
                }
            })
        }
    },
    watch: {
        msg: function(){    
            if(this.msg.payload != undefined){           
                this.value = this.msg.payload                
                this.lit()
            }
        }
    },
    computed: {
        formattedValue: function () {
            return this.value.toFixed(2)
        },
        percentage: function(){
            return Math.floor(((this.value - this.min) / (this.max - this.min)) * 100);
        }
    },
    mounted(){
        this.colors.forEach((c,i) => {
                let dot = this.getElement("dot-"+i+"-"+this.id);
                if(!dot){
                    console.log("no dots found")
                    return
                }
                dot.style.backgroundColor = c
            }
        )       
        this.inited = true;
    },        
}
</script>

<style>
    .elemento {
        display: flex; 
        flex-direction: column-reverse; 
        margin: auto; 
        width: 75px !important; 
        height: 150px !important;        
        border: 1px solid #000000 !important; 
        border-radius: 18px;
    }
    .titulo1 {
        color: #4F4F4F; 
        margin: auto; 
        font-size: 14px;
    }
    .titulo2 {
        color: #4F4F4F; 
        margin: auto; 
        font-size: 14px;
    }
    .led {
        background: #ffffff; 
        width: 100%; 
        height: 100%; 
        box-shadow: inset 0px 0px 20px 0px #00000099, 0px 0px 3px 0px #00000099; 
        filter: brightness(0.4);
    }
</style>

1 Like

I'm having trouble knowing how to align the elements in the groups. In d1 it was simple, but in d2 I still haven't learned how to do it.

THe web components I build are mostly not tied to UIBUILDER but designed to work in any environment. Even outside Node-RED! If you can imagine such a thing!

So this should indeed render within D1, D2, http-in/-out or UIBUILDER. That is one of the purposes of me recommending that approach. I wouldn't have commented here otherwise.

3 Likes

I should think not :laughing:

Granted. Just don't forget to mention how those components involved and why not some thank you for community and people who are closely behind it.

Maybe it's only me but if you are doing such work you later going to share - please do use English in your code. (variable names, function names...) It is not easy to read...

None of my skills is hidden for you to gain in tempo or fight with obstacles. Just drop me a letter. I'm just not on position to create and maintain ready made components by myself.

3 Likes

<v-card style="display: flex; flex-direction: column; margin: auto; width: 75px; height: 150px;

If you create elements with hardcoded dimensions, you most probably need to adjust containers where those elements will be situated. That means - you'll need to override most of the dashboard styles. Are you sure you want to do it?

There's no right or wrong way doing it. But most simple will be not to fight with dashboard but learn to live in it. As all your created elements will be placed into same sort of container, accept it's behaviors and allow dynamic sizes in your widgets. Dashboard does the overall layouting for you.

If you want to have really custom layout, use UIBUILDER or create your own dashboard solution from scratch.

That simple it is.

1 Like

:slight_smile: Thanks - your work is always an ongoing inspiration.

The process is much of the fun though. So learning goes on. But I'll reach out if I get stuck - thanks.

1 Like

As we do currently waiting for gauge widget, the one can at least use this poor man version of ui-level replica.

[{"id":"cf79351b4995d067","type":"ui-slider","z":"4eb808f5e19fb7e2","group":"5d78082af21e201d","name":"","label":"slider","tooltip":"","order":3,"width":0,"height":0,"passthru":false,"outs":"all","topic":"topic","topicType":"msg","thumbLabel":true,"min":0,"max":"100","step":1,"className":"","x":330,"y":280,"wires":[["7b950200a285852f","b0e36cfaef476d35"]]},{"id":"b0e36cfaef476d35","type":"ui-template","z":"4eb808f5e19fb7e2","group":"5d78082af21e201d","page":"","ui":"","name":"Linear Led Gauge","order":0,"width":"6","height":"1","head":"","format":"<template>\n    <div class=\"led-level\">\n        <div class=\"led-level-text\">\n            <span class=\"led-level-label\">{{label}}</span>\n            <span class=\"led-level-value\">{{formattedValue}}<span class=\"led-level-unit\">{{unit}}</span></span>\n        </div>\n        <div class=\"led-level-stripe\" :class=\"{ vertical: isVertical}\">\n            <div v-for=\"(color, index) in colors\" :key=\"index\" class=\"led-level-led\" :ref=\"'dot-' + index +'-'+this.id\"></div>\n        </div>\n        <div class=\"led-level-limits\">\n            <span>{{min}}</span>\n            <span>{{max}}</span>\n        </div>\n    <div>\n</template>\n\n<script>\nexport default {\n    data(){\n        return {\n            //define me here                   \n            min:0,\n            label:\"MEASURE\",\n            unit:\"cm³\",\n            max:100,\n            dark:0.4,\n            isVertical:false,\n            animate:true,                        \n            colors:[\"#4ed34e\",\n                    \"#4ed34e\",\n                    \"#4ed34e\",\n                    \"#4ed34e\",\n                    \"#4ed34e\",\n                    \"#4ed34e\",\n                    \"#4ed34e\",\n                    \"#4ed34e\",\n                    \"#4ed34e\",\n                    \"#4ed34e\",\n                    \"#ffcf00\",\n                    \"#ffcf00\",\n                    \"#ffcf00\",\n                    \"#ffcf00\",\n                    \"red\",\n                    \"red\",\n                    \"red\"],\n\n            //no need to change those\n            value:0,\n            previousValue:0,\n            inited:false\n        }\n    },\n\n   \n    methods: {        \n        getElement: function(name){\n            return this.$refs[name][0]\n        },\n\n        full: function(){\n            return Math.floor(this.colors.length*this.percentage/100)\n        },\n        half: function (){\n            let p = this.colors.length*this.percentage/100;\n            p -= this.full()\n            p *= .5\n            p += this.dark;\n            return p;\n        },\n\n        lit: function(){\n            if(this.inited == false){\n                return\n            }\n            const down = this.previousValue > this.value\n\n            let time = .01\n            this.colors.forEach((e,i) => {\n                let dot = this.getElement(\"dot-\"+i+\"-\"+this.id);\n                if(!dot){\n                    console.log(\"no dots found\")\n                    return\n                }                \n                if(i<this.full()){\n                    dot.style.filter= \"brightness(1.1)\";\n                }\n                else if(i==this.full()){                   \n                    dot.style.filter = \"brightness(\"+this.half()+\")\";\n                }\n                else{\n                    dot.style.filter= \"brightness(\"+this.dark+\")\";\n                }\n                if(down){\n                    time = (this.colors.length - i) * .12                    \n                }\n                dot.style.transition = this.animate ? \"filter \"+time+\"s\" : \"unset\";\n            })\n            this.previousValue = this.value\n        }\n    },\n    watch: {\n        msg: function(){    \n            if(this.msg.payload != undefined){           \n                this.value = this.msg.payload                \n                this.lit()\n            }\n        }\n    },\n    computed: {\n        formattedValue: function () {\n            return this.value.toFixed(2)\n        },\n        percentage: function(){\n            return Math.floor(((this.value - this.min) / (this.max - this.min)) * 100);\n        }\n    },\n    mounted(){\n        this.colors.forEach((c,i) => {\n                let dot = this.getElement(\"dot-\"+i+\"-\"+this.id);\n                if(!dot){\n                    console.log(\"no dots found\")\n                    return\n                }\n                dot.style.backgroundColor = c               \n                dot.style.transition = \"filter 0.1s\";\n            }\n        )       \n        this.inited = true;\n    },\n        \n}\n</script>\n","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":630,"y":280,"wires":[[]]},{"id":"de08b76d5e936b8a","type":"ui-template","z":"4eb808f5e19fb7e2","group":"","page":"","ui":"da26eafa8eb48ab3","name":"Linear Led Gauge for Dasboard 2.0 CSS","order":0,"width":0,"height":0,"head":"","format":".led-level{\n    display: grid;\n    grid-template-rows: 1.3em 1fr .7em;\n    gap: 2px;\n}\n.led-level-stripe{\n    display: flex;\n    gap:2px;\n}    \n.led-level-led {\n    background: #ffffff;\n    width: 100%;\n    height: 100%;\n    border-radius: 4px;\n    box-shadow: inset 0px 0px 10px 0px #00000099, 0px 0px 3px 0px #00000099;\n    filter: brightness(0.4);\n}\n.led-level-text{\n    font-size: 1.25em;\n    line-height: 1em;\n    align-self: end;\n    display: flex;\n    flex-wrap: wrap;\n    justify-content: space-between;\n    user-select: none;\n}\n.led-level-value{       \n    font-weight:bold;\n}\n.led-level-unit{\n    font-size:.75em;\n    font-weight:normal;\n    padding-inline-start: 0.15em;\n}\n.led-level-limits{\n    display: flex;\n    justify-content: space-between;\n    font-size: .75em;\n    line-height: .75em;\n    align-content: flex-end;\n    flex-wrap: wrap;\n     user-select: none;\n}","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"site:style","className":"","x":700,"y":240,"wires":[[]]},{"id":"5d78082af21e201d","type":"ui-group","name":"Some stuff","page":"71097e9858ab99f9","width":"6","height":"1","order":3,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"da26eafa8eb48ab3","type":"ui-base","name":"Board","path":"/dashboard"},{"id":"71097e9858ab99f9","type":"ui-page","name":"Test page","ui":"da26eafa8eb48ab3","path":"/second","icon":"home","layout":"grid","theme":"a965ccfef139317a","order":-1,"className":"","visible":"true","disabled":"false"},{"id":"a965ccfef139317a","type":"ui-theme","name":"Default","colors":{"surface":"#5c5c5c","primary":"#0094ce","bgPage":"#383838","groupBg":"#4f4f4f","groupOutline":"#858585"},"sizes":{"pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}}]

image

4 Likes

In the docs for loading external dependencies in the ui-template, it shows a solution of using an interval to check for some value defined since it can't be sure that the 3rd party lib has loaded yet. The alternative solution that i have found is using dynamic imports and it seems reliable and supported by all browsers that i have tried so far.

<template>
    <my-custom-element v-bind:src="msg.src" ref="mce">loading...</my-custom-element>
</template>
<script>
    (async () => {

        await import('https://cdn.jsdelivr.net/npm/hls.js@1');

        await import('/custom-elements/my-custom-element.js');

        this.send(`my-custom-element ${this.id} has loaded`);

        console.log('last received msg', this.msg);

        const eventName = `msg-input:${this.id}`;

        this.$socket.on(eventName, (msg) => {
            console.log(eventName, msg);
        });

        // here, mce is direct reference to the element in the template
        const { mce } = this.$refs; 

        mce.doSomething(); // or whatever

    })();
</script>

This always ensured that the hls.js lib was available for my custom element and also that my custom element was defined before i started interacting with it. Also, the browser only makes a network call once per import and any further elements calling import will get a cached value to use right away.

@TotallyInformation you reminded me when you mentioned the custom element you are working on being compatible with all dashboards, which was also my goal.

Warning, if you are going to dynamic import your custom element class, put a guard clause around it so you don't get the error of the class already being defined (probably only would happen when trying to load it in the ui-template where we have less control of using 3rd party libs).

if (!customElements.get('my-custom-element')) {

    class MyCustomElement extends HTMLElement {
       //...
    }

    customElements.define('my-custom-element', MyCustomElement);

}
1 Like

Should be good on all major browsers from about 2018. Not IE of course. :slight_smile:

:grinning:

Yes, could easily happen accidentally with a 2nd ui_template.

1 Like

OK. Level is not gauge. Maybe if to bend it hard and...

[{"id":"9f3c815ec5f4ba93","type":"ui-template","z":"4eb808f5e19fb7e2","group":"5d78082af21e201d","page":"","ui":"","name":"Round Led Gauge","order":7,"width":"2","height":"2","head":"","format":"<template>\n    <div class=\"round-led-level\" :style=\"`--dot-count:${count}; --size:${size}; --ledsize:${ledSize};`\" >\n        <header>\n            <div class=\"round-led-level-text\">\n                <span class=\"round-led-level-label\">{{label}}</span>\n            </div>\n        </header>\n        <div>\n            <div class=\"round-led-level-stripe\">\n                <div v-for=\"(color, index) in colors\" :key=\"index\" class=\"round-led-level-led\" :ref=\"'dot-' + index +'-'+this.id\">\n                </div>\n            </div>\n            <div class=\"round-led-level-centered-text\">\n                <span class=\"round-led-level-value\">{{formattedValue}}</span>\n                <span class=\"round-led-level-unit\">{{unit}}</span>\n            </div>\n            <div class=\"round-led-level-limits\">\n                <span>{{min}}</span>\n                <span>{{max}}</span>\n            </div>\n        </div>     \n    <div>\n</template>\n\n<script>\nexport default {\n    data(){\n        return {\n            //define me here\n            label:\"MEASURE\", // the label\n            min:0, //smallest expected value\n            max:100, //higest expected value\n            unit:\"cm³\",// unit of the measurement\n            size:140, // ovaerall size. (think in pixels) under 100 will be too small.                                   \n            animate:true,// animating leds is not most performant thing in the world.                          \n            dark:0.4, // how dim is led when not glowing  \n            //define colors as you wish but the thing works clockwise.\n            //led size depends on how many colors is defined.         \n            colors:[                   \n                    \"rgb(var(--v-theme-primary))\",\n                    \"rgb(var(--v-theme-primary))\",\n                    \"rgb(var(--v-theme-primary))\",\n                    \"rgb(var(--v-theme-primary))\",\n                    \"rgb(var(--v-theme-primary))\",\n                    \"rgb(var(--v-theme-primary))\",\n                    \"rgb(var(--v-theme-primary))\",\n                    \"rgb(var(--v-theme-primary))\",\n                    \"rgb(var(--v-theme-primary))\",\n                    \"rgb(var(--v-theme-primary))\",\n                    \"rgb(var(--v-theme-warning))\",\n                    \"rgb(var(--v-theme-warning))\",\n                    \"rgb(var(--v-theme-warning))\",\n                    \"rgb(var(--v-theme-error))\",\n                    \"rgb(var(--v-theme-error))\",\n                   ],            \n            //no need to change those\n            value:0,\n            previousValue:0,\n            inited:false\n        }\n    },\n\n   \n    methods: {        \n        getElement: function(name,base){\n            if(base){\n                return this.$refs[name]\n            }\n            return this.$refs[name][0]\n        },\n\n        full: function(){\n            return Math.floor(this.colors.length*this.percentage/100)\n        },\n        half: function (){\n            let p = this.colors.length*this.percentage/100;\n            p -= this.full()\n            p *= .5\n            p += this.dark;\n            return p;\n        },\n\n        lit: function(){\n            if(this.inited == false){\n                return\n            }\n            const down = this.previousValue > this.value\n\n            let time = .01\n            this.colors.forEach((e,i) => {\n                let dot = this.getElement(\"dot-\"+i+\"-\"+this.id);\n                if(!dot){\n                    console.log(\"no dots found\")\n                    return\n                }                \n                if(i<this.full()){\n                    dot.style.filter= \"brightness(1.1)\";\n                }\n                else if(i==this.full()){                   \n                    dot.style.filter = \"brightness(\"+this.half()+\")\";\n                }\n                else{\n                    dot.style.filter= \"brightness(\"+this.dark+\")\";\n                }\n                if(down){\n                    time = (this.colors.length - i) * .12                    \n                }\n                dot.style.transition = this.animate ? \"filter \"+time+\"s\" : \"unset\";\n            })\n            this.previousValue = this.value\n        }\n    },\n    watch: {\n        msg: function(){    \n            if(this.msg.payload != undefined){           \n                this.value = this.msg.payload                \n                this.lit()\n            }\n        }\n    },\n    computed: {\n        formattedValue: function () {\n            return this.value.toFixed(2)\n        },\n        percentage: function(){\n            return Math.floor(((this.value - this.min) / (this.max - this.min)) * 100);\n        },\n        count: function() {\n            return this.colors.length;\n        },        \n        ledSize:function(){\n            const s = 4.71239 * ((this.size - (this.size*0.3))/2)            \n            return s / this.colors.length\n        }\n    },\n    mounted(){\n        let angle;\n        const step = 270 / this.colors.length; //this.ledSize/2;//270 / (this.colors.length + 2);\n        const radius = (this.size - (this.size*0.1))/2//this.size / 2;\n        const s = this.ledSize / -2;        \n        this.colors.forEach((c,i) => {\n                let dot = this.getElement(\"dot-\"+i+\"-\"+this.id);\n                if(!dot){\n                    console.log(\"no dots found\")\n                    return\n                }\n                dot.style.backgroundColor = c               \n                dot.style.transition = \"filter 0.1s\";\n                dot.style.setProperty('--dot',i);\n                angle = ((i+1)*step) * Math.PI / 180;\n                dot.style.left = s + radius * Math.cos(angle) + 'px';\n                dot.style.top = s + radius * Math.sin(angle) + 'px';\n                dot.style.transform = 'translate('+s+'px, '+s+'px)'; \n                dot.style.rotate = (angle - 0.08)+\"rad\"               \n            }\n        )\n       \n        this.inited = true;\n    },\n        \n}\n</script>\n","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":630,"y":360,"wires":[[]]},{"id":"2545d92a227a8572","type":"ui-template","z":"4eb808f5e19fb7e2","group":"","page":"","ui":"da26eafa8eb48ab3","name":"Round Led Gauge for Dasboard 2.0 CSS","order":0,"width":0,"height":0,"head":"","format":".round-led-level{\n    display: grid;\n    grid-template-rows: 1em 1fr;\n    height: calc(var(--size) * 1px);\n    aspect-ratio: 1/1;\n    position: relative;\n    margin: auto;\n}\n.round-led-level>div{\n    position: relative;;\n}\n.round-led-level-stripe{\n    display: block;\n    position: absolute;\n    left: 50%;\n    top: 56%;\n    rotate: 135deg;  \n}    \n.round-led-level-led {\n    background: #ffffff;\n    position: absolute;\n    width: calc(var(--ledsize) * 1px);\n    aspect-ratio: 1/1;\n    border-radius: 4px;\n    box-shadow: inset 0px 0px calc(var(--ledsize) / 3 * 1px) 0px #00000099, 0px 0px calc(var(--ledsize) / 7 * 1px) 0px #00000099;\n    filter: brightness(0.4);\n    transform-origin: center center;\n    \n\n}\n.round-led-level-text{    \n    font-size: 1rem;\n    line-height: 1rem;\n    text-align: center;\n    user-select: none;\n}\n.round-led-level-centered-text{\n    position: absolute;\n    inset: 0;\n    font-size: 1rem;\n    line-height: 1;\n    display: grid;\n    text-align: center;\n    grid-template-rows: 1fr 1fr;\n    gap: .25em;\n    user-select: none;\n    padding-block: 42%;\n    align-items: center;\n}\n.round-led-level-value{       \n    font-weight:bold;\n    font-size: calc(var(--size) * .15 * 1px);\n}\n.round-led-level-unit{\n    font-size:calc(var(--size) * .1 * 1px);\n    font-weight:normal;\n    padding-inline-start: 0.15em;\n}\n.round-led-level-limits{\n    position: absolute;\n    inset:0; \n    display: flex;\n    justify-content: space-between;\n    font-size: calc(var(--size) * .06 * 1px);\n    line-height: calc(var(--size) * .06 * 1px);\n    align-content: flex-end;\n    flex-wrap: wrap;\n    padding-inline:1em;\n    user-select: none;\n}","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"site:style","className":"","x":700,"y":320,"wires":[[]]},{"id":"5d78082af21e201d","type":"ui-group","name":"Some stuff","page":"71097e9858ab99f9","width":"6","height":"1","order":1,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"da26eafa8eb48ab3","type":"ui-base","name":"Board","path":"/dashboard"},{"id":"71097e9858ab99f9","type":"ui-page","name":"Test page","ui":"da26eafa8eb48ab3","path":"/second","icon":"home","layout":"grid","theme":"a965ccfef139317a","order":-1,"className":"","visible":"true","disabled":"false"},{"id":"a965ccfef139317a","type":"ui-theme","name":"Default","colors":{"surface":"#575757","primary":"#0fb8ad","bgPage":"#474747","groupBg":"#525252","groupOutline":"#7c837e"},"sizes":{"pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}}]

14 Likes