Dashboard 2.0 is now Generally Available

I suspect the piece I'm missing here is a differentiation between on-load and on-connect. The former is running smoothly, when a browser loads, refreshes, etc. The latter is what I would expect the SocketIO client to do automatically, but it would appear that it isn't.

Socket.IO does not cache any data, you have to do that yourself in Node-RED. I think that D1 did this internally in some "magic" function. in UIBUILDER, I avoided the problem and maintained flexibility by having a separate cache node and making the main node output suitable control messages to trigger the cache.

However, the latest version of Socket.IO DOES have a caching capability. Not sure how good it is though and not sure what the impact might be if an environment had tons of complex messages with lots of offline users.

@joepavitt @TotallyInformation @Colin

I believe we are talking about the same thing when I point out that the ui-templat node, when it receives a simple button project, does not maintain its state when the page is reloaded or updated. Everything I set up within ui-templat returns to its origin when I restart the system or simply reload the page. D1 maintained this, I don't know exactly how. I would like to learn how to maintain the state. Below is an example of a button.

<template>
    <v-card ref="card"
        style="display: flex; flex-direction: column; margin: auto; width: 75px; height: 105px; background-color: #4F4F4F; border: 1px solid #000000; border-radius: 18px;">
        <v-btn ref="botao"
            style="width: 75px; height: 75px; color: #000000; background-color: #4F4F4F; font-size: 14px; border-radius: 18px;"
            stacked @click="alternar">
            Luz
            <v-icon ref="icon" style="font-size: 45px;">mdi-lightbulb-variant-outline</v-icon>
        </v-btn>
        <v-slider ref="barra" color="#BD9608" v-model="barravalor" @end="deslisamento"></v-slider>
    </v-card>
</template>

<script>
    export default {
        data() {
            return {
                chave: 'a1',
                barravalor: 0
            };
        },
        methods: {
            alternar: function () {
                if (window.localStorage.getItem(this.chave) === 'ON') {
                    this.desligar();                    
                } else if (window.localStorage.getItem(this.chave) === 'OFF') {
                    this.ligar();                    
                } else {
                    this.desligar();
                }
            },
            ligar: function () {            
                window.localStorage.setItem(this.chave, 'ON');
                this.$refs.icon.$el.style.color = '#BD9608';
                this.$refs.icon.$el.style.textShadow = '0px 0px 10px #BD9608';
                this.barravalor = 1;
                if (window.localStorage.getItem(this.chave) === 'OFF') {
                    this.send({ status: 'ON' });           
                }
                
            },
            desligar: function () {                
                window.localStorage.setItem(this.chave, 'OFF');
                this.$refs.icon.$el.style.color = '#A9A9A9';
                this.$refs.icon.$el.style.textShadow = '0px 0px 0px'; 
                this.barravalor = 0;
                if (window.localStorage.getItem(this.chave) === 'ON') {
                    this.send({ status: 'OFF' });           
                }
                
            },
            deslisamento: function () {
                if (window.localStorage.getItem(this.chave) === "ON") {
                    this.send({ brilho: parseInt(this.barravalor) });
                } else {
                    this.barravalor = 0;
                }       
            },
            setState (value) {
                if (value === 'ON') {                    
                    this.ligar();
                } else if (value === 'OFF') {                    
                    this.desligar();
                }
            }
        },
        mounted () {          
            this.setState(window.localStorage.getItem(this.chave));         
            this.$socket.on("msg-input:" + this.id, (msg) => {
                this.setState(msg.payload);
            });                               
        }
    }
</script>

Not exactly, I am talking about the core widgets such as switches, text etc, which do retain their state over a reload. The situation is, I believe, different for a template node.

We plan to build a GUI for a new product based on Dashboard 2.0. This GUI will be tested many times before release.

There may be a minor bug on the Dashboard 2.0 (v1.0 and v1.0.2).: The landing page does not change with the order of pages Each time when users go the the dashboard, it always land on the "Gateway config" page (circled in red)


It is supposed to land on the "Home" page instead.

Hi @davidz its a missing feeature at the moment: Configure Default Page Ā· Issue #137 Ā· FlowFuse/node-red-dashboard Ā· GitHub

Hopefully we will get it in soon

I made some changes to the labels of the ui-text nodes. I could not figure out why the labels did not change after deploy. I had to restart the flow to make the changes happen

If you can reproduce the problem again, can you please open an issue here: GitHub - FlowFuse/node-red-dashboard

@joepavitt Got it. Thank you!

Quick one for monday morning to play with ...

image

Template

<template>
    <div class="ag-wrapper">
        <div class="ag-icon">
            <v-icon aria-hidden="false">{{icon}}</v-icon>
        </div>
        <div class="ag-content">
            <div class="ag-text">
                <span class="ag-label">{{label}}</span>
                <span class="ag-value">{{formattedValue}}<span class="ag-unit">{{unit}}</span></span>
            </div>
            <div class="ag-track">
                <div class="ag-track-background"></div>            
                <div class="ag-track-foreground" :style="{'width': percentage +'%'}"></div>
            </div>
            <div class="ag-limits">
                <span class="ag-min">{{min}}</span>
                <span class="ag-max">{{max}}</span>
            </div>
        </div>
    </div>
    </div>
</template>

<script>
    export default {
        data() {
            return {
                // define me here               
                label:"MEASURE",
                unit:"Kg",
                icon:"mdi-account",               
                min:0,
                max:100,

                //no need to change those
                value: 0
            }
        },
        watch: {
            msg: function(){
                if(this.msg.payload != undefined){                   
                    this.value = this.msg.payload                    
                }
            }
        },
        computed: {
            formattedValue: function () {
                return this.value.toFixed(2)
            },
            percentage: function(){
                return Math.floor(((this.value - this.min) / (this.max - this.min)) * 100);
            } 
        }
    }
</script>

CSS

.ag-wrapper {
    display: grid;
    grid-template-columns: 3em 1fr;
    gap:1em;
}
.ag-icon{
    font-size: 2em;
    display: flex;
    flex-direction: column;
    justify-content: center;        
}
.ag-content{
    display: grid;
    grid-template-rows: 1fr 7px 0.75em;
    gap: 2px;
}
.ag-text{
    font-size: 1.25em;
    line-height: 1em;
    align-self: end;
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
}
.ag-value{       
    font-weight:bold;
}
.ag-unit{
    font-size:.75em;
    font-weight:normal;
    padding-inline-start: 0.15em;
}
.ag-limits{
    display: flex;
    justify-content: space-between;
    font-size: .75em;
    line-height: .75em;
    align-content: center;
    flex-wrap: wrap;
}

.ag-track{
    position:relative;
    display:flex;
    align-items: center;
    width: 100%;
    border-radius: 6px;
}

.ag-track-background{
    position:absolute;
    background: rgb(var(--v-theme-primary));
    opacity: 0.45;
    width: 100%;
    height: 50%;
    border-radius:inherit;
}
.ag-track-foreground{
    position:absolute;
    background: rgb(var(--v-theme-primary));       
    width: 50%;
    height: 100%;
    border-radius:inherit;
}
7 Likes

I'm using Google Chome and the bar only shows the value updating when I refresh the page. This approach was great and I would like to understand more about how you are maintaining the value when updating the page.

I used this approach

image

<template>
    <v-progress-circular size="20" style="display: flex; margin: auto; width: 75px; height: 75px; color: #BD9608; background-color: #4F4F4F; border: 1px solid #000000; border-radius: 18px;" v-model="entrada">
        <template v-slot:default="{ value }">
            <strong style="color: #000000">{{ Math.ceil(value) }}%</strong>
        </template>
    </v-progress-circular>
</template>

<script>
  export default {
    data() {
      return {
        chave: 'a5',
        entrada: 0,
      };
    },
    methods: {
      setState(value) {
        this.entrada = value;
        localStorage.setItem(this.chave, value.toString());
    },
    recuperarEstadoAnterior() {
      const savedValue = localStorage.getItem(this.chave);
        if (savedValue !== null) {
          this.entrada = parseInt(savedValue, 10);
        }
      },
    },
    mounted() {
      this.recuperarEstadoAnterior();
      this.$socket.on("msg-input:" + this.id, (msg) => {
        this.setState(msg.payload);
      });
    },
  };
</script>

I'm far from being an expert on Vue stuff, but as it is possible to watch changes and the msg is also possible to watch, then whenever the msg comes in, the change is reflected.

mounted() {
      this.recuperarEstadoAnterior();
      this.$socket.on("msg-input:" + this.id, (msg) => {
        this.setState(msg.payload);
      });
    }

By registering the socket listener on mounted, I think it may be too late and you may be missing first message which has last known good value.

But as told - I don't know even if I'm doing it right way ...

Surely I must have a problem here. I'm taking your code without changing anything and when I send the value to the template it doesn't change. To see the new value I sent, I need to refresh the page. =[

This is actually a really nice way to do it, away on holidsy this week, but I'll update the docs when I'm back with this

I had already reached this goal previously, but every time the page is updated, the button is lost and does not tell me whether it is on or off.

image

<template>
    <v-btn ref="botao"
        style="display: flex; flex-direction: column; margin: auto; height: 75px; width: 75px; background-color: #4F4F4F; color: #000000; border: 1px solid #000000; font-size: 14px; border-radius: 18px;"
        stacked @click="alternar">
        Externa
        <v-icon ref="icone" style="font-size: 40px;">mdi-beach</v-icon>
    </v-btn>
</template>

<script>
    export default {
        data() {
            return {
                value: "OFF"
            };
        },
        methods: {
            alternar: function () {
                if (this.value === 'ON') {
                    this.desligar();
                } else if (this.value === 'OFF') {
                    this.ligar();
                } else {
                    this.desligar();
                }
            },
            ligar: function () {
                this.$refs.icone.$el.style.color = '#BD9608';
                this.$refs.icone.$el.style.textShadow = '0px 0px 10px #BD9608';
                if (this.value === 'OFF') {
                    this.send({ status: 'ON' });
                    this.value = "ON"; 
                }
            },
            desligar: function () {
                this.$refs.icone.$el.style.color = '#A9A9A9';
                this.$refs.icone.$el.style.textShadow = '0px 0px 0px';
                if (this.value === 'ON') {
                    this.send({ status: 'OFF' });
                    this.value = "OFF"; 
                }
            }
        },
        watch: {
            msg: function(){
                if(this.msg.payload != undefined){
                    if (this.msg.payload === "ON") {
                        this.ligar();
                        this.value = "ON"; 
                    } else if (this.msg.payload === "OFF") {
                        this.desligar();
                        this.value = "OFF"; 
                    }
                }
            }
        }
    }
</script>

What do you mean by that?

image

Maybe this way it works.

<template>
    <v-btn ref="botao"
        style="display: flex; flex-direction: column; margin: auto; height: 75px; width: 75px; background-color: #4F4F4F; color: #000000; border: 1px solid #000000; font-size: 14px; border-radius: 18px;"
        stacked @click="alternar">
        Externa
        <v-icon ref="icone" style="font-size: 40px;">mdi-beach</v-icon>
    </v-btn>
</template>

<script>
    export default {
        data() {
            return {
                value: "OFF"
            };
        },
        methods: {
            alternar: function () {
                if (this.value === 'ON') {
                    this.desligar();
                } else if (this.value === 'OFF') {
                    this.ligar();
                } else {
                    this.desligar();
                }
            },
            ligar: function () {
                this.$refs.icone.$el.style.color = '#BD9608';
                this.$refs.icone.$el.style.textShadow = '0px 0px 10px #BD9608';
                if (this.value === 'OFF') {
                    this.send({ payload: 'ON' });
                    this.value = "ON"; 
                }
            },
            desligar: function () {
                this.$refs.icone.$el.style.color = '#A9A9A9';
                this.$refs.icone.$el.style.textShadow = '0px 0px 0px';
                if (this.value === 'ON') {
                    this.send({ payload: 'OFF' });
                    this.value = "OFF"; 
                }
            }
        },
        watch: {
            msg: function(){
                if(this.msg.payload != undefined){
                    console.log('got message :',this.msg)
                    if (this.msg.payload === "ON") {
                        this.ligar();
                    } else if (this.msg.payload === "OFF") {
                        this.desligar();                        
                    }
                }
            }
        }
    }
</script>

It worked out! Was that the change?

console.log('got message :',this.msg)

You had send function without payload. That was main thing why reply message wasn't heard.