node-red-custom-widget 1

Follow Creating your own UI widget for a Node-RED dashboard using an external charting library guide, create ui-widget with external source Table With Embedded Line Chart - D3 Example

ui-template code snippet

<!-- external libraries -->
<script src="http://d3js.org/d3.v3.min.js"></script> 
<link rel="stylesheet" href="http://billmill.org/css/style.css" /> 
<style>
 ...
</style>
<script>
;(function(scope) {

  scope.msgReady = ... // ng-if 
  
  var timer = setInterval(function() { //check that D3 libs are loaded, if not wait
    if (!window.d3) return;
    clearInterval(timer);
   
    scope.$watch('msg', function (msg) {
    if (msg) {
        
        d3.select("#datatable").selectAll("*").remove();  //  Clear D3 nodes
        var rows = msg.payload;
        ...
        d3.select // D3 libs
        ...
        
     } // if (msg) 
    }); // scope.$watch()
    
 }, 3000); // setInterval:3 secs for spinner demo
})(scope);
</script>

In Action

Flow

[{"id":"18d60280.ae51ce","type":"ui_template","z":"4ca6d44e.16a0ec","group":"1444eda7.0423a2","name":"D3 Chart Widget","order":0,"width":"16","height":"16","format":"<!-- external links -->\n<script src=\"http://d3js.org/d3.v3.min.js\"></script> \n<link rel=\"stylesheet\" href=\"http://billmill.org/css/style.css\" /> \n<style>\n<!-- D3 table style --> \ntable {\n  border-collapse: collapse;\n}\nth {\n  border-bottom: 2px solid #ddd;\n  padding: 8px;\n  font-weight: bold;\n}\ntd {\n  padding: 8px;\n  border-top: 1px solid #ddd;\n}\n#chart {\n  padding: 0px;\n}\n.xaxislabel {\n  font-size: 9px;\n}\n\n<!--spinner style --> \n\n/* Center the loader */\n#loader {\n  position: absolute;\n  left: 50%;\n  top: 50%;\n  z-index: 1;\n  width: 150px;\n  height: 150px;\n  margin: -75px 0 0 -75px;\n  border: 16px solid #f3f3f3;\n  border-radius: 50%;\n  border-top: 16px solid #3498db;\n  width: 120px;\n  height: 120px;\n  -webkit-animation: spin 2s linear infinite;\n  animation: spin 2s linear infinite;\n}\n\n@-webkit-keyframes spin {\n  0% { -webkit-transform: rotate(0deg); }\n  100% { -webkit-transform: rotate(360deg); }\n}\n\n@keyframes spin {\n  0% { transform: rotate(0deg); }\n  100% { transform: rotate(360deg); }\n}\n\n/* Add animation to \"page content\" */\n.animate-bottom {\n  position: relative;\n  -webkit-animation-name: animatebottom;\n  -webkit-animation-duration: 1s;\n  animation-name: animatebottom;\n  animation-duration: 1s\n}\n\n@-webkit-keyframes animatebottom {\n  from { bottom:-100px; opacity:0 } \n  to { bottom:0px; opacity:1 }\n}.spinner {\n  margin: 100px auto;\n  width: 50px;\n  height: 40px;\n  text-align: center;\n  font-size: 10px;\n}\n\n.spinner > div {\n  background-color: #333;\n  height: 100%;\n  width: 6px;\n  display: inline-block;\n  \n  -webkit-animation: sk-stretchdelay 1.2s infinite ease-in-out;\n  animation: sk-stretchdelay 1.2s infinite ease-in-out;\n}\n\n.spinner .rect2 {\n  -webkit-animation-delay: -1.1s;\n  animation-delay: -1.1s;\n}\n\n.spinner .rect3 {\n  -webkit-animation-delay: -1.0s;\n  animation-delay: -1.0s;\n}\n\n.spinner .rect4 {\n  -webkit-animation-delay: -0.9s;\n  animation-delay: -0.9s;\n}\n\n.spinner .rect5 {\n  -webkit-animation-delay: -0.8s;\n  animation-delay: -0.8s;\n}\n\n@-webkit-keyframes sk-stretchdelay {\n  0%, 40%, 100% { -webkit-transform: scaleY(0.4) }  \n  20% { -webkit-transform: scaleY(1.0) }\n}\n\n@keyframes sk-stretchdelay {\n  0%, 40%, 100% { \n    transform: scaleY(0.4);\n    -webkit-transform: scaleY(0.4);\n  }  20% { \n    transform: scaleY(1.0);\n    -webkit-transform: scaleY(1.0);\n  }\n}\n\n@keyframes animatebottom { \n  from{ bottom:-100px; opacity:0 } \n  to{ bottom:0; opacity:1 }\n}\n</style>\n<h3 align=\"center\">Table With Embedded Line Chart </h3>\n<div id=\"datatable\" ng-if=\"msgReady\" ng-style=\"msgStyle\" ></div>\n<div class=\"spinner\"  ng-if=\"!msgReady\">\n  <div class=\"rect1\"></div>\n  <div class=\"rect2\"></div>\n  <div class=\"rect3\"></div>\n  <div class=\"rect4\"></div>\n  <div class=\"rect5\"></div>\n</div>\n\n<script>\n\n;(function(scope) {\n    \n  scope.msgReady = false;\n  scope.msgStyle = { 'height':'700px' };\n \n  \n  var timer = setInterval(function() { //check that D3 libs are loaded, if not wait\n    if (!window.d3) return;\n    clearInterval(timer);\n    \n    scope.msgReady = true;    \n    scope.$watch('msg', function (msg) { //watch for an incoming NR msg\n \n    if (msg) {\n        \n        d3.select(\"#datatable\").selectAll(\"*\").remove();\n        \n        var rows = msg.payload;\n \n        var table = d3.select(\"#datatable\").append(\"table\");\n            thead = table.append(\"thead\");\n            tbody = table.append(\"tbody\");\n\n        thead.append(\"th\").text(\"Date\");\n        thead.append(\"th\").text(\"Opponent\");\n        thead.append(\"th\").text(\"Result\");\n        thead.append(\"th\").text(\"Rating\");\n        thead.append(\"th\").text(\"\");\n\n        var tr = tbody.selectAll(\"tr\")\n                 .data(rows)\n                 .enter().append(\"tr\");\n\n        var td = tr.selectAll(\"td\")\n                .data(function(d) { return [d.dt, d.opp, d.result, d.mu]; })\n                .enter().append(\"td\")\n                .text(function(d) { return d; });\n\n        var width = 80,mx = 10, radius = 2,\n            height = d3.select(\"table\")[0][0].clientHeight;\n\n        // Now add the chart column\n        d3.select(\"#datatable tbody tr\").append(\"td\")\n          .attr(\"id\", \"chart\")\n          .attr(\"width\", width + \"px\")\n          .attr(\"rowspan\", rows.length);\n\n        var chart = d3.select(\"#chart\").append(\"svg\")\n                   .attr(\"class\", \"chart\")\n                   .attr(\"width\", width)\n                   .attr(\"height\", height);\n\n        var maxMu = 0;\n        var minMu = Number.MAX_VALUE;\n        for (i=0; i < rows.length; i++) {\n            if (rows[i].mu > maxMu) { maxMu = rows[i].mu; }\n            if (rows[i].mu < minMu) { minMu = rows[i].mu; }\n        }\n\n        var dates = rows.map(function(t) { return t.dt; });\n\n        var xscale = d3.scale.linear()\n                       .domain([minMu, maxMu])\n                       .range([mx, width-mx])\n                       .nice();\n\n        var yscale = d3.scale.ordinal()\n                       .domain(dates)\n                       .rangeBands([0, height]);\n\n        chart.selectAll(\".xaxislabel\")\n             .data(xscale.ticks(2))\n             .enter().append(\"text\")\n             .attr(\"class\", \"xaxislabel\")\n             .attr(\"x\", function(d) { return xscale(d); })\n             .attr(\"y\", 10)\n             .attr(\"text-anchor\", \"middle\")\n             .text(String)\n\n        chart.selectAll(\".xaxistick\")\n             .data(xscale.ticks(2))\n             .enter().append(\"line\")\n             .attr(\"x1\", function(d) { return xscale(d); })\n             .attr(\"x2\", function(d) { return xscale(d); })\n             .attr(\"y1\", 10)\n             .attr(\"y2\", height)\n             .attr(\"stroke\", \"#eee\")\n             .attr(\"stroke-width\", 1);\n\n        chart.selectAll(\".line\")\n             .data(rows)\n             .enter().append(\"line\")\n             .attr(\"x1\", function(d) { return xscale(d.mu); })\n             .attr(\"y1\", function(d) { return yscale(d.dt) + yscale.rangeBand()/2; })\n             .attr(\"x2\", function(d,i) { return rows[i+1] ? xscale(rows[i+1].mu) : xscale(d.mu); })\n             .attr(\"y2\", function(d,i) { return rows[i+1] ? yscale(rows[i+1].dt) + yscale.rangeBand()/2 : yscale(d.dt) + yscale.rangeBand()/2; })\n             .attr(\"stroke\", \"#777\")\n             .attr(\"stroke-width\", 1);\n\n        var pt = chart.selectAll(\".pt\")\n                      .data(rows)\n                      .enter().append(\"g\")\n                      .attr(\"class\", \"pt\")\n                      .attr(\"transform\", function(d) { return \"translate(\" + xscale(d.mu) + \",\" + (yscale(d.dt) + yscale.rangeBand()/2) + \")\"; });\n\n        pt.append(\"circle\")\n          .attr(\"cx\", 0)\n          .attr(\"cy\", 0)\n          .attr(\"r\", radius)\n          .attr(\"opacity\", .5)\n          .attr(\"fill\", \"#ff0000\");\n\n    } // if\n  }); // watch\n\n\n }, 3000); // close out the setInterval \n           // 3 secs for spinner demo\n\n})(scope);\n\n</script>","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":602.3003234863281,"y":356.8004741668701,"wires":[[]]},{"id":"8f387961.25fc58","type":"inject","z":"4ca6d44e.16a0ec","name":"","topic":"","payload":"","payloadType":"date","repeat":"3","crontab":"","once":true,"onceDelay":0.1,"x":152.3000030517578,"y":359.066725730896,"wires":[["d3ce3d90.6b48f"]]},{"id":"d3ce3d90.6b48f","type":"function","z":"4ca6d44e.16a0ec","name":"random csv","func":"\n\nvar msg = {};\nmsg.topic = \"Line Chart\";\n\nvar count = Math.floor(Math.random() * 2);\n\nswitch( count ) {\n\ncase 0 :\nmsg.payload ='mu,sigma,opp,result,date\\n'+\n'1662.30921255,290.31746003,FC Augsburg,1,2013-08-10T00:00:00\\n'+\n'1710.31516965,260.486532706,TSV Eintracht Braunschweig,1,2013-08-18T00:00:00\\n'+\n'1833.29188401,225.673906004,Werder Bremen,1,2013-08-23T00:00:00\\n'+\n'1857.5300504,214.245325191,Eintracht Frankfurt,1,2013-09-01T00:00:00\\n'+\n'1875.73400724,205.674370682,Hamburg SV,1,2013-09-14T00:00:00\\n'+\n'1784.18071145,185.716271567,Napoli,0,2013-09-18T00:00:00\\n'+\n'1718.35291449,179.834040342,Nurnberg,0.5,2013-09-21T00:00:00\\n'+\n'1740.98858679,171.270511287,SC Freiburg,1,2013-09-28T00:00:00\\n'+\n'1786.58260116,157.720707589,Marseille,1,2013-10-01T00:00:00\\n'+\n'1693.24904256,150.534136974,Borussia Monchengladbach,0,2013-10-05T00:00:00\\n'+\n'1730.8689396,141.113609063,Hannover 96,1,2013-10-19T00:00:00\\n'+\n'1796.86111973,134.157129861,Arsenal,1,2013-10-22T00:00:00\\n'+\n'1815.9808198,129.14679898,Schalke 04,1,2013-10-26T00:00:00\\n'+\n'1827.35641274,126.152133747,VfB Stuttgart,1,2013-11-01T00:00:00\\n'+\n'1792.11402645,120.045882423,Arsenal,0,2013-11-06T00:00:00';\nbreak;\ncase 1 :\nmsg.payload ='mu,sigma,opp,result,date\\n'+\n'1762.30921255,290.31746003,FC Augsburg,1,2013-08-10T00:00:00\\n'+\n'1719.31516965,260.486532706,TSV Eintracht Braunschweig,1,2013-08-18T00:00:00\\n'+\n'1633.29188401,225.673906004,Werder Bremen,1,2013-08-23T00:00:00\\n'+\n'1897.5300504,214.245325191,Eintracht Frankfurt,1,2013-09-01T00:00:00\\n'+\n'1775.73400724,205.674370682,Hamburg SV,1,2013-09-14T00:00:00\\n'+\n'1884.18071145,185.716271567,Napoli,0,2013-09-18T00:00:00\\n'+\n'1818.35291449,179.834040342,Nurnberg,0.5,2013-09-21T00:00:00\\n'+\n'1840.98858679,171.270511287,SC Freiburg,1,2013-09-28T00:00:00\\n'+\n'1686.58260116,157.720707589,Marseille,1,2013-10-01T00:00:00\\n'+\n'1793.24904256,150.534136974,Borussia Monchengladbach,0,2013-10-05T00:00:00\\n'+\n'1830.8689396,141.113609063,Hannover 96,1,2013-10-19T00:00:00\\n'+\n'1896.86111973,134.157129861,Arsenal,1,2013-10-22T00:00:00\\n'+\n'1615.9808198,129.14679898,Schalke 04,1,2013-10-26T00:00:00\\n'+\n'1727.35641274,126.152133747,VfB Stuttgart,1,2013-11-01T00:00:00\\n'+\n'1892.11402645,120.045882423,Arsenal,0,2013-11-06T00:00:00';\nbreak;\ndefault :\nmsg.payload ='mu,sigma,opp,result,date\\n'+\n'1682.30921255,290.31746003,FC Augsburg,1,2013-08-10T00:00:00\\n'+\n'1810.31516965,260.486532706,TSV Eintracht Braunschweig,1,2013-08-18T00:00:00\\n'+\n'1733.29188401,225.673906004,Werder Bremen,1,2013-08-23T00:00:00\\n'+\n'1657.5300504,214.245325191,Eintracht Frankfurt,1,2013-09-01T00:00:00\\n'+\n'1775.73400724,205.674370682,Hamburg SV,1,2013-09-14T00:00:00\\n'+\n'1884.18071145,185.716271567,Napoli,0,2013-09-18T00:00:00\\n'+\n'1818.35291449,179.834040342,Nurnberg,0.5,2013-09-21T00:00:00\\n'+\n'1640.98858679,171.270511287,SC Freiburg,1,2013-09-28T00:00:00\\n'+\n'1786.58260116,157.720707589,Marseille,1,2013-10-01T00:00:00\\n'+\n'1893.24904256,150.534136974,Borussia Monchengladbach,0,2013-10-05T00:00:00\\n'+\n'1730.8689396,141.113609063,Hannover 96,1,2013-10-19T00:00:00\\n'+\n'1796.86111973,134.157129861,Arsenal,1,2013-10-22T00:00:00\\n'+\n'1815.9808198,129.14679898,Schalke 04,1,2013-10-26T00:00:00\\n'+\n'1847.35641274,126.152133747,VfB Stuttgart,1,2013-11-01T00:00:00\\n'+\n'1792.11402645,120.045882423,Arsenal,0,2013-11-06T00:00:00';\n\nbreak;\n}\nreturn msg;","outputs":1,"noerr":0,"x":337.3001174926758,"y":342.866943359375,"wires":[["e66191ed.19ddc"]]},{"id":"e66191ed.19ddc","type":"function","z":"4ca6d44e.16a0ec","name":"csv2json","func":"\nvar csv = [];\nvar lines=msg.payload.split(\"\\n\");\nvar headers=lines[0].split(\",\");\nfor(var i=1;i<lines.length;i++){\n    var obj = {};\n    var currentline=lines[i].split(\",\");\n\tfor(var j=0;j<headers.length;j++)  obj[headers[j]] = currentline[j];\n\tcsv.push(obj);\n}\n\nvar result = [];\nfor(var i=1; i<csv.length;i++){\n  csv[i].mu = parseFloat(csv[i].mu).toFixed(1);\n  csv[i].sigma = parseFloat(csv[i].sigma).toFixed(1);\n  csv[i].dt = new Date(Date.parse(csv[i].date));\n\n  var res = parseFloat(csv[i].result);\n  if (res < .5) {\n    csv[i].result = \"loss\";\n  } else if (res > .5) {\n    csv[i].result = \"win\";\n  } else {\n    csv[i].result = \"draw\";\n  }\n  result.push(csv[i]);\n}\nmsg.payload = result;\n\nreturn msg;","outputs":1,"noerr":0,"x":429.3000793457031,"y":400.2669153213501,"wires":[["18d60280.ae51ce"]]},{"id":"1444eda7.0423a2","type":"ui_group","z":"","name":"","tab":"6bb5e46c.104aec","order":1,"disp":true,"width":"16","collapse":false},{"id":"6bb5e46c.104aec","type":"ui_tab","z":"","name":"UI Widget","icon":"dashboard","order":3}]

node-red-custom-widget 2 : Modal PIN Dialog

The original work : Security Pin Dialog UI Template for Node-Red-Dashboard by Daniel Lando

Flow Changes :

  • Lock Door -> Display a PIN dialog
  • Display a PIN dialog if pin_error
  • Display a PIN dialog if the session is expired

In Action

Flow

[{"id":"c6907ba1.f042a8","type":"ui_button","z":"24c9024.86a84fe","name":"","group":"5c14d060.b73e7","order":0,"width":0,"height":0,"passthru":false,"label":"Lock Door","color":"","bgcolor":"","icon":"","payload":"true","payloadType":"bool","topic":"show","x":270.0000915527344,"y":280.2501029968262,"wires":[["fbd1ef60.16855"]]},{"id":"d26fc943.b99f08","type":"function","z":"24c9024.86a84fe","name":"verify_pin","func":"var pins = [\"2136\"];\nvar verified = false;\n\nfor(var i=0;i<pins.length;i++){\n    if(msg.passcode == pins[i]){\n        verified = true;\n        break;\n    }\n}\n\nmsg.verified = verified;\n\nreturn msg;","outputs":1,"noerr":0,"x":513.6250953674316,"y":279.25004482269287,"wires":[["f3e13fa.ea9cac"]]},{"id":"f3e13fa.ea9cac","type":"switch","z":"24c9024.86a84fe","name":"check","property":"verified","propertyType":"msg","rules":[{"t":"true"},{"t":"false"}],"checkall":"false","repair":false,"outputs":2,"x":626.1251335144043,"y":389.25010681152344,"wires":[["8080576b.049568"],["c7ddbcb1.48df5"]]},{"id":"5a70443c.a2120c","type":"ui_toast","z":"24c9024.86a84fe","position":"top right","displayTime":"3","outputs":0,"ok":"OK","cancel":"","topic":"","name":"","x":988.1250839233398,"y":444.25010681152344,"wires":[]},{"id":"8080576b.049568","type":"function","z":"24c9024.86a84fe","name":"pin_ok","func":"var msg2 = {};\nmsg2.topic = \"Pin successfully verified!\";\nmsg2.payload = \"\";\n    \nvar start = Date.now();\n\nflow.set(\"session\", start);\n\nreturn [msg, msg2];","outputs":"2","noerr":0,"x":733.1250762939453,"y":270.25002670288086,"wires":[["356c9605.1471fa"],["5a70443c.a2120c"]]},{"id":"c7ddbcb1.48df5","type":"function","z":"24c9024.86a84fe","name":"pin_error","func":" msg.topic = \"Wrong PIN\";\n msg.payload = \"\";\n    \nreturn [msg, { topic : 'show', payload : true } ];","outputs":2,"noerr":0,"x":737.1251373291016,"y":544.2502136230469,"wires":[["5a70443c.a2120c"],["fbd1ef60.16855"]]},{"id":"fbd1ef60.16855","type":"ui_template","z":"24c9024.86a84fe","group":"5266d204.35555c","name":"Pin_Unlock","order":0,"width":"0","height":"0","format":"<div ng-init=\"init()\" id=\"pin_insert\" class=\"dialog\">\n    \n    <div class=\"dialog_content\">\n        \n        <div class=\"dialog_header\">\n            <!-- <span ng-click=\"closeDialog()\" class=\"close\">&times;</span> -->\n            <h2>PIN</h2>\n        </div>\n        \n        <div class=\"dialog_body\">\n\n           <div layout=\"row\" layout-align=\"center\">\n                <div class=\"number_placeholder\">\n                    {{passcode.substring(0, 1)}}\n                </div>\n                <div class=\"number_placeholder\">\n                    {{passcode.substring(1, 2)}}\n                </div>\n                <div class=\"number_placeholder\">\n                    {{passcode.substring(2, 3)}}\n                </div>\n                <div class=\"number_placeholder\">\n                    {{passcode.substring(3, 4)}}\n                </div>\n            </div>\n            \n            <div layout=\"column\" layout-align=\"center\" style=\"margin-top: 50px\">\n                <div layout=\"row\" layout-align=\"center\">\n                    <div class=\"number_box\">\n                        <md-button class=\"md-raised\" ng-click=\"add(1)\">1</md-button>\n                    </div>\n                    <div class=\"number_box\">\n                        <md-button class=\"md-raised\" ng-click=\"add(2)\">2</md-button>\n                    </div>\n                    <div class=\"number_box\">\n                        <md-button class=\"md-raised\" ng-click=\"add(3)\">3</md-button>\n                    </div>\n                </div>\n                 <div layout=\"row\" layout-align=\"center\">\n                    <div class=\"number_box\">\n                        <md-button class=\"md-raised\" ng-click=\"add(4)\">4</md-button>\n                    </div>\n                    <div class=\"number_box\">\n                        <md-button class=\"md-raised\" ng-click=\"add(5)\">5</md-button>\n                    </div>\n                    <div class=\"number_box\">\n                        <md-button class=\"md-raised\" ng-click=\"add(6)\">6</md-button>\n                    </div>\n                </div>\n                 <div layout=\"row\" layout-align=\"center\">\n                    <div class=\"number_box\">\n                        <md-button class=\"md-raised\" ng-click=\"add(7)\">7</md-button>\n                    </div>\n                    <div class=\"number_box\">\n                        <md-button class=\"md-raised\" ng-click=\"add(8)\">8</md-button>\n                    </div>\n                    <div class=\"number_box\">\n                        <md-button class=\"md-raised\" ng-click=\"add(9)\">9</md-button>\n                    </div>\n                </div>\n                 <div layout=\"row\" layout-align=\"center\">\n                    <div class=\"number_box\">\n                        <md-button class=\"md-raised\" ng-click=\"confirm()\">\n                            <ng-md-icon icon=\"done\" style=\"color:#fff;\"></ng-md-icon>\n                        </md-button>\n                    </div>\n                    <div class=\"number_box\">\n                        <md-button class=\"md-raised\" ng-click=\"add(0)\">0</md-button>\n                    </div>\n                    <div class=\"number_box\">\n                        <md-button class=\"md-raised\" ng-click=\"delete()\">\n                            <ng-md-icon icon=\"arrow_back\" style=\"color:#fff;\"></ng-md-icon>\n                        </md-button>\n                    </div>\n                </div>\n            </div> \n          \n        </div> <!--dialog_body-->\n    </div> <!--dialog_content-->\n</div>  <!--dialog-->\n\n\n<style>\n\n/* The Dialog (background) */\n.dialog {\n    display: none; /* Hidden by default */\n    position: fixed; /* Stay in place */\n    z-index: 9999; /* Sit on top */\n    left: 0;\n    top: 0;\n    width: 100%; /* Full width */\n    height: 100%; /* Full height */\n    overflow: auto; /* Enable scroll if needed */\n    background-color: rgb(0,0,0); /* Fallback color */\n    background-color: rgba(0,0,0,0.4); /* Black w/ opacity */\n    -webkit-transform: translateZ(0px);\n    -webkit-transform: translate3d(0,0,0);\n    -webkit-perspective: 1000;\n}\n\n.dialog_content {\n    position: absolute;\n    background-color: #fff;\n    margin-top: 10%;\n    margin-left: 40%;\n    padding: 0;\n    width: 345px;\n    box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19);\n    -webkit-animation-name: animatetop;\n    animation-name: animatetop;\n    animation-duration: 0.4s;\n}\n\n/* Media query for smartphones (to Fix?) */\n@media only screen and (min-device-width : 375px) and (max-device-width : 667px) { \n    .dialog_content {\n    margin-top: 5%;\n    margin-left: 5%;\n}\n}\n\n/* Add Animation */\n@-webkit-keyframes animatetop {\n    from {top: -300px; opacity: 0} \n    to {top: 0; opacity: 1}\n}\n\n@keyframes animatetop {\n    from {top: -300px; opacity: 0}\n    to {top: 0; opacity: 1}\n}\n\n/* Dialog Header */\n.dialog_header {\n    padding: 2px 16px;\n    background-color: #03A9F4;\n    color: white;\n}\n\n/* Dialog Body */\n.dialog_body {padding: 16px 16px;}\n\n/* The Close Button */\n.close {\n    color: #fff;\n    float: right;\n    font-size: 28px;\n    font-weight: bold;\n}\n\n.close:hover,\n.close:focus {\n    color: #1565C0;\n    text-decoration: none;\n    cursor: pointer;\n}\n\n/* __ */\n.number_placeholder{\n    width: 50px;\n    height: 34px;\n    margin: 10px;\n    font-size: 20pt;\n    text-align: center;\n    border-bottom: 1px solid black;\n}\n\n/* Number container */\n.number_box{\n    margin: 10px;\n}\n\n/* Buttons style */\n.md-button.md-default-theme.md-raised, .md-button.md-raised{\n    min-height: 50px;\n    min-width: 50px;\n    font-weight: bold;\n    margin: 0px 10px 10px 0px;\n    box-shadow: 4px 4px 6px 0 #dadada;\n    background-color: #29B6F6;\n    color: #fff;\n}\n\n.md-button.md-default-theme.md-raised:not([disabled]):hover, .md-button.md-raised:not([disabled]):hover{\n    background-color: #03A9F4;\n}\n\n.btn1 {\n  color : rgb(49, 46, 46);\n  background-color: rgba(255, 222, 121, 0.96);\n  border-radius: 10px 0 0 10px;\n  font-size: 16px;\n}\n\n.btn1:not([disabled]):hover {\n  background-color: rgba(107, 103, 91, 0.96);\n  color: white;\n}\n\n.btn1[disabled] {\n  color : rgb(187, 187, 187);\n  background-color: rgba(230, 230, 229, 0.96);\n}\n\n</style>\n\n<script>\n\n/**\n * pin_dialog.js\n * Node-Red UI template for Node-Red Dashboard. \n * Custom dialog that asks for a PIN to allow actions\n * Enjoy it :). \n * -- Daniel\n *\n *\n * @license The Unlicense, http://unlicense.org/\n * @version 0.1\n * @author  Daniel Lando, https://github.com/robertsLando\n * @updated 2017-03-08\n * @link    ----\n *\n *\n */\n\nvar dialog;\n\n/* ==== */\n(function(scope) {\n    \n    scope.passcode = \"\";\n    scope.payload = \"\";\n    scope.inited = false;\n    \n    scope.init = function() {\n        scope.passcode = \"\";\n        //Hide the md-panel\n        $('#pin_insert').parent().parent().css(\"display\", \"none\");\n        //This trick make it works on smartphones too :)\n        dialog = $('#pin_insert').detach();\n        dialog.appendTo(document.body);\n        \n        scope.showDialog();\n    }\n    \n    scope.showDialog = function() {\n        dialog.css(\"display\", \"block\");\n    }\n    \n    scope.closeDialog = function(){\n        dialog.css(\"display\", \"none\");\n    }\n    \n    scope.add = function(value) {\n        if(scope.passcode.length < 4) {\n            scope.passcode = scope.passcode + value;\n            if(scope.passcode.length == 4) {\n                console.log(\"The four digit code was entered\");\n                   \n            }\n        }\n    }\n \n    scope.delete = function() {\n        if(scope.passcode.length > 0) {\n            scope.passcode = scope.passcode.substring(0, scope.passcode.length - 1);\n        }\n    }\n    \n    scope.confirm = function() {\n        if(scope.passcode.length == 4) {\n            scope.send({passcode: scope.passcode, payload : scope.payload});\n            scope.closeDialog();\n            scope.passcode = \"\";\n            scope.payload = \"\";\n        }\n    }\n\n    scope.$watch('msg', function(data) {\n        if(data && data.topic){\n            switch(data.topic){\n               case \"show\":\n                   if(scope.inited){\n                        scope.payload = data.payload;\n                        scope.showDialog();\n                   }\n                   else\n                        scope.inited = true;\n                break;\n                case \"close\": \n                    scope.closeDialog(); \n                break;\n            }\n        }\n    });\n})(scope);\n\n</script>\n","storeOutMessages":false,"fwdInMessages":false,"templateScope":"local","x":406.37511444091797,"y":384.2143335342407,"wires":[["d26fc943.b99f08"]]},{"id":"356c9605.1471fa","type":"debug","z":"24c9024.86a84fe","name":"do_Something","active":true,"tosidebar":true,"console":false,"complete":"payload","x":972.8754043579102,"y":343.0001449584961,"wires":[]},{"id":"ab9dba5f.33f0e8","type":"comment","z":"24c9024.86a84fe","name":"ping_error triggers showdialog()","info":"","x":810.1666717529297,"y":638.0001573562622,"wires":[]},{"id":"b482cf1c.5d0e4","type":"comment","z":"24c9024.86a84fe","name":"Always display PIN dialog","info":"","x":403.1667175292969,"y":340.0000400543213,"wires":[]},{"id":"6576ab0c.489c34","type":"inject","z":"24c9024.86a84fe","name":"","topic":"","payload":"","payloadType":"date","repeat":"3","crontab":"","once":true,"onceDelay":0.1,"x":292.1666793823242,"y":683.2000637054443,"wires":[["939a219f.625c2"]]},{"id":"939a219f.625c2","type":"function","z":"24c9024.86a84fe","name":"Session Timeout > 10 secs","func":"\nvar start = flow.get(\"session\") || 0;\nvar timeOut = 10000; // 10 secs\n\nif ( start == 0 ) {\n    msg = [ null, { payload : 'in-active'}];\n} else {\n    var millis = Date.now() - start;\n    if ( millis > timeOut) { // 10 secs for \n        flow.set(\"session\", 0) ;\n        msg = [ { topic : 'show', payload : true}, { payload : 'terminated'}];\n    } else {\n       msg = [ null, { payload : 'active'}];\n       \n    }\n}\nreturn msg;","outputs":2,"noerr":0,"x":511.1666717529297,"y":683.3333358764648,"wires":[["fbd1ef60.16855"],["7c989aed.556ba4"]]},{"id":"7c989aed.556ba4","type":"debug","z":"24c9024.86a84fe","name":"TimeOut !!!","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","x":778.1666717529297,"y":691.2669258117676,"wires":[]},{"id":"180cffa8.069a5","type":"comment","z":"24c9024.86a84fe","name":"10 secs for this demo","info":"","x":510.1666564941406,"y":638.3333530426025,"wires":[]},{"id":"c3f2941c.a42b88","type":"comment","z":"24c9024.86a84fe","name":"To-do-List : Display Secure Dashboard","info":"","x":1029.1667022705078,"y":293.3333282470703,"wires":[]},{"id":"5c14d060.b73e7","type":"ui_group","z":"","name":"Secure","tab":"4ee5fa32.77b934","disp":true,"width":"6"},{"id":"5266d204.35555c","type":"ui_group","z":"","name":"pin","tab":"4ee5fa32.77b934","disp":false,"width":"1"},{"id":"4ee5fa32.77b934","type":"ui_tab","z":"","name":"Home","icon":"home","order":1}]

FYI