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"}}]