UI Tabular Table Sorting

Hello,

I have a Tabular Table that gets rows added via this function:

var msg_obj = msg.payload ;
var arr_msgs = flow.get("msg_events", 'memoryOnly');

if (arr_msgs===undefined ) {
    // Create an empty array if it does not exist yet
    arr_msgs = [];
    //arr_msgs.push(msg_obj) ; 
        if (msg_obj !== "1") {
            arr_msgs.push(msg_obj);
            flow.set("msg_events",arr_msgs, 'memoryOnly');
        }
    
//    return msg ;

} else {
        // New row
        if (msg_obj !== "1") {
            arr_msgs.push(msg_obj);
            flow.set("msg_events",arr_msgs, 'memoryOnly');
        }
} 
msg.payload = flow.get("msg_events", 'memoryOnly');
return msg;

I can't figure out how to sort the table onload by one of the columns.

Another problem is that each new row that gets added resets the manual sort.

Also, how do I limit the max number of rows?

Thank you!

I tried looking up Tabular Tables, but I can't find anything on the application in my initial search. It's it for sure Tabular Tables and not Tabulator? Just making sure so I don't pay something that won't work because I'm talking about the wrong script.

sorry, of course, I meant Tabulator

No problem. Just wanted to make sure I was on the same page. Which now begs the question, how is your Tabulator table setup and how are you passing messages? There are several ways to do each and if you're doing it one of the ways I think you are, you're going to have the issues your describing.

This is the flow related to the table

[
    {
        "id": "c444930f52b9ac10",
        "type": "tab",
        "label": "Flow 1",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "ee2126a55132abe2",
        "type": "ui_table",
        "z": "c444930f52b9ac10",
        "group": "be72bda40c2b3554",
        "name": "Screener",
        "order": 1,
        "width": "7",
        "height": "17",
        "columns": [
            {
                "field": "flag",
                "title": "I",
                "width": "2",
                "align": "left",
                "formatter": "plaintext",
                "formatterParams": {
                    "target": "_blank"
                }
            },
            {
                "field": "currency",
                "title": "Coin",
                "width": "9%",
                "align": "center",
                "formatter": "plaintext",
                "formatterParams": {
                    "target": "_blank"
                }
            },
            {
                "field": "exchange",
                "title": "Exchange",
                "width": "12%",
                "align": "center",
                "formatter": "plaintext",
                "formatterParams": {
                    "target": "_blank"
                }
            },
            {
                "field": "percent",
                "title": "Up",
                "width": "10%",
                "align": "center",
                "formatter": "plaintext",
                "formatterParams": {
                    "target": "_blank"
                }
            },
            {
                "field": "window",
                "title": "Window",
                "width": "10%",
                "align": "center",
                "formatter": "plaintext",
                "formatterParams": {
                    "target": "_blank"
                }
            },
            {
                "field": "change",
                "title": "Change",
                "width": "10%",
                "align": "center",
                "formatter": "plaintext",
                "formatterParams": {
                    "target": "_blank"
                }
            },
            {
                "field": "b_coin",
                "title": "Binance",
                "width": "10%",
                "align": "center",
                "formatter": "plaintext",
                "formatterParams": {
                    "target": "_blank"
                }
            },
            {
                "field": "f_coin",
                "title": "FTX",
                "width": "5%",
                "align": "center",
                "formatter": "plaintext",
                "formatterParams": {
                    "target": "_blank"
                }
            },
            {
                "field": "g_coin",
                "title": "Gate.io",
                "width": "5%",
                "align": "center",
                "formatter": "plaintext",
                "formatterParams": {
                    "target": "_blank"
                }
            },
            {
                "field": "volume",
                "title": "Volume",
                "width": "10%",
                "align": "center",
                "formatter": "plaintext",
                "formatterParams": {
                    "target": "_blank"
                }
            },
            {
                "field": "price",
                "title": "Price",
                "width": "10%",
                "align": "center",
                "formatter": "plaintext",
                "formatterParams": {
                    "target": "_blank"
                }
            },
            {
                "field": "time",
                "title": "TIME",
                "width": "18%",
                "align": "center",
                "formatter": "plaintext",
                "formatterParams": {
                    "target": "_blank"
                }
            }
        ],
        "outputs": 1,
        "cts": true,
        "x": 840,
        "y": 440,
        "wires": [
            []
        ]
    },
    {
        "id": "224d895e5ba78e8d",
        "type": "function",
        "z": "c444930f52b9ac10",
        "name": "msg_events",
        "func": "var msg_obj = msg.payload ;\nvar arr_msgs = flow.get(\"msg_events\", 'memoryOnly');\n\nif (arr_msgs===undefined ) {\n    // Create an empty array if it does not exist yet\n    arr_msgs = [];\n    //arr_msgs.push(msg_obj) ; \n        if (msg_obj !== \"1\") {\n            arr_msgs.push(msg_obj);\n            flow.set(\"msg_events\",arr_msgs, 'memoryOnly');\n        }\n    \n//    return msg ;\n\n} else {\n        // New row\n        if (msg_obj !== \"1\") {\n            arr_msgs.push(msg_obj);\n            flow.set(\"msg_events\",arr_msgs, 'memoryOnly');\n        }\n} \nmsg.payload = flow.get(\"msg_events\", 'memoryOnly');\nreturn msg;\n",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 480,
        "y": 400,
        "wires": [
            [
                "838c6c525c2f0253",
                "0b1a84d9dcd9db57"
            ]
        ]
    },
    {
        "id": "aef2f473d5ec09d0",
        "type": "function",
        "z": "c444930f52b9ac10",
        "name": "clear",
        "func": "var cfg = undefined ;\nflow.set('msg_events', cfg, 'memoryOnly');\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 610,
        "y": 360,
        "wires": [
            [
                "ee2126a55132abe2"
            ]
        ]
    },
    {
        "id": "42c7011ea98d8366",
        "type": "inject",
        "z": "c444930f52b9ac10",
        "name": "clear",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": "0.1",
        "topic": "",
        "payload": "1",
        "payloadType": "str",
        "x": 470,
        "y": 360,
        "wires": [
            [
                "aef2f473d5ec09d0"
            ]
        ]
    },
    {
        "id": "838c6c525c2f0253",
        "type": "delay",
        "z": "c444930f52b9ac10",
        "name": "1ms",
        "pauseType": "delay",
        "timeout": "2",
        "timeoutUnits": "milliseconds",
        "rate": "1",
        "nbRateUnits": "1",
        "rateUnits": "second",
        "randomFirst": "1",
        "randomLast": "5",
        "randomUnits": "seconds",
        "drop": false,
        "allowrate": false,
        "outputs": 1,
        "x": 610,
        "y": 400,
        "wires": [
            [
                "ee2126a55132abe2"
            ]
        ]
    },
    {
        "id": "0b1a84d9dcd9db57",
        "type": "change",
        "z": "c444930f52b9ac10",
        "name": "refresh",
        "rules": [
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "[]",
                "tot": "json"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 490,
        "y": 440,
        "wires": [
            [
                "b0efa543f475da8c"
            ]
        ]
    },
    {
        "id": "b0efa543f475da8c",
        "type": "change",
        "z": "c444930f52b9ac10",
        "name": "ui_control",
        "rules": [
            {
                "t": "set",
                "p": "ui_control",
                "pt": "msg",
                "to": "{\t   \"tabulator\": {\t   initialSort:[\t        {column:\"Time\", dir:\"asc\"}\t    ],\t\t       \"columns\": [\t           {\t               \"title\": \"|\",\t               \"field\": \"flag\",\t               \"width\": \"1%\",\t               \"align\": \"center\"             \t           },\t           {\t               \"title\": \"Coin\",\t               \"field\": \"currency\",\t               \"width\": \"9%\",\t               \"align\": \"center\"             \t           },           \t           {\t               \"title\": \"Exchange\",\t               \"field\": \"exchange\",\t               \"width\": \"10%\",\t               \"align\": \"center\"             \t           },\t           {\t               \"title\": \"UP\",\t               \"field\": \"percent\",\t               \"width\": \"9%\",\t               \"align\": \"center\",\t               \"formatter\": \"function(cell, formatterParams){var value = cell.getValue(); if (value !== null) {if(value >= 0){cell.getElement().style.color ='#609f70'} else {cell.getElement().style.color ='#ff4a68'} return value +'%';}}\"             \t           },\t           {\t               \"title\": \"W\",\t               \"field\": \"window\",\t               \"align\": \"center\",\t               \"width\": \"6%\",\t               \"formatter\": \"function(cell, formatterParams, onRendered) {return \\\"<span style='font-size:9pt;'>\\\" + cell.getValue() + \\\"</span>\\\";}\"             \t           },\t           {\t               \"title\": \"24Hr\",\t               \"field\": \"change\",\t               \"width\": \"10%\",\t               \"align\": \"center\",\t               \"formatter\": \"function(cell, formatterParams){var value = cell.getValue(); if (value !== null) {if(value >= 0){cell.getElement().style.color ='#609f70'} else {cell.getElement().style.color ='#ff4a68'} return value +'%';}}\"             \t           },\t           {\t               \"title\": \"B\",\t               \"field\": \"b_coin\",\t               \"width\": \"2%\",\t               \"align\": \"center\",\t               \"formatter\": \"function(cell, formatterParams, onRendered) {return \\\"<span style='color:#2962ff; font-weight:bold;'>\\\" + cell.getValue() + \\\"</span>\\\";}\"             \t           },\t           {\t               \"title\": \"F\",\t               \"field\": \"f_coin\",\t               \"width\": \"2%\",\t               \"align\": \"center\"             \t           },\t           {\t               \"title\": \"G\",\t               \"field\": \"g_coin\",\t               \"width\": \"2%\",\t               \"align\": \"center\"             \t           },\t           {\t               \"title\": \"Volume\",\t               \"field\": \"volume\",\t               \"width\": \"11%\",\t               \"align\": \"right\",\t               \"formatter\": \"money\",\t               \"formatterParams\": {\t                   \"thousand\":\",\",\t                \"precision\": \"false\"\t                }               \t           },\t           {\t               \"title\": \"Price\",\t               \"field\": \"price\",\t               \"width\": \"10%\",\t               \"align\": \"right\",\t               \"formatter\": \"money\",\t               \"formatterParams\": {\t                   \"thousand\":\",\"\t                }\t           },\t           {\t               \"title\": \"Time\",\t               \"field\": \"time\",\t               \"width\": \"11%\",\t               \"align\": \"center\",         \t               \"formatter\": \"datetime\",\t               \"formatterParams\": {\t                   \"inputFormat\": \"x\",\t                   \"outputFormat\": \"hh:mm a\",\t                   \"invalidPlaceholder\": \"(invalid time)\"\t                },\t                \"sorterParams\": {\t                   \"format\": \"hh:mm a\",\t                   \"dir\" : \"desc\",\t                   \"sorter\": \"time\"\t                }\t\t           }         \t       ],\t       \"item\": \"\",\t       \"layout\": \"fitColumns\",\t       \"movableColumns\": false\t   } \t}",
                "tot": "jsonata"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 620,
        "y": 440,
        "wires": [
            [
                "ee2126a55132abe2"
            ]
        ]
    },
    {
        "id": "d2109d28258526cd",
        "type": "ui_button",
        "z": "c444930f52b9ac10",
        "name": "",
        "group": "085d7feec44f7e35",
        "order": 9,
        "width": 1,
        "height": "1",
        "passthru": false,
        "label": "table",
        "tooltip": "",
        "color": "#ccc",
        "bgcolor": "",
        "className": "",
        "icon": "",
        "payload": "1",
        "payloadType": "str",
        "topic": "",
        "topicType": "str",
        "x": 510,
        "y": 320,
        "wires": [
            [
                "aef2f473d5ec09d0"
            ]
        ]
    },
    {
        "id": "be72bda40c2b3554",
        "type": "ui_group",
        "name": "RIGHT",
        "tab": "7e13e6107768d821",
        "order": 4,
        "disp": false,
        "width": "7",
        "collapse": false,
        "className": ""
    },
    {
        "id": "085d7feec44f7e35",
        "type": "ui_group",
        "name": "LEFT",
        "tab": "7e13e6107768d821",
        "order": 2,
        "disp": false,
        "width": "4",
        "collapse": false,
        "className": ""
    },
    {
        "id": "7e13e6107768d821",
        "type": "ui_tab",
        "name": "CRYPTO",
        "icon": "dashboard",
        "order": 2,
        "disabled": false,
        "hidden": false
    }
]

Didn't mean to leave you hanging. Took a bit to get back to this and look at the flows and work my way through the understanding.

The main problem you're having with your table is that ui-table is based on a partial wrap of Tabulator that existed over two years ago. The commands you can pass in are limited and you have to know the exact version of Tabulator they implemented, which unfortunately isn't listed, to use the commands correctly. There's a lot that's been updated in Tabulator since then, including how it interfaces with everything else, that makes it a much better implementation. One problem you're going to have is that there is little you can do with ui-table to make it so the data refresh after doing stuff like adding rows doesn't redo everything on the table. All the work I've done with ui-table didn't find anything currently in it that will stop this.

That being said. How willing are you to ditch ui-table and put Tabulator itself in its native form into your flow? It's easy to do and a lot of these problems you're experiencing now will go away just by switching. And you'll get native Tabulator functionality which far surpasses anything ui-table can do.

Hi, thank you for following up. I am VERY willing to ditch my current config since it's indeed quite buggy. I've haven't been able to find an example that I could understand/adapt. The functionality is simple:

  1. Table is generated by adding new rows (where max number of rows can be specified); this is the payload that's coming in from a function:
{ "currency": currency, "exchange": exchange, "f_coin": f_coin, "g_coin": g_coin, "b_coin": b_coin, "percent": percent, "window": window, "change": change, "price": price, "volume": volume, "time": msg.time, "flag": flag }
  1. The newly added rows remain on top (i.e. desc sort by timestamp).

Thanks again!

No problem. Check out this post here where I showed someone else how to get started with it. At first, you'll need two template nodes. Everything for the table is setup in those with a little function that allows a node connection to the one housing the Tabulator code. You can literally take code from the tabulator.info page and paste it into the ui-template node and have it work. There's lots of documentation available.

Depending on your setup, you might also be able to move your code into the ui-template itself and run everything in that. The caveat with that though is that the ui-template node runs everything on the client and not on the server. So if the client is gathering data from other sources and manipulating it in the ui-template node, the server won't know about it unless you export the data. Importing data into the node is as simple as connecting the data node to the ui-template node and handling the data in the little interface function. On the plus side though, you don't have to add new rows and delete old ones. You can simply just update the row in question with new data and not change a thing, if that's what you want to do. There's tons of examples and documentation on the web page with code that can simply be copied and it works.

There's a bit of a learning curve to doing it this way, but I think you'll pick it up quick. Let me know if/where you get stuck and I'll help you through it.

I might have seen this, but I couldn't figure out how to modify it so that rows get added from payload input. So lets say I have a function sending out the following:

msg.payload = { "currency": currency, "exchange": exchange, "time":msg.time}

and the table from your example:

<div id="example-table"></div>
<script>
    //Copy/paste Tabulator examples directly from the web page here.
    //You will need example data to use them directly.
    //Otherwise copy the examples and alter to your needs.
    //Most of the following is directly from the quickstart page.
    
    var tabledata = [
    	{id:1, currency:"BTC", exchange:"FTX", time:"time"},
    ];
    
    var table = new Tabulator("#example-table", {
     	height:205, // set height of table (in CSS or here), this enables the Virtual DOM and improves render speed dramatically (can be any valid css height value)
     	data:tabledata, //assign data to table
     	layout:"fitColumns", //fit columns to width of table (optional)
     	columns:[ //Define Table Columns
    	 	{Currency:"Name", field:"currency", width:150},
    	 	{title:"Exchange", field:"exchange", hozAlign:"left"},
    	 	{title:"Time", field:"time", sorter:"time", hozAlign:"center"},
     	],
    });
    
    table.on("rowClick", function(e, row){ 
    	alert("Row " + row.getData().id + " Clicked!!!!");
    });
    
    //This is needed to interact with Node-Red when a message is sent to the ui-template.
    (function(scope){
        scope.$watch('msg', function(msg){
            if(msg){
                //do something here when a message arrives like table.setData();
            }
        });
    })(scope);
</script>

SO the magic you're looking for happens in this block here:

//This is needed to interact with Node-Red when a message is sent to the ui-template.
    (function(scope){
        scope.$watch('msg', function(msg){
            if(msg){
                //do something here when a message arrives like table.setData();
            }
        });
    })(scope);

What you'll need to do is add something in the if statement in that block to react to the message you sent, such as this modified from the version on the Tabulator site to work with msg.payload:

//This is needed to interact with Node-Red when a message is sent to the ui-template.
    (function(scope){
        scope.$watch('msg', function(msg){
            if(msg){
                table.addData([msg.payload], true); //Your new reaction code
            }
        });
    })(scope);

Or, if you were to pass in your values as an array like this:

msg.payload = [{ "currency": currency, "exchange": exchange, "time":msg.time}];

You would just modify the reaction code to this:

table.addData(msg.payload, true);

The function reads the message coming into the node, takes msg.payload and adds the payload to the top of the table (the true in the addData function). There are several other functions in there you can use as well. Some on that same page allow you to replace the data in the table based on index, which you can store in your Node-Red side of the program and pass along as necessary. It all depends on what you're trying to get your table to do and what features need to be utilized to do it.

Ok, I understand, added the action ... but for whatever reason the rows are not getting added :frowning:

Also, does the var tabledata = [{id:1, currency:"BTC", exchange:"FTX", time:"time"},] need to be modified? in your example you had 3 rows of data specified, what is the purpose of that?

P.S. why is the "Header" and "Tabulator" two different template nodes that are not connected?

That's the easiest question to answer so far. The header template node is configured to inject its contents into the header of the dashboard page you load. That's what loads the Tabulator scripts and configurations. The other template node is configured to be a body component. Since the two are injected into two different areas of the web page, they have to have a different node for each one of those areas. If you wanted to group different Tabulator tables into different dashboard groups or tabs, you would need a different node for each group or tab. Hence the multiple template nodes.

As you have it written, it shouldn't. The only issue there could be is the comma at the end of your object. But Javascript should deal with that just fine. I've never had it complain before. But no guarantees. As it's written though, it *should* populate. I'm not on my system right now where I can test it, so I can't verify what you're seeing.

You would have to point out those rows. Not sure I'm seeing what you're seeing...

That being said, there is something else you're going to have to get used to using ui-template. Unless you pass out errors and such, you'll never see them in Node-Red. What you're going to want to get used to is when you're not seeing something on your web page correctly, open the developer's console (F12) and view the console.
image
In there, you'll see any errors that happen in the locally loaded Javascript. That should help clue you towards anything that's wrong inside your ui-template node. Check that out. It may have the error you're looking for that will tell you what's wrong. Barring that, post the contents of your flow and I'll see if I can make the same error happen on my end.

I actually saw it working "once" but can't get it back. I put back the original tabular node example, without any changes ... seeing these errors;

VM1932:1391 Invalid column definition option in 'age' column: hozAlign
(anonymous) @ VM1932:1391

VM1932:1391 Invalid column definition option in 'dob' column: hozAlign

VM1935:27 Uncaught TypeError: table.on is not a function
at :27:11
at b (app.min.js:20:866)
at He (app.min.js:20:48376)
at S.fn.init.append (app.min.js:20:49727)
at S.fn.init. (app.min.js:20:50819)
at B (app.min.js:20:32425)
at S.fn.init.html (app.min.js:20:50497)
at app.min.js:592:532
at m.$digest (app.min.js:174:351)
at m.$apply (app.min.js:177:484)

This is from your example; dummy data for 5 rows
var tabledata = [
{id:1, name:"Oli Bob", age:"12", col:"red", dob:""},
{id:2, name:"Mary May", age:"1", col:"blue", dob:"14/05/1982"},
{id:3, name:"Christine Lobowski", age:"42", col:"green", dob:"22/05/1982"},
{id:4, name:"Brendon Philips", age:"125", col:"orange", dob:"01/08/1980"},
{id:5, name:"Margret Marmajuke", age:"16", col:"yellow", dob:"31/01/1999"},

I don't understand how that works in my case. Do I need to specify dummy data as well? For how many rows? In my current config number of rows is not known in advance.

No. No dummy data is needed. In fact, you can leave the data option out of the table setup completely and have no adverse effects. Tabulator will just build an empty table with whatever is defined and then just respond to whatever commands you give. The example data I had was just straight from the Tabulator site so that the example could be seen working if copied.

I find that one of the "faults" (not necessarily a fault, just not natively handled) of Javascript is that if you don't have stuff in your page to handle refreshes and whatnot, you're going to have problems with your script. One of the problems with running Tabulator this way is you can't refresh your page and have things work correctly as I've shown you. You have to implement other coding to handle the page refresh and reloading the background scripts, etc. That's beyond my knowledge as I have that problem too. But I have found, in my case, that switching to another tab and back will generally reset things. Also, either restarting your flows or closing the dashboard tab in the browser and opening another in its place will help too.

As long as you start off with a fresh dashboard and don't use browser refresh, you should be ok.

One question that occurs to me. Do you have any instances of ui-table running anywhere else in your instance of Node-Red? If so, that will mess EVERYTHING up. UI-table loads its scripts before Tabulator so you'll run on the old ui-table based version of Tabulator which does not work the same. That's what I'm seeing in the "table.on is not a function" error. That wasn't a feature in the ui-table days and only came out afterward.

I finally got it work. And one of the issues was indeed other UI-table nodes. So now it seems I'll need to redo those, one in particular is tricky (will ask in a follow up). A few remaining issues on this one:

  1. In my original table I had functionality of sending msg payload with all variables on row click. In your example you have a pop-window; what needs to be added?

  2. I can't format the time field. I tried exact same Tabulator formatter that worked perfectly on the original table but it has no effect here. I noted the reference to needing "luxon.js" which I ended up installing (even though it was working on the original table) but hasn't helped.

{title:"Example", field:"example", formatter:"datetime", formatterParams:{
inputFormat:"x",
outputFormat:"hh:mm"
}}

  1. Some of my formatted cells are getting displayed is "undefined". In the original table I was able to figure out how to replace that with blanks; can' get it to work here.

  2. And finally, do I understand correctly that with the Tabulator the data always disappears on page refresh, there is no reasonably easy way to address that? It's actually quite problematic in my case :frowning: Also, is there a way to set the limit on how many rows get added (otherwise the table will just keep growing)?

THANK YOU!

Very simple. Here is a list of everything triggered on the table. Pay particular attention to the row click event listener under the Row Events section to add row click functionality. Getting your data out can be done in several ways. There are some interactions you can do with the ui-template node itself, but I don't know what those are yet. The method I found is to use the HTTP in/out nodes. You should already have them as I believe they're native to Node-Red itself. Most of what I'll describe I'm doing from memory as I don't have my setup with me. Simply setup a HTTP in node that uses a POST setup (as opposed to a GET setup), give it a unique address (this will go on the end of your Node-Red IP, such as http://localhost:1880/some-post-url where some-post-url will be your address) and post to that address to get data out. The only caveat is that you have to have a HTTP out or response node (I forget specifically) to send the acknowledgement back to the call in the template node. Inside the template node itself, you can use the fetch() function of Javascript to POST data to the HTTP node address. Again, I don't have my code, but there is tons of documentation on fetch() for posting to HTTP. It's really helpful to build a function that can be referenced repeatedly if you're using a lot of POST calls. And it doesn't matter what ui-template node you put it in either. All ui-template body nodes can access all other body node's functions. Which also means you'll need to call all of your tables something besides "table" and adjust all their events to match accordingly... (I'll save you the heartache of trying to debug that for hours...)

Date-time formatter This is probably what you referenced. Yes, luxon will need to be installed and/or referenced in the head section. If you are using my base code (which I think you are), you're already calling Tabulator in the head section using a separate storage server. Luxon would need to be run directly from the Node-Red directories. I haven't had to figure this out yet as I run Tabulator remotely, so I can't say how you do this. But there is stuff about how to do that online. Once you know how to run/call Luxon, your formatter will work. It will take adding luxon.js to your header and using the developer's console to see if it loaded successfully or not. Let me know if you still have problems with this one outside of Luxon itself.

This one is simple. Declare emptyValue in your column definition to default the value not declared. Look here for an example on how it's implemented in the list environment. There are other places where it's an example, but that's the first one I found.

Part 1: I have a table that has filter fields at the top of it where I can filter each column for specific text. I can start typing in that text and if I update the data, it keeps the filter and applies it to the new data. When you load data, it should maintain whatever is happening on the table. Now if you're talking about a browser refresh, you're going to have some trouble. You have to get away from browser refreshes while using Tabulator unless/until you figure out how to handle page refreshes so it reloads the Tabulator source and then re-runs all your node scripts to rebuild all your tables. It's possible to handle, but it's beyond the scope of what I know how to do. Fortunately for me, I can switch tabs and have everything refresh, but that isn't the case if someone does a browser refresh. If you really need it, you'll have to search online for someone who knows how to handle that in that way. Like I said, it's possible. I just don't know how to do it. If you can get away from refreshes, that would be my advice.
Part 2: While I haven't found a particular row limit option, I doubt one exists. I could be wrong, but I doubt it. Here's the reason why. Most table software allows you to store and manipulate the data you put into it. There's nothing the table software does other than format the table and run functions. It doesn't manipulate the data for you unless you tell it to. That being said, you've set yourself up for a very easy solution. You're putting new data at the top so the oldest data is at the bottom. When you add a row to the table, just use the function you're using to add a row also check for the number of rows in the table. If the number of rows equals or exceeds a certain amount, delete the row equal to the bottom row (there is documentation for this in Tabulator). You can do this check before or after you add your row, it doesn't matter. A couple lines of code will do what you're describing and keep your table data cleaned up automatically.

Hi, thanks for the detailed response - unfortunately I am not having any success.

  1. I added to the header but has not helped.

  2. I looked at the emptyValue example but I don't understand how it's used / what the "list environment is - placing it as-as in the column didn't work.

  3. And I can't figure out the row click function, which is critical to my application (it's useless without it) - I had it working in the previous table and I don't recall doing anything to set it. Can you provide me with a working example please?

This is not an answer to your issue, just an observation. When getting a context variable in a function node, you can use || to set a condition. So in your function node you could use
var arr_msgs = flow.get("msg_events")||[];
which will set arr_msgs to an empty array if the flow variable 'msg_events' does ot exist. This would change your flow from:

var msg_obj = msg.payload;
var arr_msgs = flow.get("msg_events")||[];

if (arr_msgs === undefined) {
    // Create an empty array if it does not exist yet
    arr_msgs = [];
    //arr_msgs.push(msg_obj) ; 
    if (msg_obj !== "1") {
        arr_msgs.push(msg_obj);
        flow.set("msg_events", arr_msgs);
    }

    //    return msg ;

} else {
    // New row
    if (msg_obj !== "1") {
        arr_msgs.push(msg_obj);
        flow.set("msg_events", arr_msgs);
    }
}
msg.payload = flow.get("msg_events");
return msg;

to:

var msg_obj = msg.payload;
var arr_msgs = flow.get("msg_events")||[];

if (msg_obj !== "1") {
    arr_msgs.push(msg_obj);
    flow.set("msg_events", arr_msgs);
 }

msg.payload = flow.get("msg_events");
return msg;

which should be much easier to maintain in the future.