FlowFuse Dashboard 2.0 Webinar Examples

https://flowfuse.com/webinars/2024/node-red-dashboard-multi-user/

[{"id":"d0bb42ebf3c14b07","type":"comment","z":"deee9af5aea3c97a","name":"Raspberry Pi Data","info":"","x":150,"y":580,"wires":[]},{"id":"08e7f793a451afc0","type":"project link in","z":"deee9af5aea3c97a","name":"project in 1","project":"all","broadcast":false,"topic":"mqtt-topic","x":120,"y":620,"wires":[["146e2455d5ea5457","0667790c888ead2f","5b77808caf572572"]]},{"id":"b7c88334044d2442","type":"ui-form","z":"deee9af5aea3c97a","name":"","group":"f5c2b7111eeb7ab2","label":"","order":0,"width":0,"height":0,"options":[{"label":"Title","key":"title","type":"text","required":true,"rows":null},{"label":"Description","key":"description","type":"text","required":true,"rows":null},{"label":"Due Date","key":"due","type":"date","required":true,"rows":null},{"label":"Is Priority","key":"priority","type":"switch","required":false,"rows":null}],"formValue":{"title":"","description":"","due":"","priority":false},"payload":"","submit":"submit","cancel":"clear","resetOnSubmit":true,"topic":"topic","topicType":"msg","splitLayout":"","className":"","x":110,"y":180,"wires":[["0079bf7797296143","65e68efe103bc565"]]},{"id":"8be707bb34ed9421","type":"ui-notification","z":"deee9af5aea3c97a","ui":"60061a41144c1c54","position":"top right","colorDefault":false,"color":"#1bde42","displayTime":"3","showCountdown":true,"outputs":0,"allowDismiss":true,"dismissText":"Close","raw":false,"className":"","name":"","x":490,"y":200,"wires":[]},{"id":"0079bf7797296143","type":"change","z":"deee9af5aea3c97a","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"Thanks for submitting a task","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":280,"y":200,"wires":[["8be707bb34ed9421"]]},{"id":"fc8d2438da464532","type":"ui-template","z":"deee9af5aea3c97a","group":"","page":"","ui":"60061a41144c1c54","name":"User Info (top-right)","order":0,"width":0,"height":0,"head":"","format":"<template>\n    <Teleport v-if=\"loaded\" to=\"#app-bar-actions\">\n        <div class=\"user-info\">\n            <img :src=\"setup.socketio.auth.user.image\" />\n            <span>Hi, {{ setup.socketio.auth.user.name }}!</span>\n        </div>\n    </Teleport>\n</template>\n\n<script>\n    export default {\n    data () {\n        return {\n            loaded: false\n        }\n    },\n    mounted() {\n        // code here when the component is first loaded\n        console.log('on mounted')\n        console.log(this.$store.state.setup?.setup?.socketio?.auth.user)\n        this.loaded = true\n    }\n}\n</script>\n\n<style>\n    .user-info {\n        display: flex;\n        align-items: center;\n        gap: 8px;\n    }\n\n    .user-info img {\n        width: 24px;\n        height: 24px;\n    }\n</style>","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"widget:ui","className":"","x":150,"y":40,"wires":[[]]},{"id":"65e68efe103bc565","type":"function","z":"deee9af5aea3c97a","name":"Store Form Entry","func":"const tasks = global.get(\"tasks\") || []\n\n// merge the form data with the _client.user\ntasks.push({\n    ...msg.payload,\n    ... {\n        user: msg._client.user\n    }\n})\n\n// set this to a global variable \nglobal.set(\"tasks\", tasks)","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":290,"y":160,"wires":[[]]},{"id":"13e96e85f55ecd1d","type":"ui-template","z":"deee9af5aea3c97a","group":"0d8bb4422190121d","page":"","ui":"","name":"Tasks List","order":0,"width":0,"height":0,"head":"","format":"<template>\n    <!-- Provide an input text box to search the content -->\n    <v-text-field v-model=\"search\" label=\"Search\" prepend-inner-icon=\"mdi-magnify\" single-line variant=\"outlined\"\n    hide-details></v-text-field>\n    <v-data-table v-model:search=\"search\" :items=\"msg?.payload\">\n      <template v-slot:header.current>\n        <!-- Override how we render the header for the \"current\" column -->\n        <div class=\"text-center\">Center-Aligned</div>\n      </template>\n      \n      <template v-slot:item.priority=\"{ item }\">\n        <v-icon v-if=\"item.priority\" icon=\"mdi-alert\" color=\"red\"></v-icon>\n      </template>\n\n      <template v-slot:item.user=\"{ item }\">\n        <!-- Add a custom suffix to the value for the \"target\" column -->\n        <div class=\"user-info\">\n            <img :src=\"item.user.image\" />\n            <span>{{ item.user.username }}</span>\n        </div>\n      </template>\n\n      <template v-slot:item.due=\"{ item }\">\n        {{ daysBetween(item.due, new Date())}} Days\n      </template>\n    \n    </v-data-table>\n</template>\n\n<script>\n    export default {\n    data () {\n      return {\n        search: ''\n      }\n    },\n    methods: {\n        // add a function to determine the color of the progress bar given the row's item\n        getColor: function (item) {\n          if (item.current > item.target) {\n            return 'red'\n          } else {\n            return 'green'\n          }\n        },\n        daysBetween(date1, date2) {\n            // Convert both dates to milliseconds\n            var date1_ms = new Date(date1).getTime();\n            var date2_ms = new Date(date2).getTime();\n            \n            // Calculate the difference in milliseconds\n            var difference_ms = Math.abs(date1_ms - date2_ms);\n            \n            // Convert back to days and return\n            return Math.round(difference_ms / (1000 * 60 * 60 * 24));\n        }\n    }\n  }\n</script>","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":780,"y":300,"wires":[[]]},{"id":"bdc084bff2615fb0","type":"ui-event","z":"deee9af5aea3c97a","ui":"60061a41144c1c54","name":"","x":120,"y":340,"wires":[["430be0411268db42","dddb1c95f1be3a69","bb0cbe58cd80129a","817e3b341f4b02c6"]]},{"id":"f2f8c446e3f24f39","type":"change","z":"deee9af5aea3c97a","name":"Load Tasks from Global","rules":[{"t":"set","p":"payload","pt":"msg","to":"tasks","tot":"global","dc":true}],"action":"","property":"","from":"","to":"","reg":false,"x":590,"y":300,"wires":[["13e96e85f55ecd1d"]]},{"id":"430be0411268db42","type":"switch","z":"deee9af5aea3c97a","name":"$pageview: Admin View","property":"payload.page.name","propertyType":"msg","rules":[{"t":"eq","v":"Admin View","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":350,"y":300,"wires":[["f2f8c446e3f24f39"]]},{"id":"e8c2f792a7a98233","type":"ui-template","z":"deee9af5aea3c97a","group":"b362eb01162ac195","page":"","ui":"","name":"Tasks List","order":0,"width":0,"height":0,"head":"","format":"<template>\n    <!-- Provide an input text box to search the content -->\n    <v-text-field v-model=\"search\" label=\"Search\" prepend-inner-icon=\"mdi-magnify\" single-line variant=\"outlined\"\n    hide-details></v-text-field>\n    <v-data-table v-model:search=\"search\" :items=\"msg?.payload\">\n      <template v-slot:header.current>\n        <!-- Override how we render the header for the \"current\" column -->\n        <div class=\"text-center\">Center-Aligned</div>\n      </template>\n\n      <template v-slot:item.user=\"{ item }\">\n        <!-- Add a custom suffix to the value for the \"target\" column -->\n        {{ item.user.username }}\n      </template>\n\n      <template v-slot:item.due=\"{ item }\">\n        {{ item.due }}\n        <!-- <v-progress-linear v-model=\"item.current\" min=\"15\" max=\"25\" height=\"25\" :color=\"getColor(item)\">\n          <template v-slot:default=\"{ value }\">\n            <strong>{{ item.current }}°C</strong>\n          </template>\n        </v-progress-linear> -->\n      </template>\n    \n    </v-data-table>\n</template>\n\n<script>\n    export default {\n    data () {\n      return {\n        search: ''\n      }\n    },\n    methods: {\n        // add a function to determine the color of the progress bar given the row's item\n      getColor: function (item) {\n        if (item.current > item.target) {\n          return 'red'\n        } else {\n          return 'green'\n        }\n      }\n    }\n  }\n</script>","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":980,"y":380,"wires":[[]]},{"id":"884934de8d0ac7b3","type":"change","z":"deee9af5aea3c97a","name":"Load Tasks from Global","rules":[{"t":"set","p":"payload","pt":"msg","to":"tasks","tot":"global","dc":true}],"action":"","property":"","from":"","to":"","reg":false,"x":590,"y":380,"wires":[["fdb08d99dd2bdadc"]]},{"id":"dddb1c95f1be3a69","type":"switch","z":"deee9af5aea3c97a","name":"$pageview: Your Tasks","property":"payload.page.name","propertyType":"msg","rules":[{"t":"eq","v":"Your Tasks","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":340,"y":380,"wires":[["884934de8d0ac7b3"]]},{"id":"fdb08d99dd2bdadc","type":"function","z":"deee9af5aea3c97a","name":"Filter Tasks to User","func":"msg.payload = msg.payload?.filter((task) => {\n    return msg._client.user.username === task.user.username\n})\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":810,"y":380,"wires":[["e8c2f792a7a98233"]]},{"id":"bb0cbe58cd80129a","type":"switch","z":"deee9af5aea3c97a","name":"Is Admin User","property":"admins","propertyType":"global","rules":[{"t":"cont","v":"_client.user.username","vt":"msg"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":340,"y":460,"wires":[["860a6512ff99ab69"],["80567e592f64f4df"]]},{"id":"d2d66c16c4bcc38a","type":"ui-control","z":"deee9af5aea3c97a","name":"","ui":"60061a41144c1c54","events":"all","x":720,"y":460,"wires":[[]]},{"id":"860a6512ff99ab69","type":"change","z":"deee9af5aea3c97a","name":"Show Admin Page","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"pages\":{\"show\":[\"Admin View\"]}}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":530,"y":440,"wires":[["d2d66c16c4bcc38a"]]},{"id":"80567e592f64f4df","type":"change","z":"deee9af5aea3c97a","name":"Hide Admin Page","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"pages\":{\"hide\":[\"Admin View\"]}}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":530,"y":480,"wires":[["d2d66c16c4bcc38a"]]},{"id":"b1f4e52a9025e398","type":"inject","z":"deee9af5aea3c97a","name":"","props":[],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","x":710,"y":80,"wires":[["f1249167a24acf65"]]},{"id":"f1249167a24acf65","type":"change","z":"deee9af5aea3c97a","name":"","rules":[{"t":"set","p":"admins","pt":"global","to":"[\"admin-user\"]","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":890,"y":80,"wires":[[]]},{"id":"6628cecfbdacf378","type":"ui-gauge","z":"deee9af5aea3c97a","name":"","group":"22b17110c0d4f034","order":0,"width":"3","height":"3","gtype":"gauge-34","gstyle":"rounded","title":"Office Temperature","units":"Temperature","icon":"","prefix":"","suffix":"°C","segments":[{"from":"0","color":"#98ece6"},{"from":"14","color":"#28d794"},{"from":"17.5","color":"#80d42b"},{"from":"22.5","color":"#ff9e42"}],"min":0,"max":"35","sizeThickness":16,"sizeGap":4,"sizeKeyThickness":8,"styleRounded":true,"styleGlow":false,"className":"","x":670,"y":620,"wires":[]},{"id":"146e2455d5ea5457","type":"change","z":"deee9af5aea3c97a","name":"map .temperature","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.temperature","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":330,"y":620,"wires":[["a582a9e4684253b9"]]},{"id":"a582a9e4684253b9","type":"function","z":"deee9af5aea3c97a","name":"1 dp","func":"msg.payload = msg.payload.toFixed(1)\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":490,"y":620,"wires":[["6628cecfbdacf378"]]},{"id":"817e3b341f4b02c6","type":"debug","z":"deee9af5aea3c97a","name":"debug 2","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":300,"y":340,"wires":[]},{"id":"a47c7c63f83a2979","type":"ui-template","z":"deee9af5aea3c97a","group":"6541a2e358dbd499","page":"","ui":"","name":"CO2 Vertical Level","order":0,"width":0,"height":0,"head":"","format":"<template>\n    <div>\n        <template v-if=\"type === 'round'\">\n            <div ref=\"hng\" class=\"round-led-level\" :style=\"`--size:${size}; --shadow:${shadow}; --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 class=\"round-led-level-graph\">\n                    <div class=\"round-led-level-stripe\" :class=\"{'led-level-flat': flat }\">\n                        <div v-for=\"(color, index) in colors\" :key=\"index\" class=\"round-led-level-led\"\n                            :ref=\"'dot-' + index\">\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        <template v-if=\"type === 'linear'\">\n            <div ref=\"hng\" class=\"led-level\" :style=\"`--shadow:${shadow};`\">\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=\"{'led-level-flat': flat }\">\n                    <div v-for=\"(color, index) in colors\" :key=\"index\" class=\"led-level-led\" :ref=\"'dot-' + index\">\n                    </div>\n                </div>\n                <div class=\"led-level-limits\">\n                    <span>{{min}}</span>\n                    <span>{{max}}</span>\n                </div>\n                <div>\n        </template>\n        <template v-if=\"type === 'vertical'\">\n            <div ref=\"hng\" class=\"led-level-vertical\" :style=\"`--shadow:${shadow}; --size:${size};`\">\n                <header>\n                    <div class=\"round-led-level-text\">\n                        <span class=\"round-led-level-label\">{{label}}</span>\n                    </div>\n                </header>\n                <div class=\"led-level-vertical-content\">\n                    <div class=\"led-level-stripe\" :class=\"{'led-level-flat': flat }\">\n                        <div v-for=\"(color, index) in colors\" :key=\"index\" class=\"led-level-led\" :ref=\"'dot-' + index\">\n                        </div>\n                    </div>\n                    <div class=\"led-level-limits\">\n                        <span>{{max}}</span>\n                        <span>{{min}}</span>\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>\n            </div>\n\n        </template>\n        <template v-if=\"type === 'artless'\">\n            <div ref=\"hng\" :class=\"icon ? 'ag-wrapper-2' : 'ag-wrapper-1'\" :style=\"`--line-color:${colors[0]};`\">\n                <div v-if=\"icon\" class=\"ag-icon\">\n                    <v-icon aria-hidden=\"false\">{{icon}}</v-icon>\n                </div>\n                <div class=\"ag-content\">\n                    <div class=\"ag-text\">\n                        <span class=\"ag-label\">{{label}}</span>\n                        <span class=\"ag-value\">{{formattedValue}}<span class=\"ag-unit\">{{unit}}</span></span>\n                    </div>\n                    <div class=\"ag-track\" ref=\"agLine\">\n                        <div class=\"ag-track-background\"></div>\n                        <div class=\"ag-track-foreground\" :style=\"{'width': percentage +'%'}\"></div>\n                    </div>\n                    <div class=\"ag-limits\">\n                        <span class=\"ag-min\">{{min}}</span>\n                        <span class=\"ag-max\">{{max}}</span>\n                    </div>\n                </div>\n            </div>\n        </template>\n    </div>\n</template>\n\n\n\n<script>\n    export default {\n    data(){\n        return {\n            //Define me here\n            type: \"vertical\", // Gauge type. \"artless\", \"linear\", \"vertical\" or \"round\"                                          \n            label: \"CO2 Concentration\", // The label\n            icon: \"mdi-molecule-co2\", // (type: artless) (optional) the icon\n            min: 0, // Smallest expected value\n            max: 1200, // Highest expected value\n            unit: \"ppm\",// The unit of the measurement\n            dim: 0.3, //(type: round, linear, vertical) How dim is led when not glowing\n            shadow: 0, //(type: round, linear, vertical) Led shadow intensity (too much makes graphics muddy, 0 removes shadows)\n            filterFunction: \"brightness\", // (type: round, linear, vertical) \"brightness\" for dark themes, \"opacity\" for light themes  \n            animate: true, // Animating led's is not most performant thing in the world.                          \n            \n            // Define colors\n\n            // For type \"round\", \"vertical\" and \"linear\" the count of colors equals count of led's.\n            // For type \"artless\" the line changes color based on percentage of value turned index of colors array.  \n            // For type \"round\" the led size depends on how many colors is defined. About 20 is optimal.\n            // Color can be defined as:\n            // HEX - \"#FF00FF\" \n            // RGB - rgb(0,65,88)\n            // named color - \"red\"\n            // or depend on some defined CSS variable      \n            colors:[\"#0fb60f\",\n                    \"#0fb60f\",\n                    \"#0fb60f\",\n                    \"#0fb60f\",\n                    \"#0fb60f\",\n                    \"#0fb60f\",\n                    \"#0fb60f\",\n                    \"#0fb60f\",\n                    \"#0fb60f\",\n                    \"#0fb60f\",\n                    \"#0fb60f\",\n                    \"orange\",\n                    \"orange\",\n                    \"orange\",\n                    \"red\",\n                    \"red\",\n                   ],            \n            \n            //no need to change those\n            value:0,\n            previousValue:0,\n            size:100,           \n            inited:false\n        }\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        validate(data){\n            let ret\n            if(typeof data !== \"number\"){\n                ret = parseFloat(data)\n                if(isNaN(ret)){\n                    console.log(\"BAD DATA! gauge type:\",this.type, \"id:\",this.id,\"data:\",data)\n                    return null\n                }   \n            }\n            else{\n                ret = data\n            }            \n            return ret\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.dim;\n            return p;\n        },\n        filter: function(amount){\n            let f\n            switch(amount){\n                case \"full\":{\n                    f = this.filterFunction == \"brightness\" ? \"brightness(1.1)\" : \"opacity(1)\";\n                    break\n                }\n                case \"half\":{\n                    f = this.filterFunction == \"brightness\" ? \"brightness(\" +this.half()+\")\" : \"opacity(\" +this.half()+\")\";\n                    break\n                }\n                default:{\n                    f = this.filterFunction == \"brightness\" ? \"brightness(\" +this.dim+\")\"  : \"opacity(\" +this.dim+\")\";\n                    break\n                }\n            }\n            return f\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((color,i) => {\n                let dot = this.getElement(\"dot-\"+i);\n                if(!dot){\n                    console.log(\"lit() no dots found\")\n                    return\n                }                \n                if(i<this.full()){\n                    dot.style.filter=this.filter(\"full\");                   \n                }\n                else if(i==this.full()){\n                    dot.style.filter= this.filter(\"half\");                   \n                }\n                else{\n                    dot.style.filter= this.filter(\"dim\");                   \n                }\n                if(down){\n                    time = (this.colors.length - i) * .12                    \n                }else{\n                    time = i * 0.08\n                }\n                dot.style.transition = this.animate ? \"filter \"+time+\"s\" : \"unset\";\n            })\n            this.previousValue = this.value\n        },\n        changeLine:function(){\n            const line = this.getElement(\"agLine\",true);\n            if(!line){\n                console.log(\"no line found\")\n                return            \n            }\n            let c = Math.floor(this.colors.length * this.percentage / 100)\n            if(c >= this.colors.length){\n                c = this.colors.length - 1\n            }\n            line.style.setProperty('--line-color',this.colors[c])\n\n        },\n        onResize:function(){            \n            let g = this.getElement(\"hng\",true)\n            if(!g){\n                return\n            }           \n            this.$nextTick(() => {\n                let last = this.size \n                let changed = this.type == \"vertical\" ? g.clientHeight : g.clientWidth;   \n                if(Math.abs(last - changed) < 3){\n                    return\n                }\n                this.size = changed\n                g.style.setProperty('--size',this.size);            \n                if(this.type == \"round\"){\n                    this.updateLayout()\n                }\n            })\n                              \n        },\n        updateLayout:function(){\n            let angle;\n            const step = 270 / this.colors.length;\n            const radius = (this.size - (this.size*0.1))/2\n            const s = this.ledSize / -2;\n            const outline = this.filterFunction == \"opacity\" ? \"black\" : \"white\";        \n            this.colors.forEach((c,i) => {\n                    let dot = this.getElement(\"dot-\"+i);\n                    if(!dot){\n                        console.log(\"round init()  no dots found\")\n                        return\n                    }\n                    dot.style.backgroundColor = c\n                    dot.style.outlineColor=\"color-mix(in srgb, \"+c+\", \"+outline+\" 35%)\";\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    },\n    watch: {\n        msg: function(){    \n            if(this.msg.payload !== undefined){  \n                const v = this.validate(this.msg.payload)                \n                if(v === null){\n                    return\n                }         \n                this.value = v\n                if(this.type != \"artless\"){\n                    this.lit()\n                }\n                else{\n                    this.changeLine()\n                } \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        ledSize:function(){\n            const s = 4.71239 * ((this.size - (this.size*0.3))/2)            \n            return s / this.colors.length\n        },\n        flat:function(){\n            return this.shadow == 0\n        }\n\n    },\n    mounted(){\n        const outline = this.filterFunction == \"opacity\" ? \"black\" : \"white\";\n        if(this.type == \"round\"){\n            let g = this.getElement(\"hng\",true)\n            if(!g){\n                return\n            }            \n            this.resizeObserver = new ResizeObserver((entries) => {\n                this.onResize()\n            });\n            this.resizeObserver.observe(g);           \n            \n            setTimeout(()=>{\n                this.onResize()\n            },20)\n        }\n        else if(this.type == \"linear\"){\n            \n            this.colors.forEach((c,i) => {\n                    let dot = this.getElement(\"dot-\"+i);\n                    if(!dot){\n                        console.log(\"linear init() no dots found\")\n                        return\n                    }\n                    dot.style.outlineColor=\"color-mix(in srgb, \"+c+\", \"+outline+\" 35%)\";\n                    dot.style.backgroundColor = c               \n                    dot.style.transition = \"filter 0.1s\";\n                }\n            )\n        }\n        else if(this.type == \"vertical\"){\n            let g = this.getElement(\"hng\",true)\n            if(!g){\n                return\n            }\n            this.resizeObserver = new ResizeObserver((entries) => {\n                    this.onResize()\n                });\n                this.resizeObserver.observe(g);                \n                setTimeout(()=>{\n                this.onResize()\n            },20)\n            this.colors.forEach((c,i) => {\n                let dot = this.getElement(\"dot-\"+i);\n                if(!dot){\n                    console.log(\"linear init() no dots found\")\n                    return\n                }\n                dot.style.outlineColor=\"color-mix(in srgb, \"+c+\", \"+outline+\" 35%)\";\n                dot.style.backgroundColor = c\n                dot.style.transition = \"filter 0.1s\";\n            })\n        }\n        else if(this.type == \"artless\"){\n            const line = this.getElement(\"agLine\",true);\n            line.style.setProperty('--line-color',this.colors[0])\n            if(this.animate == true){                \n                if(!line){\n                    console.log(\"artless init() no line found\")\n                    return\n                }\n                line.style.transition = \"width 0.5s\";\n            }\n        }        \n       \n        this.inited = true;\n    },\n    unmounted () {\n        if(this.resizeObserver){\n            this.resizeObserver.disconnect();\n            this.resizeObserver = null;            \n        }\n    }\n}\n</script>","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":670,"y":720,"wires":[[]]},{"id":"80943859cf0f917b","type":"ui-template","z":"deee9af5aea3c97a","group":"","page":"","ui":"60061a41144c1c54","name":"Artless CSS","order":0,"width":0,"height":0,"head":"","format":".led-level {\n    display: grid;\n    grid-template-rows: 1.3em minmax(3px, 1fr) .7em;\n    gap: 2px;\n    height: 100%;\n}\n\n.led-level-stripe {\n    display: flex;\n    gap: 2px;\n}\n\n.led-level-led {\n    --s: var(--shadow, 0.2);\n    --shadowColor: rgba(0, 0, 0, var(--s));\n    background: #ffffff;\n    width: 100%;\n    height: 100%;\n    outline: 1px solid;\n    outline-offset: -1px;\n    border-radius: 0px;\n    box-shadow: inset 0px 0px 10px 0px var(--shadowColor), 0px 0px 3px 0px var(--shadowColor);\n    filter: brightness(0.4);\n}\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\n.led-level-value {\n    font-weight: bold;\n}\n\n.led-level-unit {\n    font-size: .75em;\n    font-weight: normal;\n    padding-inline-start: 0.15em;\n}\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}\n\n.round-led-level,\n.led-level-vertical {\n    display: grid;\n    grid-template-rows: 1em 1fr;\n    width: 100%;\n    height: 100%;\n    aspect-ratio: 1/1;\n    position: relative;\n    margin: auto;\n}\n\n.round-led-level-graph {\n    position: relative;\n    aspect-ratio: 1;\n}\n\n.round-led-level-stripe {\n    display: block;\n    position: absolute;\n    left: 50%;\n    top: 56%;\n    rotate: 135deg;\n}\n\n.round-led-level-led {\n    --s: var(--shadow, 0.2);\n    --shadowColor: rgba(0, 0, 0, var(--s));\n    background: #ffffff;\n    position: absolute;\n    width: calc(var(--ledsize) * 1px);\n    aspect-ratio: 1/1;\n    border-radius: 4px;\n    outline: 1px solid;\n    outline-offset: -1px;\n    box-shadow: inset 0px 0px calc(var(--ledsize) / 3 * 1px) 0px var(--shadowColor), 0px 0px calc(var(--ledsize) / 7 * 1px) 0px var(--shadowColor);\n    filter: brightness(0.4);\n    transform-origin: center center;\n}\n\n.round-led-level-text {\n    font-size: clamp(0.5em, calc(var(--size) * .1 * 1px), 1.25em);\n    line-height: 1rem;\n    text-align: center;\n    user-select: none;\n    white-space: nowrap;\n}\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: 1.5fr 1fr;\n    gap: 0.1em;\n    user-select: none;\n    align-items: center;\n}\n\n.round-led-level-value {\n    font-weight: bold;\n    font-size: calc(var(--size) * .15 * 1px);\n    align-self: end;\n}\n\n.round-led-level-unit {\n    font-size: calc(var(--size) * .1 * 1px);\n    font-weight: normal;\n    align-self: start;\n    padding-inline-start: 0.15em;\n}\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}\n\n.ag-wrapper-2 {\n    display: grid;\n    grid-template-columns: 3em 1fr;\n    gap: 1em;\n}\n\n.ag-wrapper-1 {\n    display: grid;\n    grid-template-columns: 1fr;\n}\n\n.ag-icon {\n    font-size: 2em;\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n}\n\n.ag-content {\n    display: grid;\n    grid-template-rows: 1fr 7px 0.75em;\n    gap: 2px;\n}\n\n.ag-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\n.ag-value {\n    font-weight: bold;\n}\n\n.ag-unit {\n    font-size: .75em;\n    font-weight: normal;\n    padding-inline-start: 0.15em;\n}\n\n.ag-limits {\n    display: flex;\n    justify-content: space-between;\n    font-size: .75em;\n    line-height: .75em;\n    align-content: center;\n    flex-wrap: wrap;\n    user-select: none;\n}\n\n.ag-track {\n    position: relative;\n    display: flex;\n    align-items: center;\n    width: 100%;\n    border-radius: 6px;\n}\n\n.ag-track-background {\n    position: absolute;\n    background: var(--line-color, rgb(var(--v-theme-primary)));\n    opacity: 0.45;\n    width: 100%;\n    height: 50%;\n    border-radius: inherit;\n}\n\n.ag-track-foreground {\n    position: absolute;\n    background-color: var(--line-color, rgb(var(--v-theme-primary)));\n    width: 50%;\n    height: 100%;\n    max-width: 100%;\n    border-radius: inherit;\n    transition: inherit;\n}\n\n.led-level-vertical {\n    gap: calc(var(--size) * .1 * 1px);\n    aspect-ratio: var(--aspect-ratio);\n}\n\n.led-level-vertical-content {\n    display: grid;\n    grid-template-columns: 1fr auto 4fr;\n}\n\n.led-level-vertical .round-led-level-centered-text {\n    grid-template-rows: 2.5fr 1fr;\n    padding-inline-start: 1em;\n}\n\n.led-level-vertical .round-led-level-value {\n    font-size: calc(var(--size) * .2 * 1px);\n}\n\n.led-level-vertical .led-level-stripe {\n    flex-direction: column-reverse;\n}\n\n.led-level-vertical .led-level-limits {\n    display: flex;\n    justify-content: space-between;\n    font-size: calc(var(--size) * .075 * 1px);\n    line-height: calc(var(--size) * .075 * 1px);\n    align-content: flex-start;\n    align-items: start;\n    flex-wrap: wrap;\n    user-select: none;\n    flex-direction: column;\n    padding-inline-start: 0.4em;\n}\n\n.led-level-flat div {\n    box-shadow: unset;\n    min-height: 12px;\n}","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"site:style","className":"","x":650,"y":760,"wires":[[]]},{"id":"0667790c888ead2f","type":"change","z":"deee9af5aea3c97a","name":"map .co2Concentration","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.co2Concentration","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":350,"y":720,"wires":[["a47c7c63f83a2979"]]},{"id":"5b77808caf572572","type":"debug","z":"deee9af5aea3c97a","name":"debug 3","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":300,"y":660,"wires":[]},{"id":"7c13e5015b7c28c0","type":"comment","z":"deee9af5aea3c97a","name":"Define Admins Here","info":"","x":730,"y":40,"wires":[]},{"id":"4615b9d913078624","type":"comment","z":"deee9af5aea3c97a","name":"Form Entry","info":"","x":120,"y":120,"wires":[]},{"id":"f5c2b7111eeb7ab2","type":"ui-group","name":"Group Name","page":"1ac8f0dc6af762b8","width":"6","height":"1","order":-1,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"60061a41144c1c54","type":"ui-base","name":"Dashboard","path":"/dashboard","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"navigationStyle":"icon"},{"id":"0d8bb4422190121d","type":"ui-group","name":"Task List","page":"abf2f56d637dd7e2","width":"12","height":"1","order":-1,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"b362eb01162ac195","type":"ui-group","name":"Tasks List","page":"9ddbd8b53184555c","width":"12","height":"1","order":-1,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"22b17110c0d4f034","type":"ui-group","name":"Raspberry Pi Data","page":"621b72a71b7fea3c","width":"3","height":"1","order":-1,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"6541a2e358dbd499","type":"ui-group","name":"CO2 Levels","page":"621b72a71b7fea3c","width":"4","height":"1","order":-1,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"1ac8f0dc6af762b8","type":"ui-page","name":"Task Submission","ui":"60061a41144c1c54","path":"/submit-data","icon":"form-textbox","layout":"notebook","theme":"5ae8613757b62f89","order":1,"className":"","visible":"true","disabled":"false"},{"id":"abf2f56d637dd7e2","type":"ui-page","name":"Admin View","ui":"60061a41144c1c54","path":"/admin","icon":"lock","layout":"grid","theme":"a70602e73bbb2a95","order":3,"className":"","visible":false,"disabled":"false"},{"id":"9ddbd8b53184555c","type":"ui-page","name":"Your Tasks","ui":"60061a41144c1c54","path":"/tasks","icon":"view-list-outline","layout":"grid","theme":"5ae8613757b62f89","order":2,"className":"","visible":"true","disabled":"false"},{"id":"621b72a71b7fea3c","type":"ui-page","name":"Data Dashboard","ui":"60061a41144c1c54","path":"/data","icon":"gauge","layout":"grid","theme":"5ae8613757b62f89","order":-1,"className":"","visible":"true","disabled":"false"},{"id":"5ae8613757b62f89","type":"ui-theme","name":"Theme Name","colors":{"surface":"#ffffff","primary":"#0094ce","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}},{"id":"a70602e73bbb2a95","type":"ui-theme","name":"Admin Theme","colors":{"surface":"#454545","primary":"#d47e1c","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}}]

Flow Info

Created 1 year, 2 months ago
Rating: not yet rated

Owner

Actions

Rate:

Node Types

Core
  • change (x8)
  • comment (x3)
  • debug (x2)
  • function (x3)
  • inject (x1)
  • switch (x3)
Other

Tags

  • FlowFuse
  • Webinar
  • Dashboard2.0
Copy this flow JSON to your clipboard and then import into Node-RED using the Import From > Clipboard (Ctrl-I) menu option