Scrolling Text box on dashboard?

As usual with Node-RED there are many possible ways to achieve what you want. Considering that you want a table with very few lines and, more important, with a fixed amount of lines (only 10 lines) then I would do it with SVG text in a UI template node. An auxiliary function would manage an array of elements and have them stored in the flow context.

Flow:

[{"id":"594506cc.51e408","type":"tab","label":"Scrolling text ","disabled":false,"info":""},{"id":"f26df972.5d3ae8","type":"ui_template","z":"594506cc.51e408","group":"1ebe6305.3aa6ad","name":"SVG based template","order":0,"width":"6","height":"6","format":"<style>\n\n    #tex1 {\n        font-weight:bolder;\n        font-size: 12;\n        letter-spacing: 4px;\n        fill: white;\n    }\n    \n</style>\n\n\n<svg height=\"300\" width=\"300\" >\n\n<text id=\"tex1\" x=\"10\" y=\"30\" > {{msg.payload[0]}} </text>\n<text id=\"tex1\" x=\"10\" y=\"50\" > {{msg.payload[1]}} </text>\n<text id=\"tex1\" x=\"10\" y=\"70\" > {{msg.payload[2]}} </text>\n<text id=\"tex1\" x=\"10\" y=\"90\" > {{msg.payload[3]}} </text>\n<text id=\"tex1\" x=\"10\" y=\"110\" > {{msg.payload[4]}} </text>\n<text id=\"tex1\" x=\"10\" y=\"130\" > {{msg.payload[5]}} </text>\n<text id=\"tex1\" x=\"10\" y=\"150\" > {{msg.payload[6]}} </text>\n<text id=\"tex1\" x=\"10\" y=\"170\" > {{msg.payload[7]}} </text>\n<text id=\"tex1\" x=\"10\" y=\"190\" > {{msg.payload[8]}} </text>\n<text id=\"tex1\" x=\"10\" y=\"210\" > {{msg.payload[9]}} </text>\n</svg>\n\n","storeOutMessages":false,"fwdInMessages":false,"templateScope":"local","x":760,"y":380,"wires":[[]]},{"id":"3438664c.1d39ba","type":"function","z":"594506cc.51e408","name":"Create table in context","func":"let sctab = [];\nflow.set(\"sctab\", sctab);\nreturn msg;","outputs":"1","noerr":0,"x":400,"y":220,"wires":[[]]},{"id":"6b543271.7a47fc","type":"inject","z":"594506cc.51e408","name":"","topic":"","payload":"Start","payloadType":"str","repeat":"","crontab":"","once":true,"onceDelay":"5","x":150,"y":220,"wires":[["3438664c.1d39ba"]]},{"id":"67195cf7.a5ea24","type":"function","z":"594506cc.51e408","name":"Add word to scrolling table","func":"let pay = msg.payload;\n\n// Read scrolling table from context\nlet sctab = flow.get(\"sctab\");\n\n// Modify scrolling table by adding msg.payload as first element\nlet size = sctab.unshift(pay);\n\n// Remove last element from scrolling table\nif (size >9) sctab.pop();\n\n// Update context for scrolling text\nflow.set(\"sctab\",sctab);\n\n// Shalow copy updated scrolling table to msg.payload\nmsg.payload = [...sctab];\n\nreturn msg;","outputs":1,"noerr":0,"x":500,"y":380,"wires":[["f26df972.5d3ae8"]]},{"id":"e4b42f5e.2458c","type":"inject","z":"594506cc.51e408","name":"","topic":"","payload":"Alfa","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":250,"y":340,"wires":[["67195cf7.a5ea24"]]},{"id":"d5d60837.4cadd8","type":"inject","z":"594506cc.51e408","name":"","topic":"","payload":"Bravo","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":250,"y":380,"wires":[["67195cf7.a5ea24"]]},{"id":"79f8982e.19f1d8","type":"inject","z":"594506cc.51e408","name":"","topic":"","payload":"Charlie","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":250,"y":420,"wires":[["67195cf7.a5ea24"]]},{"id":"68b03084.d691","type":"inject","z":"594506cc.51e408","name":"","topic":"","payload":"Delta","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":250,"y":460,"wires":[["67195cf7.a5ea24"]]},{"id":"1ebe6305.3aa6ad","type":"ui_group","z":"","name":"LAB","tab":"2d3c18ea.7ea3d8","order":3,"disp":false,"width":"6","collapse":false},{"id":"2d3c18ea.7ea3d8","type":"ui_tab","z":"","name":"Table","icon":"dashboard"}]
5 Likes

You are the man Andrei !!!

That is fantastic thanks - almost perfectly what i want !

regards

Craig

2 Likes

Good to know it helped. If you see any opportunity to improve the code or restyle the dashboard for better please let me know. Perhaps if it is improved I can make it available in the flow library.

Let's make less code lines with usage of ng-repeat

<svg height="300" width="300" > <text id="tex1" ng-repeat="line in msg.payload track by $index" x="10" ng:attr:y="{{$index * 20}}" > {{line}} </text> </svg>

:slight_smile:

Will do - but i think it will be a long time until i can improve on your code !!

regards

Craig

Thanks for that hotNipi - i am sure that is more elegant and tidier - but as a "babe in the woods" i find Andrei's is easier for me to follow and understand.

regards

Craig

I have just used your code and it works but it keeps cutting out part of the first payload... the full text is not shown, only parts of the string come up on the dashboard. like the cuts of some part of the string.. please do you know why this is

Yep. lets shift textlines down a bit
<svg height="300" width="300" > <text id="tex1" ng-repeat="line in msg.payload track by $index" x="10" ng:attr:y="{{($index * 20)+20}}" > {{line}} </text> </svg>

1 Like

Thank you soo mucch... that works.. youre a genius :DD

Oo no I'm not. It's just html :smiley:

Considering that you want a table with very few lines and, more important, with a fixed amount of lines (only 10 lines) then I would do it with SVG text in a UI template node

@Andrei : Do you suggest that for other scenarios another approach would be better suitable? I have an application where the lines in the text box are created dynamically (kind of log messages received asynchronously via mqtt, generated more or less continuously). I would limit the number of lines to be displayed at for example 100, and practically it should be possible to scroll the laast 100 lines stored in a ring buffer.

Hi @abra, I don't know how to do it with SVG (used in previous flow) as this would require complex handling of x, y positions. It's is not difficult to do something with the template node though. I just drafted a solution that seems to work (not thoroughly tested).

You will need to install node-red-contrib-ring-buffer, which is a fine node.

In this flow I use the moment.js library to generate fake time and some additional nodes to generate a fake list of events. The template node expects msg.payload to be an object with the properties time and event but, of course, you will modify this (and everything else) to suit your needs.

Flow:

[{"id":"f6b18fb3.2e17c","type":"tab","label":"Attempt Scrolling widget","disabled":false,"info":"https://groups.google.com/d/topic/node-red/oJyDSrXHvpg/discussion\n\n\n\n```\n    {\n        \"building\": \"home\",\n        \"room\": \"bedroom\",\n        \"device\": \"light\",\n        \"tech\": \"X10\",\n        \"id\": \"C5\",\n        \"type\": \"actuator\",\n        \"desc\": \"x10 device and lamp\"\n    },\n    {\n        \"building\": \"home\",\n        \"room\": \"office\",\n        \"device\": \"light\",\n        \"tech\": \"X10\",\n        \"id\": \"C6\",\n        \"type\": \"actuator\",\n        \"desc\": \"x10 device and lamp\"\n    }\n```\n\n![template-view-object](/nri/template-view-object.png)\n"},{"id":"71c11480.98778c","type":"template","z":"f6b18fb3.2e17c","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<!DOCTYPE html>\n<html>\n    <head></head>\n    <body>\n        <ul>\n            {{#payload}}\n                <li>{{time}}, {{event}}</li>\n            {{/payload}}\n        </ul>\n</body>\n</html>","x":620,"y":180,"wires":[["b6a83bc0.08c928"]]},{"id":"b6a83bc0.08c928","type":"ui_template","z":"f6b18fb3.2e17c","group":"435d299e.91f718","name":"","order":0,"width":"12","height":"3","format":"<style>\n\n    #tex1 {\n        font-weight:bolder;\n        font-family: Menlo, monospace;\n        font-size: 5;\n        letter-spacing: 4px;\n        color: lime;\n    }\n    \n</style>\n<div ng-bind-html=\"msg.payload\" id=\"tex1\"></div>\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":780,"y":180,"wires":[[]]},{"id":"f0a87718.580bb8","type":"comment","z":"f6b18fb3.2e17c","name":"Attempt to create a scrolling widget","info":"","x":180,"y":100,"wires":[]},{"id":"d9b9a91b.d40098","type":"ring-buffer","z":"f6b18fb3.2e17c","name":"","capacity":16,"order":"old-to-new","sendOnlyIfFull":false,"pushAfterClear":false,"extra":false,"x":460,"y":180,"wires":[["71c11480.98778c"]]},{"id":"f1f3829a.64333","type":"inject","z":"f6b18fb3.2e17c","name":"","topic":"","payload":"","payloadType":"date","repeat":"2","crontab":"","once":true,"onceDelay":"5","x":130,"y":180,"wires":[["4722a34d.ba57bc"]]},{"id":"4722a34d.ba57bc","type":"function","z":"f6b18fb3.2e17c","name":"Random object","func":"let mRandom = msg.payload % 26;\nlet moment = global.get('moment'); \nlet tim = moment().format('MMMM Do YYYY, h:mm:ss a');\n\nmsg.payload = {\"time\" : tim, \"event\" : flow.get(\"fakeEvents\")[mRandom]};\nreturn msg;","outputs":1,"noerr":0,"x":300,"y":180,"wires":[["d9b9a91b.d40098"]]},{"id":"1238b2c5.173cbd","type":"function","z":"f6b18fb3.2e17c","name":"Create fake events","func":"\nlet words = [\n    \"Alfa\",\n    \"Bravo\",\n    \"Charlie\", \n    \"Delta\",\n    \"Echo\",\n    \"Foxtrot\",\n    \"Golf\",\n    \"Hotel\",\n    \"India\",\n    \"Juliett\", \n    \"Kilo\", \n    \"Lima\", \n    \"Mike\", \n    \"November\", \n    \"Oscar\", \n    \"Papa\", \n    \"Quebec\", \n    \"Romeo\", \n    \"Sierra\", \n    \"Tango\", \n    \"Uniform\", \n    \"Victor\", \n    \"Whiskey\", \n    \"X-ray\", \n    \"Yankee\", \n    \"Zulu\"\n    \n    ];\n\nflow.set(\"fakeEvents\", words);\n\nmsg.payload = words;\nreturn msg;","outputs":"1","noerr":0,"x":390,"y":280,"wires":[["1c76d58b.f2018a"]]},{"id":"496927ba.28ba08","type":"inject","z":"f6b18fb3.2e17c","name":"","topic":"","payload":"Start","payloadType":"str","repeat":"","crontab":"","once":true,"onceDelay":"0.1","x":130,"y":280,"wires":[["1238b2c5.173cbd"]]},{"id":"1c76d58b.f2018a","type":"debug","z":"f6b18fb3.2e17c","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":580,"y":280,"wires":[]},{"id":"435d299e.91f718","type":"ui_group","z":"","name":"Group 1","tab":"7ac377c5.e6ac08","disp":true,"width":"12","collapse":false},{"id":"7ac377c5.e6ac08","type":"ui_tab","z":"","name":"Tab1","icon":"dashboard"}]
1 Like

I forgot that there is a much better looking solution in Node-RED library:

https://flows.nodered.org/flow/6f164cfd4b548d603c7387b29ed54027

1 Like

Thaks for your sample and for the link. I probably need a combination of both, as the HTML scroll sample does not have a vertical scroll bar to see the older messages. I also have to search a sample about how to select a line from the table by double-clicking it.
Would be possible to open a "detailed view" when a line from the table is double-clicked, and to pass the context (the message payload for the line) to it?

It would be necessary a good amount of time and effort to devolop a widget with such capability. This would require an experienced front end developer and possibly additional libraries being added to the project. Perhaps a better approach is using Node-RED + influx stack (db + other tools).

Is it possible to use bootstrap in a node red ui_template ? If yes, does anyone have a sample or a link to some sample?

Yes, you can use the Angular scope to send the data for each row of your table back to the downstream flow. This discussion has some examples of how to do that.

Put a debug node on the output of your ui_template node that builds the table, so you can see what is being passed back when you click the row. Once you have the right data being returned, you can pass that message to a change node to set the msg.tab property to be the "next page" to show, and send that to to a ui_control node, which then switches the dashboard to that page. At that point you just use the payload from the clicked table row to populate the widgets showing more details -- simple, eh? ;*)

Thanks for your reply.
How could be possible for the vertically scrollable table from the example above to automatically set the vertical scroll position to the newest entry, at the bottom of the table?

1 Like

One technique is to have the table reverse sorted, so newest to oldest -- then any new rows will always be visible at the top of the page.

Another way would be to add a bit of javascript to the page, that gets the last row of the table and scrolls the browser to show it, using the DOM scrollIntoView() method.

1 Like

Thank you for the hint. I got it working (see flow below), but I have some small problems/questions about it.
If I try to access the table element by its id inside scope.$watch() in the UI template, I got a null element

var elmtTable = document.getElementById("trace-table");

I can access the table element only as child of div

var elmt = document.getElementById("div-table");
var nrCh = elmt.children.length;
if (nrCh > 0) {
    // the only div child is the table !
    var elmtChild = elmt.children[0];

Why does this occur?

And the second question is why the table scrolls correctly to the bottom only after the trace buffer size is reached (only when more than 20 trace messages generated, in the example below).

Here is the flow I used:

[{"id":"efd360ba.a6cb","type":"inject","z":"d57cfd26.f61e1","name":"Trace1","topic":"","payload":"{\"t\":\"Trace message one, hello world\",\"l\":1}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":300,"wires":[["e8472d92.442e6"]]},{"id":"8dbbf957.24d1f8","type":"inject","z":"d57cfd26.f61e1","name":"Trace2","topic":"","payload":"{\"t\":\"Trace message two, how are you\",\"l\":1}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":340,"wires":[["e8472d92.442e6"]]},{"id":"cd720de.72391f","type":"inject","z":"d57cfd26.f61e1","name":"Trace3","topic":"","payload":"{\"t\":\"Trace message three, thats for me\",\"l\":3}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":380,"wires":[["e8472d92.442e6"]]},{"id":"a8c47c53.a9065","type":"inject","z":"d57cfd26.f61e1","name":"Trace4","topic":"","payload":"{\"t\":\"Trace message four, lets go !\",\"l\":2}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":420,"wires":[["e8472d92.442e6"]]},{"id":"213a470b.3158f8","type":"inject","z":"d57cfd26.f61e1","name":"Trace5","topic":"","payload":"{\"t\":\"Trace message five, big surprise\",\"l\":4}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":460,"wires":[["e8472d92.442e6"]]},{"id":"e8472d92.442e6","type":"function","z":"d57cfd26.f61e1","name":"set timestamp","func":"function date_format( d, p ) {\n    var pad = function (n, l) {\n        for (n = String(n), l -= n.length; --l >= 0; n = '0'+n);\n        return n;\n    };\n    var tz = function (n, s) {\n        return ((n<0)?'+':'-')+pad(Math.abs(n/60),2)+s+pad(Math.abs(n%60),2);\n    };\n    return p.replace(/([DdFHhKkMmSsyZ])\\1*|'[^']*'|\"[^\"]*\"/g, function (m) {\n        l = m.length;\n        switch (m.charAt(0)) {\n                case 'd': return pad(d.getDate(), l);\n                case 'H': return pad(d.getHours(), l);\n                case 'h': return pad(d.getHours() % 12 || 12, l);\n                case 'K': return pad(d.getHours() % 12, l);\n                case 'k': return pad(d.getHours() || 24, l);\n                case 'm': return pad(d.getMonth() + 1, l );\n                case 'M': return pad(d.getMinutes(), l);\n                case 'S': return pad(d.getMilliseconds(), l);\n                case 's': return pad(d.getSeconds(), l);\n                case 'y': return (l == 2) ? String(d.getFullYear()).substr(2) : pad(d.getFullYear(), l);\n                case 'Z': return tz(d.getTimezoneOffset(), ' ');\n                case \"'\":\n                case '\"': return m.substr(1, l - 2);\n                default: throw new Error('Illegal pattern');\n        }\n    });\n}\n\njs_obj = msg.payload;\n\nvar jstime = date_format(new Date(), 'dd-mm-yyyy HH:MM:ss.SSS');\n//var jstime  = d.format(\"DD-MM-yyyy hh:mm:ss SSS\");\n\njs_obj.timestamp = jstime;\n\nmsg.payload = js_obj ;\n\nreturn msg;\n\n","outputs":1,"noerr":0,"x":420,"y":340,"wires":[["78e5413a.04c8c"]]},{"id":"78e5413a.04c8c","type":"function","z":"d57cfd26.f61e1","name":"handle trace_events","func":"var msg_obj       = msg.payload ;\nvar arr_msgs = flow.get(\"trace_events\", 'memoryOnly');\n\nif (arr_msgs===undefined ) {\n    // Create an empty array if it does not exist yet\n    arr_msgs = [];\n}\n\narr_msgs.push(msg_obj);\nflow.set(\"trace_events\",arr_msgs, 'memoryOnly');\n\nmsg.payload = flow.get(\"trace_events\", 'memoryOnly');\nreturn msg;\n","outputs":1,"noerr":0,"x":440,"y":420,"wires":[["b29f238c.b59e7"]]},{"id":"b29f238c.b59e7","type":"change","z":"d57cfd26.f61e1","name":"sort","rules":[{"t":"set","p":"payload","pt":"msg","to":"($sort(payload,function($l , $r){$l.timestamp > $r.timestamp }) )","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":650,"y":420,"wires":[["14b85e9b.acdff1"]]},{"id":"14b85e9b.acdff1","type":"function","z":"d57cfd26.f61e1","name":"trace buffer","func":"var arr = msg.payload ;\n\nvar bufferSize = 20;\nif(typeof arr === undefined) {\n    return ;\n} else {\n    var arrSize = arr.length;\n    var minSlice = 0;\n    var maxSlice = arrSize;\n    if (arrSize > bufferSize) {\n        minSlice = arrSize - bufferSize;\n    }\n    msg.payload = arr.slice(minSlice, maxSlice);    \n    msg.topic = 'The newest 20 messages :';\n    return msg;\n}","outputs":1,"noerr":0,"x":830,"y":380,"wires":[["88c6a602.0cdba8"]]},{"id":"88c6a602.0cdba8","type":"template","z":"d57cfd26.f61e1","name":"css","field":"payload.style","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"table {\n    color: #333;\n    font-family: Helvetica, Arial, sans-serif;\n    width: 100%;\n    border-collapse: collapse;\n    border-spacing: 0;\n    overflow-x: auto;\n    overflow-y: auto;\n}\ntd, th {\n    border: 1px solid transparent;\n    /* No more visible border */\n    height: 30px;\n    transition: all 0.3s;\n    /* Simple transition for hover effect */\n}\nth {\n    background: #DFDFDF;\n    /* Darken header a bit */\n    font-weight: bold;\n}\ntd {\n    background: #FAFAFA;\n    text-align: center;\n}\n\n/* Cells in even rows (2,4,6...) are one color */\n\ntr:nth-child(even) td {\n    background: #F1F1F1;\n}\n\n/* Cells in odd rows (1,3,5...) are another (excludes header cells)  */\n\ntr:nth-child(odd) td {\n    background: #FEFEFE;\n}\ntr td:hover {\n    background: #666;\n    color: #FFF;\n}\n\n/* Hover cell effect! */","output":"str","x":970,"y":380,"wires":[["fb4adece.6d85b"]]},{"id":"fb4adece.6d85b","type":"template","z":"d57cfd26.f61e1","name":"html","field":"payload","fieldType":"msg","format":"html","syntax":"mustache","template":"<head>\n    <style>\n        {{{payload.style}}}\n    </style>\n</head>\n\n\n<table id=\"trace-table\" border=\"1\">\n    \n    \n    <thead>\n        <tr>\n            <th colspan=\"3\">{{topic}}</th>\n        </tr>\n    </thead>\n    \n    \n    <tr>\n        <th>TimeStamp</th>\n        <th>Trace</th>\n        <th>Level</th>\n\n    </tr>\n    {{#payload}}\n        <tr class=\"\">\n            <td>{{timestamp}}</td>            \n            <td>{{t}}</td>\n            <td>{{l}}</td>\n        </tr>\n    {{/payload}}\n</table>\n","output":"str","x":830,"y":480,"wires":[["5654ee4c.5b0a"]]},{"id":"5654ee4c.5b0a","type":"ui_template","z":"d57cfd26.f61e1","group":"cc45e554.8ef688","name":"Scrolling Traces","order":0,"width":"12","height":"8","format":"<div ng-bind-html=\"msg.payload\" id=\"div-table\" height=\"500\" ></div>\n\n<script>\n    \n    (function(scope) {\n        scope.$watch('msg.payload', function(data) {\n            var elmt = document.getElementById(\"div-table\");\n            console.log(elmt);\n            // this one is null... why?\n            var elmtTable = document.getElementById(\"trace-table\");\n            console.log(elmtTable);\n            var nrCh = elmt.children.length;\n            console.log(\"nrCh: \" + nrCh);\n            if (nrCh > 0) {\n                // the only div child is the table !\n                var elmtChild = elmt.children[0];\n                console.log(elmtChild);\n                elmtChild.scrollIntoView(false);\n                console.log('scroll to bottom');\n            }\n        });\n    })(scope);\n\n</script>","storeOutMessages":false,"fwdInMessages":false,"templateScope":"local","x":1020,"y":480,"wires":[[]]},{"id":"cc45e554.8ef688","type":"ui_group","z":"","name":"Test Traces","tab":"2f7c01d7.51b69e","order":2,"disp":true,"width":"12","collapse":false},{"id":"2f7c01d7.51b69e","type":"ui_tab","z":"","name":"Demo Html Scrollable Table","icon":"dashboard","disabled":false,"hidden":false}]